use super::{BoardStatic, z};
use crate::app::Sim;
use crate::app::art::Art;
use crate::app::layout::TILE;
use crate::app::sim_events::SimEvent;
use bevy::prelude::*;
#[derive(Component)]
pub struct TideWash {
age: f32,
}
#[derive(Component)]
pub struct WashFoam;
const SWEEP: f32 = 0.85;
const HOLD: f32 = 0.3;
const DRAIN: f32 = 0.7;
pub(super) const REACH: f32 = 9000.0;
pub fn start_tide_wash(
mut commands: Commands,
mut events: MessageReader<SimEvent>,
art: Res<Art>,
settings: Res<crate::app::settings::GameSettings>,
mut trauma: ResMut<crate::app::effects::Trauma>,
running: Query<Entity, With<TideWash>>,
) {
let arriving = events.read().any(|e| matches!(e, SimEvent::RoundEnded));
if !arriving || settings.reduced_motion || !running.is_empty() {
return;
}
commands.spawn((
BoardStatic,
TideWash { age: 0.0 },
Sprite {
image: art.ramp.clone(),
color: Color::srgba(0.21, 0.46, 0.67, 0.88),
custom_size: Some(Vec2::ZERO),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, z::PIP + 0.1))
.with_rotation(Quat::from_rotation_z(std::f32::consts::PI)),
));
commands.spawn((
BoardStatic,
TideWash { age: 0.0 },
WashFoam,
Sprite {
image: art.foam.clone(),
color: Color::srgba(1.0, 1.0, 1.0, 0.92),
custom_size: Some(Vec2::ZERO),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, z::PIP + 0.12)),
));
trauma.add(0.45);
}
fn wave(age: f32, half_height: f32) -> (f32, f32, f32) {
let reach = half_height.max(REACH / 4.0);
let body = half_height * 2.0 + REACH / 2.0;
let sky = reach + TILE;
let deep = -reach - 3.0 * TILE;
let sweep = (age / SWEEP).clamp(0.0, 1.0);
let eased = 1.0 - (1.0 - sweep) * (1.0 - sweep);
let front = sky + (deep - sky) * eased;
let drained = ((age - SWEEP - HOLD) / DRAIN).clamp(0.0, 1.0);
(front, body, 1.0 - drained)
}
pub fn advance_tide_wash(
time: Res<Time>,
sim: Res<Sim>,
mut commands: Commands,
mut pieces: Query<(
Entity,
&mut TideWash,
&mut Sprite,
&mut Transform,
Option<&WashFoam>,
)>,
) {
let board = &sim.0;
let dt = time.delta_secs();
let half_h = f32::from(board.height()) * TILE / 2.0;
let span = REACH;
for (entity, mut wash, mut sprite, mut transform, foam) in &mut pieces {
wash.age += dt;
if wash.age >= SWEEP + HOLD + DRAIN {
commands.entity(entity).despawn();
continue;
}
let (front, body, strength) = wave(wash.age, half_h);
let front = front + (wash.age * 9.0).sin() * 3.0;
let peak = base_alpha(foam);
sprite.color.set_alpha(strength * peak);
transform.translation.x = 0.0;
if foam.is_some() {
sprite.custom_size = Some(Vec2::new(span, 24.0));
transform.translation.y = front;
continue;
}
sprite.custom_size = Some(Vec2::new(span, body));
transform.translation.y = front + body / 2.0;
}
}
fn base_alpha(foam: Option<&WashFoam>) -> f32 {
if foam.is_some() { 0.92 } else { 0.88 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_wave_comes_in_off_the_sea_and_covers_the_beach() {
let half_height = 5.0 * TILE;
let (front, body, strength) = wave(0.0, half_height);
assert!(front > half_height, "starts clear of the top: {front}");
assert!(
front + body > half_height.max(REACH / 4.0),
"with its shoulder clear of the top of the view"
);
assert_eq!(strength, 1.0, "at full flood the moment it is spawned");
let (front, body, _) = wave(SWEEP, half_height);
let window_bottom = -half_height.max(REACH / 4.0);
assert!(
front < window_bottom,
"the front passes the bottom of the view, not just of the board: {front}"
);
let window_top = half_height.max(REACH / 4.0);
assert!(
front + body > window_top,
"and the water still covers the top of the view: {front} + {body}"
);
}
#[test]
fn the_wave_holds_before_it_drains() {
let half_height = 5.0 * TILE;
assert!(
wave(SWEEP + HOLD, half_height).2 > 0.999,
"still at flood when the hold ends"
);
let (_, _, strength) = wave(SWEEP + HOLD + DRAIN / 2.0, half_height);
assert!(
(0.1..0.9).contains(&strength),
"half drained, not all or nothing: {strength}"
);
assert_eq!(wave(SWEEP + HOLD + DRAIN, half_height).2, 0.0, "and gone");
}
}