use super::{
Tween, coop_claws, crab_body, crab_claw, resting_shell, shade_of, shell_color, smoothing_alpha,
sprite_side,
};
use crate::app::art::Art;
use crate::app::layout;
use crate::app::palette;
use crate::app::{Sim, effects};
use crate::sim::CrabKind;
use bevy::color::Mix;
use bevy::prelude::*;
#[derive(Component)]
pub struct SweepSprite {
id: u32,
keep: Vec2,
veer: Option<(Vec2, f32)>,
}
impl SweepSprite {
pub(crate) fn new(id: u32, keep: Vec2) -> SweepSprite {
SweepSprite {
id,
keep,
veer: None,
}
}
fn heading(&mut self, castle: Vec2, now: f32, launch: f32, lands: f32) -> Vec2 {
if castle != self.keep {
self.veer = (now > launch).then(|| (self.aim(now, lands), now));
self.keep = castle;
}
self.aim(now, lands)
}
fn aim(&self, now: f32, lands: f32) -> Vec2 {
match self.veer {
None => self.keep,
Some((was, at)) => {
let turned = ((now - at) / (lands - at).max(1.0)).clamp(0.0, 1.0);
was.lerp(self.keep, turned)
}
}
}
}
pub fn draw_sweeps(
mut commands: Commands,
sim: Res<Sim>,
art: Res<Art>,
settings: Res<crate::app::settings::GameSettings>,
clock: Res<crate::app::round_state::RoundClock>,
mut tween: Tween,
mut sprites: Query<(Entity, &mut SweepSprite, &mut Transform, &mut Sprite)>,
) {
let board = &sim.0;
let flights = board.sweeping();
if flights.is_empty() && sprites.is_empty() {
return;
}
let time = &tween.time;
let alpha = smoothing_alpha(
board.ticks(),
&mut tween.watch,
time.delta_secs(),
&tween.fixed_time,
);
let now = board.ticks() as f32 - 1.0 + alpha;
let glow = if settings.reduced_motion {
0.6
} else {
0.5 + 0.3 * (clock.0 * 9.0).sin()
};
let mut drawn: Vec<u32> = Vec::with_capacity(flights.len());
for (entity, mut sweep, mut transform, mut sprite) in &mut sprites {
let Some(flight) = flights.iter().find(|f| f.crab.id == sweep.id) else {
commands.entity(entity).despawn();
continue;
};
drawn.push(sweep.id);
let crab = &flight.crab;
let from = layout::creature_pos(board, crab.tile, crab.dir, crab.progress);
let launch = flight.launch as f32;
let lands = flight.lands.max(flight.launch + 1) as f32;
let keep = sweep.heading(keep_of(board, flight, from), now, launch, lands);
let shell = swept_shell(crab.kind, crab.id);
if now < launch {
transform.translation = from.extend(effects::SWEEP_WAIT_Z);
transform.scale = Vec3::ONE;
layout::tint(&mut sprite, shell.mix(&palette::GOLD, glow));
continue;
}
let progress = ((now - launch) / (lands - launch)).clamp(0.0, 1.0);
let (at, scale) = effects::sweep_arc(from, keep, progress);
transform.translation = at;
transform.scale = Vec3::splat(scale);
layout::tint(&mut sprite, shell);
}
for flight in flights {
let crab = &flight.crab;
if drawn.contains(&crab.id) {
continue;
}
let pos = layout::creature_pos(board, crab.tile, crab.dir, crab.progress);
let side = Vec2::splat(sprite_side(crab.kind));
let glowing = swept_shell(crab.kind, crab.id).mix(&palette::GOLD, glow);
commands
.spawn((
SweepSprite::new(crab.id, keep_of(board, flight, pos)),
crab_body(&art, crab.kind, crab.handed, glowing),
Transform::from_translation(pos.extend(effects::SWEEP_WAIT_Z))
.with_rotation(layout::dir_rotation(crab.dir)),
))
.with_child(crab_claw(&art, crab.handed, coop_claws(board), side));
}
}
fn swept_shell(kind: CrabKind, id: u32) -> Color {
resting_shell(kind, shell_color(kind, shade_of(id)), false)
}
fn keep_of(board: &crate::sim::Board, flight: &crate::sim::Sweeping, from: Vec2) -> Vec2 {
board
.castle_of(flight.owner)
.map_or(from, |(x, y)| layout::tile_center(board, x, y))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_swept_molt_is_drawn_resting() {
let id = 7;
let plain = shell_color(CrabKind::Molting, shade_of(id));
assert_ne!(swept_shell(CrabKind::Molting, id), plain);
assert_eq!(
swept_shell(CrabKind::Molting, id),
resting_shell(CrabKind::Molting, plain, false)
);
let common = shell_color(CrabKind::Common, shade_of(id));
assert_eq!(swept_shell(CrabKind::Common, id), common);
}
#[test]
fn a_crab_in_the_air_turns_toward_a_swapped_keep() {
let (old, new) = (Vec2::new(100.0, 0.0), Vec2::new(-100.0, 50.0));
let (launch, lands) = (10.0, 40.0);
let mut sweep = SweepSprite::new(1, old);
assert_eq!(sweep.heading(old, 20.0, launch, lands), old);
assert_eq!(
sweep.heading(new, 25.0, launch, lands),
old,
"no jump on the frame of the swap"
);
let halfway = sweep.heading(new, 32.5, launch, lands);
assert!(
halfway.distance(old.lerp(new, 0.5)) < 1e-3,
"turning: {halfway:?}"
);
assert_eq!(sweep.heading(new, lands, launch, lands), new, "lands home");
let mut waiting = SweepSprite::new(2, old);
assert_eq!(
waiting.heading(new, 5.0, launch, lands),
new,
"nobody saw where it was going"
);
}
}