use super::{MenuArt, VisualRng};
use crate::app::art;
use crate::app::palette;
use crate::sim::CrabKind;
use bevy::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CritterKind {
Crab,
Gull,
Cloud,
Boat,
}
#[derive(Component)]
pub struct MenuCritter {
speed: f32,
frame_clock: f32,
kind: CritterKind,
base_y: f32,
}
#[derive(Component)]
pub struct WaveSegment {
base: Vec2,
amp: f32,
freq: f32,
speed: f32,
phase: f32,
brightness: f32,
}
#[derive(Component)]
pub struct ShoreFoam {
base_y: f32,
}
const WAVE_LANES: usize = 7;
const WAVE_SEGMENTS: usize = 110;
const SHORE_H: f32 = 208.0;
const HORIZON: f32 = 0.62;
#[derive(Component)]
pub struct MenuShore;
fn spawn_shore(commands: &mut Commands, art: &art::Art, rng: &mut VisualRng, window: &Window) {
let (w, h) = (window.width(), window.height());
let bottom = -h / 2.0;
let horizon = bottom + h * HORIZON;
let band = |y: f32, height: f32, color: Color, z: f32| {
(
MenuArt,
MenuShore,
Sprite::from_color(color, Vec2::new(w + 40.0, height)),
Transform::from_translation(Vec3::new(0.0, y, z)),
)
};
let sky_h = h / 2.0 - horizon;
commands.spawn(band(
horizon + sky_h * 0.667,
sky_h * 0.667 * 2.0,
Color::srgb(0.42, 0.65, 0.85),
0.0,
));
commands.spawn(band(
horizon + sky_h * 0.167,
sky_h * 0.334,
Color::srgb(0.62, 0.79, 0.90),
0.05,
));
let sea_top = SHORE_H + 18.0;
let sea_h = (horizon - bottom) - sea_top;
commands.spawn(band(
horizon - sea_h * 0.25,
sea_h * 0.5,
Color::srgb(0.16, 0.36, 0.52),
0.0,
));
commands.spawn(band(
horizon - sea_h * 0.75,
sea_h * 0.5,
Color::srgb(0.24, 0.47, 0.60),
0.0,
));
commands.spawn(band(horizon, 3.0, Color::srgb(0.72, 0.84, 0.92), 0.1));
commands.spawn(band(
bottom + SHORE_H / 2.0,
SHORE_H,
Color::srgb(0.86, 0.77, 0.57),
0.0,
));
commands.spawn(band(
bottom + SHORE_H + 9.0,
18.0,
Color::srgb(0.72, 0.64, 0.47),
0.0,
));
let foam_y = bottom + SHORE_H + 20.0;
commands.spawn((
MenuArt,
MenuShore,
ShoreFoam { base_y: foam_y },
Sprite {
image: art.foam.clone(),
color: Color::srgba(1.0, 1.0, 1.0, 0.65),
custom_size: Some(Vec2::new(w + 40.0, 12.0)),
..default()
},
Transform::from_translation(Vec3::new(0.0, foam_y, 0.1)),
));
spawn_swell(commands, rng, w, sea_top, sea_h, bottom);
seed_travellers(commands, art, rng, w, h);
spawn_beach_props(commands, art, rng, w, bottom);
}
struct Prop {
image: Handle<Image>,
tint: Color,
size: Vec2,
shadow: f32,
rotation: f32,
}
fn spawn_swell(
commands: &mut Commands,
rng: &mut VisualRng,
w: f32,
sea_top: f32,
sea_h: f32,
bottom: f32,
) {
let seg_w = (w + 40.0) / WAVE_SEGMENTS as f32;
for lane in 0..WAVE_LANES {
let near = (lane as f32 + 0.5) / WAVE_LANES as f32;
let y = bottom + sea_top + sea_h * (1.0 - near) * 0.98;
let amp = 1.5 + near * near * 9.0;
let freq = 0.020 + near * 0.055;
let speed = 0.6 + near * 2.2;
let phase = rng.range(0.0, std::f32::consts::TAU);
let brightness = 0.10 + near * 0.42;
for segment in 0..WAVE_SEGMENTS {
let x = -w / 2.0 - 20.0 + seg_w * (segment as f32 + 0.5);
let base = Vec2::new(x, y);
commands.spawn((
MenuArt,
MenuShore,
WaveSegment {
base,
amp,
freq,
speed,
phase,
brightness,
},
Sprite::from_color(
Color::srgba(1.0, 1.0, 1.0, 0.0),
Vec2::new(seg_w + 1.5, 2.0 + near * 3.0),
),
Transform::from_translation(base.extend(0.2)),
));
}
}
}
fn seed_travellers(commands: &mut Commands, art: &art::Art, rng: &mut VisualRng, w: f32, h: f32) {
for _ in 0..3 {
let x = rng.range(-w * 0.45, w * 0.45);
spawn_critter(commands, art, rng, w, h, CritterKind::Cloud, Some(x));
}
let boat_x = rng.range(-w * 0.35, w * 0.35);
spawn_critter(commands, art, rng, w, h, CritterKind::Boat, Some(boat_x));
let crab_x = rng.range(-w * 0.4, w * 0.4);
spawn_critter(commands, art, rng, w, h, CritterKind::Crab, Some(crab_x));
}
fn roll_prop(art: &art::Art, rng: &mut VisualRng, scale: f32) -> Prop {
match rng.next() % 12 {
0 | 1 => Prop {
image: art.castle.clone(),
tint: palette::player_color((rng.next() % 4) as u8),
size: Vec2::splat(52.0 * scale),
shadow: 44.0 * scale,
rotation: 0.0,
},
2 => Prop {
image: art.log.clone(),
tint: Color::srgb(1.15, 1.12, 1.05),
size: Vec2::splat(50.0 * scale),
shadow: 40.0 * scale,
rotation: if rng.next().is_multiple_of(2) {
0.0
} else {
std::f32::consts::FRAC_PI_2
},
},
3 | 4 => Prop {
image: art.kelp.clone(),
tint: Color::srgba(1.0, 1.0, 1.0, 0.92),
size: Vec2::splat(rng.range(26.0, 38.0) * scale),
shadow: 0.0,
rotation: rng.range(-0.25, 0.25),
},
5 => Prop {
image: art.pool.clone(),
tint: Color::srgba(1.0, 1.0, 1.0, 0.9),
size: Vec2::splat(rng.range(46.0, 62.0) * scale),
shadow: 0.0,
rotation: 0.0,
},
6 => Prop {
image: art.hole.clone(),
tint: Color::WHITE,
size: Vec2::splat(rng.range(26.0, 34.0) * scale),
shadow: 0.0,
rotation: 0.0,
},
7 => Prop {
image: art.arrow.clone(),
tint: palette::player_color((rng.next() % 4) as u8).lighter(0.12),
size: Vec2::splat(rng.range(30.0, 38.0) * scale),
shadow: 24.0 * scale,
rotation: (rng.next() % 4) as f32 * std::f32::consts::FRAC_PI_2,
},
8..=10 => Prop {
image: art.rock.clone(),
tint: Color::WHITE,
size: Vec2::splat(rng.range(22.0, 38.0) * scale),
shadow: 22.0 * scale,
rotation: rng.range(0.0, std::f32::consts::TAU),
},
_ => Prop {
image: art.star.clone(),
tint: Color::srgba(0.95, 0.78, 0.6, 0.85),
size: Vec2::splat(rng.range(9.0, 15.0) * scale),
shadow: 0.0,
rotation: rng.range(0.0, std::f32::consts::TAU),
},
}
}
fn spawn_beach_props(
commands: &mut Commands,
art: &art::Art,
rng: &mut VisualRng,
w: f32,
bottom: f32,
) {
let slots = ((w / 58.0) as usize).clamp(10, 26);
for slot in 0..slots {
let lane = (slot as f32 + 0.5) / slots as f32;
let x = (lane - 0.5) * w + rng.range(-38.0, 38.0);
let depth = rng.range(0.0, 1.0);
let y = bottom + 88.0 + depth * (SHORE_H - 104.0);
let scale = 1.0 - depth * 0.35;
let prop = roll_prop(art, rng, scale);
let z = 0.6 - depth * 0.25;
if prop.shadow > 0.0 {
commands.spawn((
MenuArt,
MenuShore,
Sprite {
image: art.shadow.clone(),
color: Color::srgba(0.36, 0.28, 0.18, 0.28),
custom_size: Some(Vec2::new(prop.shadow, prop.shadow * 0.42)),
..default()
},
Transform::from_translation(Vec3::new(x, y - prop.size.y * 0.36, z - 0.01)),
));
}
commands.spawn((
MenuArt,
MenuShore,
Sprite {
image: prop.image,
color: prop.tint,
custom_size: Some(prop.size),
..default()
},
Transform::from_translation(Vec3::new(x, y, z))
.with_rotation(Quat::from_rotation_z(prop.rotation)),
));
}
for _ in 0..90 {
let pale = rng.next().is_multiple_of(3);
let tint = if pale {
Color::srgba(1.0, 0.97, 0.88, 0.30)
} else {
Color::srgba(0.55, 0.46, 0.31, 0.22)
};
let size = rng.range(2.5, 7.0);
commands.spawn((
MenuArt,
MenuShore,
Sprite::from_color(tint, Vec2::splat(size)),
Transform::from_translation(Vec3::new(
rng.range(-w / 2.0, w / 2.0),
rng.range(bottom + 4.0, bottom + SHORE_H - 6.0),
0.12,
)),
));
}
for _ in 0..5 {
commands.spawn((
MenuArt,
MenuShore,
Sprite {
image: art.foam.clone(),
color: Color::srgba(0.55, 0.47, 0.34, 0.22),
custom_size: Some(Vec2::new(rng.range(120.0, 240.0), rng.range(10.0, 18.0))),
..default()
},
Transform::from_translation(Vec3::new(
rng.range(-w / 2.0, w / 2.0),
bottom + SHORE_H - rng.range(6.0, 34.0),
0.15,
)),
));
}
}
pub fn refit_shore(
mut commands: Commands,
art: Res<art::Art>,
mut rng: ResMut<VisualRng>,
windows: Query<&Window>,
mut last: Local<Vec2>,
shore: Query<Entity, With<MenuShore>>,
critters: Query<Entity, With<MenuCritter>>,
) {
let Ok(window) = windows.single() else {
return;
};
let size = Vec2::new(window.width(), window.height());
if *last == size && !shore.is_empty() {
return;
}
*last = size;
for entity in shore.iter().chain(critters.iter()) {
commands.entity(entity).despawn();
}
spawn_shore(&mut commands, &art, &mut rng, window);
}
fn spawn_critter(
commands: &mut Commands,
art: &art::Art,
rng: &mut VisualRng,
w: f32,
h: f32,
kind: CritterKind,
at_x: Option<f32>,
) {
let bottom = -h / 2.0;
let horizon = bottom + h * HORIZON;
let rightward = rng.next().is_multiple_of(2);
let (y, speed, image, size, tint) = match kind {
CritterKind::Gull => (
rng.range(horizon + 30.0, h / 2.0 - 40.0),
rng.range(130.0, 190.0),
art.gull_fly.clone(),
Vec2::splat(46.0),
Color::WHITE,
),
CritterKind::Cloud => (
rng.range(horizon + h * 0.08, h / 2.0 - 30.0),
rng.range(9.0, 20.0),
art.cloud.clone(),
Vec2::new(rng.range(110.0, 190.0), rng.range(55.0, 95.0)),
Color::srgba(1.0, 1.0, 1.0, rng.range(0.75, 0.95)),
),
CritterKind::Boat => (
rng.range(horizon - 60.0, horizon - 14.0),
rng.range(18.0, 30.0),
art.boat.clone(),
Vec2::splat(rng.range(38.0, 54.0)),
Color::WHITE,
),
CritterKind::Crab => {
const KINDS: [CrabKind; 6] = [
CrabKind::Common,
CrabKind::Common,
CrabKind::Juvenile,
CrabKind::Giant,
CrabKind::Molting,
CrabKind::Golden,
];
let crab = KINDS[(rng.next() % 6) as usize];
(
rng.range(bottom + 18.0, bottom + SHORE_H - 40.0),
rng.range(28.0, 55.0),
art.crab.clone(),
Vec2::splat(30.0),
crate::app::creatures::body_color(crab),
)
}
};
let speed = if rightward { speed } else { -speed };
let rotation = match kind {
CritterKind::Crab | CritterKind::Gull => crate::app::layout::dir_rotation(if rightward {
crate::sim::Direction::Right
} else {
crate::sim::Direction::Left
}),
CritterKind::Cloud | CritterKind::Boat => Quat::IDENTITY,
};
let z = match kind {
CritterKind::Cloud => 1.5,
CritterKind::Gull => 2.0,
CritterKind::Boat => 0.3,
CritterKind::Crab => 0.5,
};
let x = at_x.unwrap_or(-speed.signum() * (w / 2.0 + 110.0));
commands.spawn((
MenuArt,
MenuCritter {
speed,
frame_clock: rng.range(0.0, 3.0),
kind,
base_y: y,
},
Sprite {
image,
color: tint,
custom_size: Some(size),
flip_x: kind == CritterKind::Boat && !rightward,
..default()
},
Transform::from_translation(Vec3::new(x, y, z)).with_rotation(rotation),
));
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn menu_ambience(
mut commands: Commands,
time: Res<Time>,
art: Res<art::Art>,
mut rng: ResMut<VisualRng>,
mut countdown: Local<f32>,
windows: Query<&Window>,
mut critters: Query<(Entity, &mut MenuCritter, &mut Transform, &mut Sprite)>,
mut waves: Query<(&WaveSegment, &mut Transform, &mut Sprite), Without<MenuCritter>>,
mut foam: Query<
(&ShoreFoam, &mut Transform, &mut Sprite),
(Without<MenuCritter>, Without<WaveSegment>),
>,
) {
let Ok(window) = windows.single() else {
return;
};
let (w, h) = (window.width(), window.height());
let dt = time.delta_secs();
*countdown -= dt;
if *countdown <= 0.0 {
*countdown = rng.range(2.0, 5.0);
let kind = match rng.next() % 10 {
0..=2 => CritterKind::Gull,
3..=4 => CritterKind::Cloud,
5 => CritterKind::Boat,
_ => CritterKind::Crab,
};
spawn_critter(&mut commands, &art, &mut rng, w, h, kind, None);
}
for (entity, mut critter, mut transform, mut sprite) in &mut critters {
transform.translation.x += critter.speed * dt;
if transform.translation.x.abs() > w / 2.0 + 130.0 {
commands.entity(entity).despawn();
continue;
}
critter.frame_clock += dt;
match critter.kind {
CritterKind::Crab => {
let frame_b = (critter.frame_clock / 0.16) as u32 % 2 == 1;
let want = if frame_b { &art.crab_b } else { &art.crab };
if sprite.image != *want {
sprite.image = want.clone();
}
}
CritterKind::Boat => {
transform.translation.y = critter.base_y + (critter.frame_clock * 1.6).sin() * 2.5;
}
CritterKind::Gull | CritterKind::Cloud => {}
}
}
let now = time.elapsed_secs();
for (wave, mut transform, mut sprite) in &mut waves {
let angle = wave.base.x * wave.freq - now * wave.speed + wave.phase;
let long = (angle * 0.37 - now * wave.speed * 0.55).sin();
let swell = (angle.sin() + long * 0.55) / 1.55;
transform.translation.y = wave.base.y + swell * wave.amp;
let crest = ((swell + 1.0) * 0.5).powf(2.2);
sprite.color.set_alpha(wave.brightness * crest);
}
for (shore, mut transform, mut sprite) in &mut foam {
let breath = (now * 0.42).sin();
transform.translation.y = shore.base_y + breath * 5.0;
sprite.color.set_alpha(0.5 + 0.22 * (breath * 0.5 + 0.5));
}
}