use std::f64::consts::TAU;
use super::OPUS;
use crate::Spell;
use crate::kit::{WHITE, bump, ease, frac, noise, real};
use crate::paint::{Ink, Palette, Rgb};
use crate::recipe::{Glyphs, Recipe};
use crate::stage::{Canvas, Shader, Stage, TICK_SECS};
pub(super) const RECIPE: Recipe = Recipe {
name: "Caterpillar",
blurb: "an inchworm crawls; rest gives wings",
credit: OPUS,
needs: Glyphs::BASE,
wide: true,
summon: |seed| Box::new(Caterpillar::new(seed)),
};
const LARVA: Rgb = Rgb(118.0, 204.0, 82.0);
const LARVA_HEAD: Rgb = Rgb(198.0, 240.0, 96.0);
const JADE: Rgb = Rgb(86.0, 200.0, 150.0);
const GILT: Rgb = Rgb(240.0, 202.0, 82.0);
const MONARCH: Rgb = Rgb(255.0, 142.0, 38.0);
const EGG: Rgb = Rgb(244.0, 236.0, 212.0);
const LEAF: Rgb = Rgb(96.0, 168.0, 84.0);
const MEAL: u64 = 240;
const LONGEST: usize = 7;
const RIPENING: f64 = 650.0;
const GUSTS: usize = 6;
enum Life {
Egg,
Larva {
size: usize,
fed: u64,
at: f64,
gait: f64,
},
Pupa { ripe: f64 },
Imago {
eclosed: f64,
flap: f64,
gusts: [f64; GUSTS],
next: usize,
},
}
struct Caterpillar {
seed: i64,
life: Life,
}
impl Caterpillar {
fn new(seed: u64) -> Self {
Self {
seed: seed.cast_signed(),
life: Life::Egg,
}
}
}
fn stride(size: usize) -> usize {
size.saturating_sub(2).max(1)
}
fn body(size: usize, at: f64, gait: f64) -> (f64, f64, f64) {
let stride = real(stride(size));
let head = at + real(size - 1);
if gait < 0.5 {
let draw = ease(gait / 0.5);
(at + stride * draw, head, draw)
} else {
let stretch = ease((gait - 0.5) / 0.5);
(at + stride, head + stride * stretch, 1.0 - stretch)
}
}
fn open(flap: f64) -> bool {
frac(flap / 2.0) < 0.5
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "Positions along a caption are nonnegative and far inside usize."
)]
fn whole(x: f64) -> usize {
x.round() as usize
}
impl Shader for Caterpillar {
fn tick(&mut self, stage: &Stage) {
let live = stage.spell != Spell::Idle;
let busy = stage.busy();
let zest = (stage.flow( 0.5) / 70.0).min(1.5);
match &mut self.life {
Life::Egg => {
if live {
self.life = Life::Larva {
size: 2,
fed: 0,
at: 0.0,
gait: 0.0,
};
}
}
Life::Larva {
size,
fed,
at,
gait,
} => {
*fed += u64::from(stage.fresh());
if *fed >= MEAL && *size < LONGEST {
*fed -= MEAL;
*size += 1;
}
let pace = if busy {
0.7 + 0.7 * zest
} else if live {
0.12
} else {
0.0
};
*gait += TICK_SECS * pace;
if *gait >= 1.0 {
*gait -= 1.0;
*at += real(stride(*size));
}
}
Life::Pupa { ripe } => {
if live {
*ripe += f64::from(stage.fresh()) / RIPENING;
}
if *ripe >= 1.0 && busy {
self.life = Life::Imago {
eclosed: stage.t,
flap: 0.0,
gusts: [f64::NEG_INFINITY; GUSTS],
next: 0,
};
}
}
Life::Imago {
flap, gusts, next, ..
} => {
let beat = flap.floor();
let pace = if busy {
5.0 + 3.0 * zest
} else if live {
0.7
} else {
0.25
};
*flap += TICK_SECS * pace;
if live && flap.floor() > beat && open(*flap) {
gusts[*next] = stage.t;
*next = (*next + 1) % GUSTS;
}
}
}
}
fn idle(&mut self, _stage: &Stage) {
match self.life {
Life::Larva { .. } => self.life = Life::Pupa { ripe: 0.0 },
Life::Imago { .. } => self.life = Life::Egg,
Life::Egg | Life::Pupa { .. } => {}
}
}
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas) {
let t = stage.t;
let base = palette.grey(if stage.busy() { 0.66 } else { 0.5 });
let monarch = palette.legible(MONARCH);
let stirred = noise(t * 0.5, self.seed) > 0.62;
let leaf = Ink::of(if stirred { '❧' } else { '❦' }).fg(palette.legible(LEAF));
match self.life {
Life::Egg => {
canvas.letters = stage.letters(|_, _| Ink::default().fg(base));
let pulse = 0.8 + 0.2 * (TAU * t / 3.2).sin();
let egg = palette.legible(EGG).blend(palette.grey(0.35), pulse);
canvas.mark = vec![leaf, Ink::of('∘').fg(egg)];
}
Life::Larva { size, at, gait, .. } => {
let larva = palette.legible(LARVA);
let head = palette.legible(LARVA_HEAD);
let (tail, front, arch) = body(size, at, gait);
let period = stage.caption.len() + size + 3;
let first = whole(tail);
let reach = whole(front) - first;
let first = first % period;
let nibbled = palette
.grey(0.3)
.blend(base, 0.5 + 0.5 * (TAU * 5.0 * t).sin());
canvas.letters = stage.letters(|i, _| {
let rel = (i + period - first) % period;
if rel > reach {
let nibbling = rel == reach + 1 && arch > 0.3;
Ink::default().fg(if nibbling { nibbled } else { base })
} else if rel == reach {
Ink::default().fg(head).bold(true).underline(head)
} else if rel == 0 || arch <= 0.3 {
Ink::default().fg(base).underline(larva)
} else {
Ink::default().fg(larva.blend(base, 0.4 + 0.6 * arch))
}
});
canvas.mark = vec![leaf, Ink::of(' ')];
}
Life::Pupa { ripe } => {
canvas.letters = stage.letters(|_, _| Ink::default().fg(base));
let shell = monarch
.blend(palette.grey(0.25), 0.7)
.blend(palette.legible(JADE), ripe.clamp(0.0, 1.0));
let glint = frac(t / 2.3) < 0.06;
let girdle = palette.legible(if glint { WHITE } else { GILT });
canvas.mark = vec![leaf, Ink::of('θ').fg(shell).underline(girdle)];
}
Life::Imago {
eclosed,
flap,
gusts,
..
} => {
let burst = t - eclosed;
canvas.letters = stage.letters(|i, _| {
let i = real(i);
let sweep = if burst < 1.2 {
0.9 * bump(i - burst * 25.0, 3.0)
} else {
0.0
};
let wind = gusts
.iter()
.map(|launch| t - launch)
.filter(|age| *age < 3.0)
.map(|age| 0.75 * bump(i - age * 12.0, 2.0) * (-age / 0.9).exp())
.fold(sweep, f64::max);
Ink::default()
.fg(monarch.blend(base, wind))
.bold(wind > 0.6)
});
let (left, right) = if open(flap) {
('ʚ', 'ɞ')
} else {
('ɛ', 'ɜ')
};
canvas.mark = vec![
Ink::of(left).fg(monarch).bold(true),
Ink::of(right).fg(monarch).bold(true),
];
}
}
}
fn bullet(&self, _stage: &Stage, palette: Palette) -> Ink {
let (glyph, tone) = match self.life {
Life::Egg => ('∘', EGG),
Life::Larva { .. } => ('•', LARVA),
Life::Pupa { .. } => ('•', JADE),
Life::Imago { flap, .. } => ('•', if open(flap) { GILT } else { MONARCH }),
};
Ink::of(glyph).fg(palette.legible(tone))
}
}