use super::image_sprite;
use super::statics::BoardStatic;
use super::z;
use crate::app::art::Art;
use crate::app::layout::{self, TILE};
use crate::sim::{Board, TileKind};
use bevy::prelude::*;
#[derive(Clone, Copy)]
pub(crate) enum PoolLayer {
Shore(Color),
Shallow,
Deep,
}
pub(crate) const POOL_LAYERS: [(PoolLayer, f32, Vec2); 3] = [
(
PoolLayer::Shore(Color::srgb(0.77, 0.66, 0.48)),
1.1,
Vec2::new(1.2, 1.0),
),
(PoolLayer::Shallow, 0.98, Vec2::new(1.1, 0.86)),
(PoolLayer::Deep, 0.62, Vec2::new(1.0, 0.58)),
];
pub(crate) fn spawn_layer(
commands: &mut Commands,
art: &Art,
layer: PoolLayer,
place: Transform,
extent: Vec2,
marker: impl Bundle,
) {
match layer {
PoolLayer::Shore(tint) => {
commands.spawn((marker, image_sprite(&art.puddle, tint, extent), place));
}
PoolLayer::Shallow => {
let water = &art.shaded.pool_shallow;
commands.spawn((marker, art.shaded.quad(water, place, extent)));
}
PoolLayer::Deep => {
let water = &art.shaded.pool_deep;
commands.spawn((marker, art.shaded.quad(water, place, extent)));
}
}
}
pub(crate) fn spawn_glint(
commands: &mut Commands,
art: &Art,
at: Vec3,
across: Vec2,
marker: impl Bundle,
) {
let sun = -layout::SUN.normalize() * across * 0.13;
let place = Transform::from_translation(at + sun.extend(0.0))
.with_rotation(Quat::from_rotation_z(0.35));
let size = Vec2::new(across.x * 0.26, across.y * 0.1);
commands.spawn((marker, art.shaded.quad(&art.shaded.glint, place, size)));
}
#[derive(Component)]
pub struct PoolRipple {
phase: f32,
}
const RIPPLE_PERIOD: f32 = 4.2;
fn tile_hash(x: u8, y: u8) -> u32 {
let h = u32::from(x).wrapping_mul(0x9E37_79B1) ^ u32::from(y).wrapping_mul(0x85EB_CA77);
h ^ (h >> 15)
}
pub(super) fn spawn_pools(commands: &mut Commands, board: &Board, art: &Art) {
let pieces = pond_pieces(board);
for (layer, (kind, size, bridge)) in POOL_LAYERS.into_iter().enumerate() {
let z = z::POOL - 0.03 + 0.01 * layer as f32;
for piece in &pieces {
let (at, turn, extent) = match *piece {
PondPiece::Tile { at, turn } | PondPiece::Corner { at, turn } => {
(at, turn, Vec2::splat(size))
}
PondPiece::Bridge { at, turn } => (at, turn, bridge),
};
let place = Transform::from_translation(at.extend(z))
.with_rotation(Quat::from_rotation_z(turn));
spawn_layer(commands, art, kind, place, extent * TILE, BoardStatic);
}
}
for (x, y, kind) in board.tiles() {
if kind != TileKind::Pool {
continue;
}
let hash = tile_hash(x, y);
let wander = Vec2::new(
((hash >> 8) % 100) as f32 / 100.0 - 0.5,
((hash >> 16) % 100) as f32 / 100.0 - 0.5,
) * TILE
* 0.12;
commands.spawn((
BoardStatic,
PoolRipple {
phase: ((hash >> 4) % 1000) as f32 / 1000.0,
},
image_sprite(&art.ripple, Color::NONE, Vec2::splat(TILE)),
Transform::from_translation(
(layout::tile_center(board, x, y) + wander).extend(z::POOL + 0.005),
)
.with_rotation(Quat::from_rotation_z(pool_turn(hash))),
));
let at = (layout::tile_center(board, x, y) + wander).extend(z::POOL + 0.004);
spawn_glint(commands, art, at, Vec2::splat(TILE), BoardStatic);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PondPiece {
Tile { at: Vec2, turn: f32 },
Bridge { at: Vec2, turn: f32 },
Corner { at: Vec2, turn: f32 },
}
fn pool_turn(hash: u32) -> f32 {
(hash % 628) as f32 / 100.0
}
fn pond_pieces(board: &Board) -> Vec<PondPiece> {
let pool = |x: u8, y: u8| {
x < board.width() && y < board.height() && board.tile_at(x, y) == TileKind::Pool
};
let mut pieces = Vec::new();
for (x, y, kind) in board.tiles() {
if kind != TileKind::Pool {
continue;
}
let at = layout::tile_center(board, x, y);
let turn = pool_turn(tile_hash(x, y));
pieces.push(PondPiece::Tile { at, turn });
for (dx, dy, turn) in [(1, 0, 0.0), (0, 1, std::f32::consts::FRAC_PI_2)] {
if pool(x + dx, y + dy) {
let next = layout::tile_center(board, x + dx, y + dy);
pieces.push(PondPiece::Bridge {
at: (at + next) / 2.0,
turn,
});
}
}
if pool(x + 1, y) && pool(x, y + 1) && pool(x + 1, y + 1) {
pieces.push(PondPiece::Corner {
at: at + Vec2::new(TILE / 2.0, -TILE / 2.0),
turn: turn + 1.0,
});
}
}
pieces
}
pub fn ripple_pools(
time: Res<Time>,
settings: Res<crate::app::settings::GameSettings>,
mut ripples: Query<(&PoolRipple, &mut Transform, &mut Sprite)>,
) {
if settings.reduced_motion {
for (_, _, mut sprite) in &mut ripples {
if sprite.color != Color::NONE {
sprite.color = Color::NONE;
}
}
return;
}
let now = time.elapsed_secs();
for (ripple, mut transform, mut sprite) in &mut ripples {
let t = (now / RIPPLE_PERIOD + ripple.phase).fract();
transform.scale = Vec3::splat(0.12 + 0.5 * t);
let strength = (t * std::f32::consts::PI).sin();
layout::tint(&mut sprite, Color::srgba(0.88, 0.95, 0.98, 0.42 * strength));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn neighbouring_puddles_turn_their_own_ways() {
for y in 0..6u8 {
for x in 0..6u8 {
let turn = pool_turn(tile_hash(x, y));
assert!((0.0..std::f32::consts::TAU).contains(&turn), "{turn}");
assert_ne!(
tile_hash(x, y),
tile_hash(x + 1, y),
"({x}, {y}) and its right"
);
assert_ne!(tile_hash(x, y), tile_hash(x, y + 1), "({x}, {y}) and below");
}
}
}
#[test]
fn pools_hold_still_for_less_motion() {
let mut app = App::new();
app.init_resource::<Time>();
app.insert_resource(crate::app::settings::GameSettings {
reduced_motion: true,
..Default::default()
});
app.add_systems(Update, ripple_pools);
let ripple = app
.world_mut()
.spawn((
PoolRipple { phase: 0.5 },
Sprite::from_color(Color::WHITE, Vec2::ONE),
Transform::default(),
))
.id();
app.update();
let sprite = app.world().get::<Sprite>(ripple).expect("a ripple");
assert_eq!(sprite.color, Color::NONE);
}
fn pond(width: u8, height: u8, pools: &[(u8, u8)]) -> Vec<PondPiece> {
let mut board = Board::new(width, height, 1);
for &(x, y) in pools {
board.set_tile(x, y, TileKind::Pool);
}
pond_pieces(&board)
}
fn count(pieces: &[PondPiece]) -> (usize, usize, usize) {
let tiles = pieces
.iter()
.filter(|p| matches!(p, PondPiece::Tile { .. }));
let bridges = pieces
.iter()
.filter(|p| matches!(p, PondPiece::Bridge { .. }));
let corners = pieces
.iter()
.filter(|p| matches!(p, PondPiece::Corner { .. }));
(tiles.count(), bridges.count(), corners.count())
}
#[test]
fn a_lone_pool_is_one_puddle() {
assert_eq!(count(&pond(5, 5, &[(2, 2)])), (1, 0, 0));
}
#[test]
fn a_lane_is_bridged_between_every_pair() {
let across = pond(6, 3, &[(1, 1), (2, 1), (3, 1)]);
assert_eq!(count(&across), (3, 2, 0));
let down = pond(3, 6, &[(1, 1), (1, 2), (1, 3)]);
assert_eq!(count(&down), (3, 2, 0));
for piece in down {
if let PondPiece::Bridge { at, turn } = piece {
assert_eq!(at.x, 0.0, "down the middle column");
assert_eq!(at.y % TILE, 0.0, "on the edge between two rows: {at}");
assert_eq!(turn, std::f32::consts::FRAC_PI_2, "along the lane");
}
}
}
#[test]
fn a_square_of_pools_has_no_dry_middle() {
let square = pond(4, 4, &[(1, 1), (2, 1), (1, 2), (2, 2)]);
assert_eq!(count(&square), (4, 4, 1));
let corner = square.iter().find_map(|p| {
if let PondPiece::Corner { at, .. } = p {
Some(*at)
} else {
None
}
});
assert_eq!(corner, Some(Vec2::ZERO), "the square's middle, dead centre");
}
#[test]
fn diagonal_pools_stay_apart() {
assert_eq!(count(&pond(4, 4, &[(1, 1), (2, 2)])), (2, 0, 0));
}
}