use crate::app::art::Art;
use bevy::prelude::*;
#[derive(Component)]
pub struct Critter {
gull: bool,
rate: f32,
travel: f32,
phase: f32,
frame: f32,
}
pub const CRITTER: f32 = 84.0;
pub const CRITTER_GAP: f32 = 40.0;
#[cfg(test)]
pub const SHOULDERS: f32 = 2.0 * (CRITTER + CRITTER_GAP);
const FLOCK_ALPHA: f32 = 0.30;
pub type Perch = (f32, f32, f32, bool);
pub fn shoulder(parent: &mut ChildSpawnerCommands, art: &Art, gull: bool, phase: f32) {
spawn_critter(parent, art, gull, CRITTER, phase, None);
}
pub fn flock(parent: &mut ChildSpawnerCommands, art: &Art, perches: &[Perch]) {
for (index, (x, y, size, gull)) in perches.iter().enumerate() {
spawn_critter(
parent,
art,
*gull,
*size,
index as f32 * 0.9,
Some((*x, *y)),
);
}
}
fn spawn_critter(
parent: &mut ChildSpawnerCommands,
art: &Art,
gull: bool,
size: f32,
phase: f32,
at: Option<(f32, f32)>,
) {
let (image, tint) = if gull {
(art.gull.clone(), Color::WHITE)
} else {
(
art.crab.clone(),
crate::app::creatures::body_color(crate::sim::CrabKind::Common),
)
};
let node = match at {
Some((x, y)) => Node {
position_type: PositionType::Absolute,
left: Val::Percent(x * 100.0),
top: Val::Percent(y * 100.0),
width: Val::Px(size),
height: Val::Px(size),
..default()
},
None => Node {
width: Val::Px(size),
height: Val::Px(size),
flex_shrink: 0.0,
..default()
},
};
parent.spawn((
Critter {
gull,
rate: if gull { 1.5 } else { 3.4 },
travel: if gull { 7.0 } else { 3.0 },
phase,
frame: if gull { 0.30 } else { 0.16 },
},
ImageNode {
image,
color: tint.with_alpha(if at.is_some() { FLOCK_ALPHA } else { 1.0 }),
flip_x: at.map_or(gull, |(x, _)| x > 0.5),
..default()
},
node,
));
}
pub fn animate_company(
time: Res<Time>,
art: Res<Art>,
mut critters: Query<(&Critter, &mut Node, &mut ImageNode)>,
) {
let now = time.elapsed_secs();
for (critter, mut node, mut image) in &mut critters {
let bob = Val::Px((now * critter.rate + critter.phase).sin() * critter.travel);
if node.margin.top != bob {
node.margin.top = bob;
}
let beat = ((now + critter.phase) / critter.frame) as u32 % 2 == 1;
let want = match (critter.gull, beat) {
(true, true) => &art.gull_fly,
(true, false) => &art.gull,
(false, true) => &art.crab_b,
(false, false) => &art.crab,
};
if image.image != *want {
image.image = want.clone();
}
}
}
#[cfg(test)]
pub fn keep_clear(card_w: f32) -> std::ops::Range<f32> {
let taken = ((card_w + SHOULDERS) / crate::app::settings::DESIGN_W).min(1.0);
(0.5 - taken / 2.0)..(0.5 + taken / 2.0)
}
#[cfg(test)]
pub fn flock_is_hung_clear(perches: &[Perch], clear_x: std::ops::Range<f32>, clear_y: (f32, f32)) {
let frame_h = crate::app::settings::DESIGN_H - 2.0 * crate::app::menu_ui::BAR_H;
for &(x, y, size, gull) in perches {
assert!(size > 0.0, "a critter with no size is not drawn");
let (wide, tall) = (size / crate::app::settings::DESIGN_W, size / frame_h);
assert!(
x >= 0.0 && x + wide <= 1.0,
"the critter at {x} runs off the frame"
);
assert!(
y >= 0.0 && y + tall <= 1.0,
"the critter at {y} runs off the frame"
);
assert_eq!(gull, y < 0.5, "a gull on the sand, or a crab in the air");
let over_x = x + wide > clear_x.start && x < clear_x.end;
let over_y = y + tall > clear_y.0 && y < clear_y.1;
assert!(
!(over_x && over_y),
"the {size}px critter at {x},{y} covers the card, which spans \
{clear_x:?} across and {clear_y:?} down"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wider_card_leaves_a_narrower_margin() {
let narrow = keep_clear(279.0);
let wide = keep_clear(630.0);
assert!(
wide.start < narrow.start && narrow.end < wide.end,
"the wide card ({wide:?}) has to swallow the narrow one ({narrow:?})"
);
for band in [&narrow, &wide] {
assert!(
(band.start - (1.0 - band.end)).abs() < 0.001,
"{band:?} is not centred"
);
}
assert_eq!(keep_clear(crate::app::settings::DESIGN_W), 0.0..1.0);
}
#[test]
fn a_critter_is_judged_by_its_footprint_not_its_corner() {
let band = keep_clear(279.0);
let reaching = [(band.start - 0.01, 0.30, 60.0, true)];
assert!(
std::panic::catch_unwind(|| flock_is_hung_clear(&reaching, band.clone(), (0.15, 0.80)))
.is_err(),
"a critter reaching onto the card was allowed"
);
let clear = [(band.start - 0.10, 0.30, 60.0, true)];
flock_is_hung_clear(&clear, band, (0.15, 0.80));
}
}