use std::f64::consts::TAU;
use unicode_width::UnicodeWidthStr;
use super::OPUS;
use crate::Spell;
use crate::kit::{Rng, bump, lag, noise, real, smooth};
use crate::paint::{BACKDROP, Ink, Palette, Rgb};
use crate::recipe::{Glyphs, Recipe};
use crate::stage::{Canvas, Shader, Stage, TICK_SECS};
pub(super) const RECIPE: Recipe = Recipe {
name: "Dandelion",
blurb: "work sets the seeds adrift",
credit: OPUS,
needs: Glyphs::BASE,
wide: true,
summon: |seed| Box::new(Dandelion::new(seed)),
};
const PAPPUS: Rgb = Rgb(238.0, 238.0, 230.0);
const BLOOM: Rgb = Rgb(255.0, 212.0, 60.0);
const STALK: Rgb = Rgb(126.0, 172.0, 96.0);
const CLOCKS: [char; 5] = ['·', '*', '✷', '✹', '✺'];
const HEAD: usize = 48;
const FARE: u32 = 50;
const DRIFTERS: usize = 12;
const LEAD: f64 = 2.0;
const SKY: f64 = 30.0;
const BARE: f64 = 5.0;
const IDLE_BLOOM: f64 = 2.5;
const BUSY_BLOOM: f64 = 9.0;
const STRAY: f64 = 9.0;
#[derive(Clone, Copy)]
struct Seed {
x: f64,
v: f64,
spin: f64,
}
#[derive(Clone, Copy)]
enum Head {
Clock { left: usize, owed: u32 },
Bare { since: f64 },
Flower { bloom: f64 },
}
struct Dandelion {
seed: i64,
rng: Rng,
head: Head,
wind: f64,
seeds: [Option<Seed>; DRIFTERS],
}
impl Dandelion {
fn new(seed: u64) -> Self {
Self {
seed: seed.cast_signed(),
rng: Rng::new(seed),
head: Head::Clock {
left: HEAD,
owed: 0,
},
wind: 1.0,
seeds: [None; DRIFTERS],
}
}
fn loose(&mut self, v: f64) {
if let Some(slot) = self.seeds.iter_mut().find(|slot| slot.is_none()) {
*slot = Some(Seed {
x: 0.5 + self.rng.unit(),
v,
spin: TAU * self.rng.unit(),
});
}
}
fn aloft(&self) -> impl Iterator<Item = &Seed> {
self.seeds.iter().flatten()
}
fn lift(&self, column: f64) -> f64 {
self.aloft()
.map(|seed| bump(column - seed.x, 1.3) * (0.75 + 0.25 * seed.spin.sin()))
.fold(0.0, f64::max)
}
fn head(&self, palette: Palette) -> Ink {
let pappus = palette.legible(PAPPUS);
let yellow = palette.legible(BLOOM);
match self.head {
Head::Clock { left, .. } => {
Ink::of(CLOCKS[(left / 10).min(CLOCKS.len() - 1)]).fg(pappus)
}
Head::Bare { .. } => Ink::of('ı').fg(palette.legible(STALK)),
Head::Flower { bloom } if bloom > 0.45 => Ink::of('❋').fg(yellow).bold(true),
Head::Flower { bloom } if bloom > 0.2 => {
Ink::of('✾').fg(yellow.blend(palette.legible(STALK), 0.4))
}
Head::Flower { bloom } => {
Ink::of('✺').fg(pappus.blend(palette.grey(0.3), 1.0 - bloom / 0.2))
}
}
}
}
fn width(stage: &Stage) -> f64 {
real(stage.caption.iter().map(UnicodeWidthStr::width).sum())
}
impl Shader for Dandelion {
fn tick(&mut self, stage: &Stage) {
let idle = stage.spell == Spell::Idle;
let gust = 0.6 + 0.8 * noise(stage.t * 0.35, self.seed);
let wind = if stage.busy() {
3.0 + 4.0 * (stage.flow( 0.6) / 70.0).min(1.5)
} else {
1.2
};
self.wind = lag(self.wind, wind * gust, 0.6);
let sky = LEAD + width(stage) + SKY;
let wind = self.wind;
for slot in &mut self.seeds {
*slot = slot.and_then(|mut seed| {
seed.x += TICK_SECS * wind * seed.v;
seed.spin += TICK_SECS * 3.0;
(seed.x <= sky).then_some(seed)
});
}
self.head = match self.head {
Head::Clock { mut left, mut owed } => {
owed += stage.fresh();
while owed >= FARE && left > 0 {
owed -= FARE;
left -= 1;
let v = 0.6 + 0.7 * self.rng.unit();
self.loose(v);
}
if idle && self.rng.chance(TICK_SECS / STRAY) {
let v = 0.4 + 0.4 * self.rng.unit();
self.loose(v);
}
if left == 0 {
Head::Bare { since: 0.0 }
} else {
Head::Clock { left, owed }
}
}
Head::Bare { since } if since + TICK_SECS > BARE => Head::Flower { bloom: 1.0 },
Head::Bare { since } => Head::Bare {
since: since + TICK_SECS,
},
Head::Flower { bloom } => {
let bloom = bloom - TICK_SECS / if idle { IDLE_BLOOM } else { BUSY_BLOOM };
if bloom > 0.0 {
Head::Flower { bloom }
} else {
Head::Clock {
left: HEAD,
owed: 0,
}
}
}
};
}
fn idle(&mut self, _stage: &Stage) {
if !matches!(self.head, Head::Flower { .. }) {
self.head = Head::Flower { bloom: 1.0 };
}
}
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas) {
let white = palette.legible(PAPPUS);
let base = palette.grey(if stage.busy() { 0.7 } else { 0.5 });
canvas.mark = vec![self.head(palette)];
let mut column = LEAD;
canvas.letters = stage.letters(|_, letter| {
let lift = self.lift(column);
column += real(letter.width());
Ink::default().fg(white.blend(base, lift)).bold(lift > 0.6)
});
let end = column;
let far = self
.aloft()
.filter(|seed| seed.x > end - 1.5)
.map(|seed| (seed.x + 1.5).ceil())
.fold(0.0, f64::max);
if far > end {
canvas.backdrop = (0..BACKDROP)
.map(real)
.take_while(|&c| c < far)
.map(|c| {
if c < end {
return palette.bg;
}
let glint = self
.aloft()
.map(|seed| bump(c - seed.x, 0.8) * smooth(0.5, 0.95, seed.spin.sin()))
.fold(0.0, f64::max);
white.blend(palette.bg, 0.12 * glint * smooth(end + SKY, end + 6.0, c))
})
.collect();
}
}
fn bullet(&self, _stage: &Stage, palette: Palette) -> Ink {
self.head(palette)
}
}