use super::{SWELL, WATER, WATER_MAX, WATER_MIN, image_sprite, z};
use crate::app::Sim;
use crate::app::art::Art;
use crate::app::layout::TILE;
use crate::sim::TideEvent;
use bevy::prelude::*;
fn heaving(board: &crate::sim::Board, remaining: u64) -> bool {
board.round_length().is_some()
&& !board.round_over()
&& remaining <= crate::app::hud::urgency_band(board.round_length(), crate::sim::SURGE_TICKS)
}
fn tide_depth(board: &crate::sim::Board, remaining: u64, seconds: f32) -> f32 {
let total = board.ticks() + remaining;
let elapsed = 1.0 - remaining as f32 / total.max(1) as f32;
let mut depth = WATER_MIN + elapsed * WATER_MAX;
if heaving(board, remaining) {
depth += (seconds * 6.0).sin() * SWELL;
}
depth
}
#[derive(Component)]
pub struct Waterline(pub u8);
pub fn spawn_waterline(commands: &mut Commands) {
for side in 0..4u8 {
commands.spawn((
Waterline(side),
Sprite::from_color(WATER, Vec2::ZERO),
Transform::from_translation(Vec3::new(0.0, 0.0, z::WATER)),
));
}
}
pub fn update_waterline(
sim: Res<Sim>,
time: Res<Time>,
mut bars: Query<(&Waterline, &mut Sprite, &mut Transform)>,
) {
let board = &sim.0;
let Some(remaining) = board.remaining_ticks() else {
for (_, mut sprite, _) in &mut bars {
if sprite.custom_size != Some(Vec2::ZERO) {
sprite.custom_size = Some(Vec2::ZERO);
}
}
return;
};
let depth = tide_depth(board, remaining, time.elapsed_secs());
let w = f32::from(board.width()) * TILE;
let h = f32::from(board.height()) * TILE;
for (bar, mut sprite, mut transform) in &mut bars {
let (size, pos) = match bar.0 {
0 => (
Vec2::new(w + depth * 2.0, depth),
Vec2::new(0.0, (h + depth) / 2.0),
),
1 => (
Vec2::new(w + depth * 2.0, depth),
Vec2::new(0.0, -(h + depth) / 2.0),
),
2 => (Vec2::new(depth, h), Vec2::new(-(w + depth) / 2.0, 0.0)),
_ => (Vec2::new(depth, h), Vec2::new((w + depth) / 2.0, 0.0)),
};
sprite.custom_size = Some(size);
transform.translation = pos.extend(z::WATER);
}
}
#[derive(Component)]
pub struct WaterFoam(pub u8);
pub fn spawn_water_foam(commands: &mut Commands, art: &Art) {
for side in 0..4u8 {
commands.spawn((
WaterFoam(side),
image_sprite(&art.foam, Color::WHITE, Vec2::ZERO),
Transform::from_translation(Vec3::new(0.0, 0.0, z::FOAM)),
));
}
}
pub fn update_water_foam(
sim: Res<Sim>,
time: Res<Time>,
mut bars: Query<(&Waterline, &mut Sprite), Without<WaterFoam>>,
mut foam: Query<(&WaterFoam, &mut Sprite, &mut Transform)>,
) {
let board = &sim.0;
let mania_tint = match board.last_event() {
Some((TideEvent::CrabMania, at)) if board.ticks().saturating_sub(at) < 300 => {
Some(Color::srgba(0.35, 0.72, 0.55, 0.65))
}
Some((TideEvent::GullMania, at)) if board.ticks().saturating_sub(at) < 300 => {
Some(Color::srgba(0.35, 0.38, 0.48, 0.7))
}
_ => None,
};
for (_, mut sprite) in &mut bars {
let target = mania_tint.unwrap_or(WATER);
if sprite.color != target {
sprite.color = target;
}
}
let Some(remaining) = board.remaining_ticks() else {
for (_, mut sprite, _) in &mut foam {
if sprite.custom_size != Some(Vec2::ZERO) {
sprite.custom_size = Some(Vec2::ZERO);
}
}
return;
};
let depth = tide_depth(board, remaining, time.elapsed_secs());
let lap = (time.elapsed_secs() * 1.8).sin() * 1.5;
let w = f32::from(board.width()) * TILE;
let h = f32::from(board.height()) * TILE;
const FOAM: f32 = 7.0;
for (bar, mut sprite, mut transform) in &mut foam {
let (size, pos, rot) = match bar.0 {
0 => (
Vec2::new(w + depth * 2.0, FOAM),
Vec2::new(0.0, h / 2.0 + depth - lap),
0.0,
),
1 => (
Vec2::new(w + depth * 2.0, FOAM),
Vec2::new(0.0, -h / 2.0 - depth + lap),
std::f32::consts::PI,
),
2 => (
Vec2::new(h, FOAM),
Vec2::new(-w / 2.0 - depth + lap, 0.0),
-std::f32::consts::FRAC_PI_2,
),
_ => (
Vec2::new(h, FOAM),
Vec2::new(w / 2.0 + depth - lap, 0.0),
std::f32::consts::FRAC_PI_2,
),
};
sprite.custom_size = Some(size);
transform.translation = pos.extend(z::FOAM);
transform.rotation = Quat::from_rotation_z(rot);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::Board;
fn timed(round: u32, elapsed: u32) -> Board {
let mut board = Board::new(9, 7, 1);
board.set_round_length(Some(round));
for _ in 0..elapsed {
board.tick_idle();
}
board
}
#[test]
fn a_board_with_no_clock_never_heaves() {
let board = Board::new(9, 7, 1);
assert_eq!(board.remaining_ticks(), None);
assert!(!heaving(&board, 0), "no clock, no swell");
}
#[test]
fn the_swell_is_only_for_the_closing_stretch() {
let board = timed(3600, 30);
let remaining = board.remaining_ticks().expect("a clock");
assert!(!heaving(&board, remaining), "early on it is calm");
assert!(heaving(&board, 1), "and heaving at the end");
}
#[test]
fn the_tide_widens_over_the_round_and_stays_off_the_sand() {
let board = timed(3600, 1);
let full = board.remaining_ticks().expect("a clock");
let early = tide_depth(&board, full, 0.0);
let late = tide_depth(&board, full / 8, 0.0);
assert!(early > 0.0, "there is water from the start: {early}");
assert!(late > early, "and more of it later: {early} then {late}");
assert!(
late <= WATER_MAX + WATER_MIN,
"the tide never runs past its own ceiling: {late}"
);
}
}