use std::f64::consts::TAU;
use std::mem;
use super::OPUS;
use crate::Spell;
use crate::kit::{Rng, bump, frac, real};
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: "Yo-yo",
blurb: "work throws it; waiting, it sleeps",
credit: OPUS,
needs: Glyphs::BASE,
wide: true,
summon: |seed| Box::new(YoYo::new(seed)),
};
const CHERRY: Rgb = Rgb(236.0, 52.0, 74.0);
const COTTON: Rgb = Rgb(228.0, 222.0, 204.0);
const SPINNING: [char; 4] = ['◐', '◓', '◑', '◒'];
const DOWN: f64 = 0.42;
const BOTTOM: f64 = 0.1;
const ROCK: f64 = 2.4;
const UP: f64 = 0.36;
const LANDING: f64 = 30.0;
const TUG: f64 = 1.5;
const LULLABY: u64 = 900;
const DOZE: f64 = TAU / 4.0 / LULL;
#[derive(Clone, Copy)]
enum Throw {
Hand { hold: f64 },
Down,
Bottom,
Sleep,
Rock,
Up,
}
struct YoYo {
rng: Rng,
throw: Throw,
since: f64,
spin: f64,
angle: f64,
lullabies: u64,
rock: bool,
}
impl YoYo {
fn new(seed: u64) -> Self {
Self {
rng: Rng::new(seed),
throw: Throw::Hand { hold: 0.5 },
since: 0.0,
spin: 0.0,
angle: 0.0,
lullabies: 0,
rock: false,
}
}
fn place(&self, n: f64) -> f64 {
let reach = n + 2.0;
match self.throw {
Throw::Hand { .. } => -2.0,
Throw::Down => -2.0 + reach * (self.since / DOWN).powi(2),
Throw::Up => n - reach * (1.0 - (1.0 - self.since / UP).powi(2)),
Throw::Rock => n / 2.0 + 1.6 * (TAU * 1.1 * self.since).sin(),
Throw::Bottom | Throw::Sleep => n,
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "A turn's fraction, times four, floors to a quadrant in 0..4."
)]
fn frame(&self) -> char {
SPINNING[((frac(self.angle / TAU) * 4.0) as usize).min(SPINNING.len() - 1)]
}
fn strung(&self) -> bool {
!matches!(self.throw, Throw::Hand { .. })
}
}
impl Shader for YoYo {
fn tick(&mut self, stage: &Stage) {
let busy = stage.busy();
let idle = stage.spell == Spell::Idle;
self.since += TICK_SECS;
self.angle += TICK_SECS * self.spin;
let sleeping = matches!(self.throw, Throw::Sleep);
self.spin = if idle && sleeping {
DOZE
} else {
self.spin * (-TICK_SECS / if sleeping { 6.0 } else { 1.2 }).exp()
};
let lullabies = stage.quanta / LULLABY;
if lullabies > self.lullabies {
self.lullabies = lullabies;
self.rock = true;
}
let done = match self.throw {
Throw::Hand { hold } => idle || self.since > hold,
Throw::Down => self.since >= DOWN,
Throw::Bottom => self.since >= BOTTOM,
Throw::Sleep => busy || (!idle && self.spin < TUG),
Throw::Rock => idle || self.since >= ROCK,
Throw::Up => self.since >= UP,
};
if !done {
return;
}
self.throw = match self.throw {
Throw::Hand { .. } => Throw::Down,
Throw::Down => {
self.spin = if idle { DOZE } else { LANDING };
if busy {
if mem::take(&mut self.rock) {
Throw::Rock
} else {
Throw::Bottom
}
} else {
Throw::Sleep
}
}
Throw::Bottom if idle => Throw::Sleep,
Throw::Bottom | Throw::Sleep | Throw::Rock => Throw::Up,
Throw::Up => Throw::Hand {
hold: if busy {
0.1 + 0.7 * self.rng.unit()
/ (0.5 + (stage.flow( 0.6) / 70.0).min(1.5))
} else {
1.2
},
},
};
self.since = 0.0;
}
fn status(&self, stage: &Stage, palette: Palette, canvas: &mut Canvas) {
let n = real(stage.caption.len());
let x = self.place(n);
let at = x.round();
let cherry = palette.legible(CHERRY);
let cotton = palette.legible(COTTON);
let cradle = cherry.blend(cotton, 0.3);
let base = palette.grey(if stage.busy() { 0.68 } else { 0.5 });
let strung = self.strung();
let rocking = matches!(self.throw, Throw::Rock);
let moving = matches!(self.throw, Throw::Down | Throw::Up);
let middle = n / 2.0;
canvas.letters = stage.letters(|i, _| {
let i = real(i);
if (i - at).abs() < 0.5 {
return Ink::default().fg(cherry).bold(true).underline(cherry);
}
let smear = if moving { 0.35 * bump(i - x, 2.2) } else { 0.0 };
let ink = Ink::default().fg(cherry.blend(base, smear));
let string = strung && if rocking { i < middle + 2.2 } else { i < x };
match (string, rocking && (i - middle).abs() < 2.2) {
(false, _) => ink,
(true, true) => ink.underline(cradle),
(true, false) => ink.underline(cotton),
}
});
let down = strung && !rocking && x >= n - 0.5;
canvas.tail = vec![if down {
let glyph = if self.spin > 0.8 { self.frame() } else { '●' };
let fade = 0.5 + 0.5 * (self.spin / 6.0).min(1.0);
Ink::of(glyph)
.fg(cherry.blend(palette.grey(0.3), fade))
.bold(true)
} else {
Ink::of(' ')
}];
canvas.mark = vec![if strung {
Ink::of('◦').fg(cotton)
} else {
Ink::of('●').fg(cherry).bold(true)
}];
}
fn bullet(&self, _stage: &Stage, palette: Palette) -> Ink {
let spinning = self.strung() && self.spin > 0.8;
Ink::of(if spinning { self.frame() } else { '●' }).fg(palette.legible(CHERRY))
}
}