use crate::app::art::Art;
use crate::app::layout::{self, TILE};
use crate::app::palette;
use crate::app::{Sim, effects};
use crate::sim::{Crab, CrabKind, Gull, GullState, Handedness};
use bevy::color::Mix;
use bevy::platform::collections::HashMap;
use bevy::prelude::*;
#[derive(Component)]
pub struct CrabSprite {
pub id: u32,
pub kind: CrabKind,
}
#[derive(Component)]
pub struct GullSprite(pub u32);
#[derive(Component)]
pub struct CreatureShadow;
const CLAW_LEFT: Color = Color::srgb(0.18, 0.49, 0.82); const CLAW_RIGHT: Color = Color::srgb(0.97, 0.82, 0.17);
pub(crate) fn body_color(kind: CrabKind) -> Color {
match kind {
CrabKind::Common => Color::srgb(0.91, 0.50, 0.29),
CrabKind::Juvenile => Color::srgb(0.96, 0.76, 0.42),
CrabKind::Giant => Color::srgb(0.70, 0.25, 0.17),
CrabKind::Molting => Color::srgb(0.61, 0.48, 0.82),
CrabKind::Golden => Color::srgb(1.0, 0.88, 0.20),
CrabKind::Sparkling => Color::srgb(0.75, 0.95, 1.0),
}
}
fn body_size(kind: CrabKind) -> Vec2 {
match kind {
CrabKind::Giant => Vec2::new(TILE * 0.80, TILE * 0.62),
CrabKind::Juvenile => Vec2::new(TILE * 0.42, TILE * 0.34),
CrabKind::Common | CrabKind::Molting | CrabKind::Golden | CrabKind::Sparkling => {
Vec2::new(TILE * 0.56, TILE * 0.44)
}
}
}
pub fn sync_crab_sprites(
mut commands: Commands,
sim: Res<Sim>,
art: Res<Art>,
mut live: Local<HashMap<u32, Crab>>,
existing: Query<(Entity, &CrabSprite)>,
) {
let board = &sim.0;
live.clear();
live.extend(board.crabs().iter().map(|c| (c.id, *c)));
for (entity, sprite) in &existing {
if live.remove(&sprite.id).is_none() {
commands.entity(entity).despawn();
}
}
for (&id, crab) in live.iter() {
let pos = layout::creature_pos(board, crab.tile, crab.dir, crab.progress);
let size = body_size(crab.kind);
let claw = match crab.handed {
Handedness::Left => CLAW_LEFT,
Handedness::Right => CLAW_RIGHT,
};
let claw_y = match crab.handed {
Handedness::Left => size.y * 0.45,
Handedness::Right => -size.y * 0.45,
};
commands
.spawn((
CrabSprite {
id,
kind: crab.kind,
},
Sprite {
image: art.crab.clone(),
color: body_color(crab.kind),
custom_size: Some(size * 1.25),
..default()
},
Transform::from_translation(pos.extend(layout::z::CREATURE))
.with_rotation(layout::dir_rotation(crab.dir)),
))
.with_children(|parent| {
parent.spawn((
CreatureShadow,
Sprite {
image: art.shadow.clone(),
custom_size: Some(Vec2::new(size.x * 1.35, size.y * 1.15)),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, -0.5)),
));
parent.spawn((
Sprite {
image: art.claw.clone(),
color: claw,
custom_size: Some(Vec2::splat(size.y * 0.62)),
..default()
},
Transform::from_translation(Vec3::new(size.x * 0.5, claw_y, 0.1)),
));
});
}
}
fn smoothing_alpha(
ticks: u64,
watch: &mut (u64, f32),
delta_secs: f32,
fixed_time: &Time<Fixed>,
) -> f32 {
let (last_tick, frozen_for) = watch;
if ticks == *last_tick {
*frozen_for += delta_secs;
} else {
*last_tick = ticks;
*frozen_for = 0.0;
}
if *frozen_for > fixed_time.timestep().as_secs_f32() * 1.5 {
1.0
} else {
fixed_time.overstep_fraction()
}
}
#[allow(clippy::too_many_arguments)]
pub fn interpolate_crabs(
sim: Res<Sim>,
art: Res<Art>,
fixed_time: Res<Time<Fixed>>,
time: Res<Time>,
mut commands: Commands,
mut rng: ResMut<effects::VisualRng>,
mut glint_clock: Local<f32>,
mut watch: Local<(u64, f32)>,
mut by_id: Local<HashMap<u32, Crab>>,
mut sprites: Query<(&CrabSprite, &mut Transform, &mut Sprite)>,
) {
let board = &sim.0;
let alpha = smoothing_alpha(board.ticks(), &mut watch, time.delta_secs(), &fixed_time);
by_id.clear();
by_id.extend(board.crabs().iter().map(|c| (c.id, *c)));
*glint_clock += time.delta_secs();
let glint_now = *glint_clock >= 0.22;
if glint_now {
*glint_clock = 0.0;
}
let lure_tint = board.lure().map(|(owner, _)| palette::player_color(owner));
for (sprite, mut transform, mut body) in &mut sprites {
let Some(crab) = by_id.get(&sprite.id) else {
continue; };
let base = body_color(crab.kind);
body.color = match lure_tint {
Some(tint) => base.mix(&tint, 0.55),
None => base,
};
let prev = layout::creature_pos(board, crab.prev_tile, crab.prev_dir, crab.prev_progress);
let curr = layout::creature_pos(board, crab.tile, crab.dir, crab.progress);
let pos = if prev.distance(curr) > TILE * 2.0 {
curr
} else {
prev.lerp(curr, alpha)
};
transform.translation = pos.extend(layout::z::CREATURE);
let moving = prev != curr;
let wobble = if moving {
(time.elapsed_secs() * 11.0 + f32::from(sprite.id as u16 % 32)).sin() * 0.06
} else {
0.0
};
transform.rotation = layout::dir_rotation(crab.dir) * Quat::from_rotation_z(wobble);
let frame_b = moving
&& ((time.elapsed_secs() * 7.0 + f32::from(sprite.id as u16 % 8)) as u32)
.is_multiple_of(2);
let wanted = if frame_b { &art.crab_b } else { &art.crab };
if body.image != *wanted {
body.image = wanted.clone();
}
if sprite.kind == CrabKind::Molting {
let pulse = 1.0 + (time.elapsed_secs() * 5.0).sin() * 0.06;
transform.scale = Vec3::splat(pulse);
}
if glint_now {
match sprite.kind {
CrabKind::Sparkling => effects::glint(
&mut commands,
&mut rng,
&art,
curr,
Color::srgba(0.8, 0.95, 1.0, 0.95),
),
CrabKind::Golden => effects::glint(
&mut commands,
&mut rng,
&art,
curr,
Color::srgba(1.0, 0.9, 0.3, 0.95),
),
CrabKind::Common | CrabKind::Juvenile | CrabKind::Giant | CrabKind::Molting => {}
}
}
}
}
pub fn sync_gull_sprites(
mut commands: Commands,
sim: Res<Sim>,
art: Res<Art>,
mut live: Local<HashMap<u32, Gull>>,
existing: Query<(Entity, &GullSprite)>,
) {
let board = &sim.0;
live.clear();
live.extend(board.gulls().iter().map(|g| (g.id, *g)));
for (entity, sprite) in &existing {
if live.remove(&sprite.0).is_none() {
commands.entity(entity).despawn();
}
}
for (&id, gull) in live.iter() {
let pos = layout::creature_pos(board, gull.tile, gull.dir, gull.progress);
commands
.spawn((
GullSprite(id),
Sprite {
image: art.gull.clone(),
custom_size: Some(Vec2::splat(TILE * 0.78)),
..default()
},
Transform::from_translation(pos.extend(layout::z::CREATURE + 0.2))
.with_rotation(layout::dir_rotation(gull.dir)),
))
.with_children(|parent| {
parent.spawn((
CreatureShadow,
Sprite {
image: art.shadow.clone(),
custom_size: Some(Vec2::new(TILE * 0.62, TILE * 0.46)),
..default()
},
Transform::from_translation(Vec3::new(0.0, 0.0, -0.5)),
));
});
}
}
#[allow(clippy::too_many_arguments)]
pub fn interpolate_gulls(
sim: Res<Sim>,
art: Res<Art>,
fixed_time: Res<Time<Fixed>>,
time: Res<Time>,
mut watch: Local<(u64, f32)>,
mut by_id: Local<HashMap<u32, Gull>>,
mut sprites: Query<
(&GullSprite, &mut Transform, &mut Sprite, &Children),
Without<CreatureShadow>,
>,
mut shadows: Query<(&mut Transform, &mut Sprite), With<CreatureShadow>>,
) {
let board = &sim.0;
let alpha = smoothing_alpha(board.ticks(), &mut watch, time.delta_secs(), &fixed_time);
by_id.clear();
by_id.extend(board.gulls().iter().map(|g| (g.id, *g)));
let t = time.elapsed_secs();
for (sprite, mut transform, mut image, children) in &mut sprites {
let Some(gull) = by_id.get(&sprite.0) else {
continue;
};
let prev = layout::creature_pos(board, gull.prev_tile, gull.prev_dir, gull.prev_progress);
let curr = layout::creature_pos(board, gull.tile, gull.dir, gull.progress);
let pos = if prev.distance(curr) > TILE * 2.0 {
curr
} else {
prev.lerp(curr, alpha)
};
transform.translation = pos.extend(layout::z::CREATURE + 0.2);
let phase = f32::from(sprite.0 as u16 % 32);
let flying = matches!(gull.state, GullState::Flying { .. });
let wanted = if flying { &art.gull_fly } else { &art.gull };
if image.image != *wanted {
image.image = wanted.clone();
}
let (scale, waddle) = if flying {
(1.18 + (t * 16.0 + phase).sin() * 0.05, 0.0)
} else if prev == curr {
(1.0, 0.0)
} else {
(1.0, (t * 9.0 + phase).sin() * 0.05)
};
transform.rotation = layout::dir_rotation(gull.dir) * Quat::from_rotation_z(waddle);
transform.scale = Vec3::splat(scale);
for child in children {
let Ok((mut shadow_tf, mut shadow_sprite)) = shadows.get_mut(*child) else {
continue;
};
if flying {
let drift = Vec2::new((t * 2.1 + phase).cos() * 2.5, (t * 1.7 + phase).sin() * 2.5);
shadow_tf.translation = Vec3::new(-8.0 + drift.x, -8.0 + drift.y, -0.5);
shadow_tf.scale = Vec3::splat(0.55 / scale);
shadow_sprite.color = Color::srgba(1.0, 1.0, 1.0, 0.55);
} else {
shadow_tf.translation = Vec3::new(0.0, 0.0, -0.5);
shadow_tf.scale = Vec3::ONE;
shadow_sprite.color = Color::WHITE;
}
}
}
}