use super::OPUS;
use crate::Spell;
use crate::kit::{AZURE, CORAL, CYAN, LIME, Lottery, MINT, PINK, STEEL, SUN, VIOLET, chance, frac};
use crate::paint::{Ink, Palette, Rgb};
use crate::recipe::{Glyphs, Recipe};
use crate::rest::LULL;
use crate::stage::{Canvas, Shader, Stage, TICK_SECS};
pub(super) const RECIPE: Recipe = Recipe {
name: "Critter",
blurb: "a small face reacts to the output",
credit: OPUS,
needs: Glyphs::BASE,
summon: |_| Box::<Critter>::default(),
};
const SNEEZE: Lottery = Lottery {
every: 13.0,
odds: 0.15,
span: 0.9,
seed: 5,
};
const STIR: Lottery = Lottery {
every: 4.0 * LULL,
odds: 0.3,
span: 0.5 * LULL,
seed: 11,
};
const WINK: Lottery = Lottery {
every: 7.0,
odds: 0.3,
span: 0.5,
seed: 3,
};
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mood {
Happy,
Content,
Surprised,
Alarmed,
Pondering,
Wink,
Sneeze,
Sleepy,
Asleep,
}
impl Mood {
fn face(self) -> (&'static str, Rgb) {
match self {
Mood::Happy => ("◕‿◕", SUN),
Mood::Content => ("–‿–", LIME),
Mood::Surprised => ("◕o◕", CYAN),
Mood::Alarmed => ("⊙ο⊙", CORAL),
Mood::Pondering => ("◔_◔", AZURE),
Mood::Wink => ("◕‿-", PINK),
Mood::Sneeze => (">ω<", MINT),
Mood::Sleepy => ("˘‿˘", VIOLET),
Mood::Asleep => ("–_–", STEEL),
}
}
}
#[derive(Default)]
struct Critter {
happy: bool,
tint: Option<[f64; 11]>,
}
impl Critter {
fn tint(&self, palette: Palette) -> Rgb {
let Some(weights) = self.tint else {
return palette.fg;
};
let moods = [
Mood::Happy,
Mood::Content,
Mood::Surprised,
Mood::Alarmed,
Mood::Pondering,
Mood::Wink,
Mood::Sneeze,
Mood::Sleepy,
Mood::Asleep,
];
let tones = moods.map(|mood| mood.face().1.blend(palette.fg, 0.4));
tones
.into_iter()
.chain([palette.grey( 0.6), palette.fg])
.zip(weights)
.fold(Rgb(0.0, 0.0, 0.0), |sum, (tone, weight)| {
Rgb(
sum.0 + tone.0 * weight,
sum.1 + tone.1 * weight,
sum.2 + tone.2 * weight,
)
})
}
fn mood(&self, stage: &Stage) -> Mood {
if !stage.busy() {
return if stage.spell_t < 3.0
|| (stage.spell == Spell::Idle && STIR.at(stage.t).is_some())
{
Mood::Sleepy
} else {
Mood::Asleep
};
}
if let Some(sneeze) = SNEEZE.at(stage.t) {
return if sneeze < 0.45 {
Mood::Surprised
} else {
Mood::Sneeze
};
}
let now = stage.flow( 0.3);
let then = (stage.quanta_in( 0.6) - stage.quanta_in( 0.3)) / 0.3;
if stage.quiet > 4.0 {
Mood::Pondering
} else if stage.quiet > 1.2 {
Mood::Alarmed
} else if now - then > 35.0 {
Mood::Surprised
} else if WINK.at(stage.t).is_some() {
Mood::Wink
} else if self.happy {
Mood::Happy
} else {
Mood::Content
}
}
}
impl Shader for Critter {
fn tick(&mut self, stage: &Stage) {
let now = stage.flow( 0.3);
self.happy = now > 45.0 || self.happy && now >= 30.0;
let target = if stage.busy() {
self.mood(stage) as usize
} else {
9
};
let mut tint = self.tint.unwrap_or_else(|| {
let mut weights = [0.0; 11];
weights[10] = 1.0;
weights
});
let retained = (-TICK_SECS / 0.25).exp();
for (i, weight) in tint.iter_mut().enumerate() {
*weight = *weight * retained + f64::from(i == target) * (1.0 - retained);
}
self.tint = Some(tint);
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
reason = "Sneeze noise quantizes the nonnegative animation clock; caption indices fit i64 because they index an allocated slice."
)]
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas) {
let mood = self.mood(stage);
let (face, tone) = face_now(mood, stage.t);
canvas.mark = face
.chars()
.map(|glyph| Ink::of(glyph).fg(tone).bold(true))
.collect();
let tint = self.tint(palette);
let jostle = (stage.t / 0.1).floor() as i64;
canvas.letters = stage.letters(|i, _| {
let shoved = (mood == Mood::Sneeze && chance(7 * i as i64 + jostle, 0.3))
.then(|| stage.caption.get(i + 1))
.flatten()
.and_then(|next| {
let mut chars = next.chars();
chars.next().filter(|_| chars.next().is_none())
});
Ink {
glyph: shoved,
..Ink::default().fg(tint)
}
});
}
#[expect(
clippy::cast_possible_truncation,
reason = "The sleep glyph deliberately alternates on whole animation-clock buckets."
)]
fn bullet(&self, stage: &Stage, palette: Palette) -> Ink {
if stage.t % 60.0 < 2.0 {
Ink::of('♪').fg(SUN).bold(true)
} else if self.mood(stage) == Mood::Asleep {
Ink::of(if (stage.t / 0.7).floor() as i64 % 2 == 1 {
'Z'
} else {
'z'
})
.fg(VIOLET)
} else {
Ink::of('·').fg(palette.grey( 0.4))
}
}
}
fn face_now(mood: Mood, t: f64) -> (&'static str, Rgb) {
let (face, tone) = mood.face();
if mood == Mood::Happy && frac(t / 3.7) < 0.035 {
("–‿–", tone)
} else {
(face, tone)
}
}