use super::pools::spawn_pools;
use super::surround::{spawn_beach, spawn_weather};
use super::{OnTile, clear_stale, image_sprite, z};
use crate::app::Sim;
use crate::app::art::Art;
use crate::app::layout::{self, TILE};
use crate::sim::{Board, Direction, TileKind};
use bevy::prelude::*;
#[derive(Component)]
pub struct BoardStatic;
fn ground_shadow(commands: &mut Commands, art: &Art, pos: Vec2, size: f32) {
commands.spawn((
BoardStatic,
image_sprite(
&art.shadow,
Color::srgba(1.0, 1.0, 1.0, 0.75),
Vec2::new(size, size * 0.82),
),
Transform::from_translation((pos + layout::SUN).extend(z::TILE_FEATURE - 0.05)),
));
}
fn spawn_tile_feature(
commands: &mut Commands,
art: &Art,
(x, y): (u8, u8),
pos: Vec2,
kind: TileKind,
) {
match kind {
TileKind::Empty => {}
TileKind::Rock => {
ground_shadow(commands, art, pos, TILE * 0.98);
commands.spawn((
BoardStatic,
super::cast_shadow(
art,
pos.extend(z::TILE_FEATURE - 0.06),
super::Caster {
footprint: TILE * 0.85,
height: TILE * 0.16,
alpha: 0.5,
},
),
));
commands.spawn((
BoardStatic,
image_sprite(&art.rock, Color::WHITE, Vec2::splat(TILE * 0.94)),
Transform::from_translation(pos.extend(z::TILE_FEATURE)),
));
}
TileKind::Castle(_) => {}
TileKind::Spawner(spawner) => {
commands.spawn((
BoardStatic,
SpawnerSprite {
period: spawner.period,
at: (x, y),
},
image_sprite(&art.hole, Color::WHITE, Vec2::splat(TILE * 0.78)),
Transform::from_translation(pos.extend(z::TILE_FEATURE)),
));
}
TileKind::Kelp => {
ground_shadow(
commands,
art,
pos + Vec2::new(0.0, -TILE * 0.22),
TILE * 0.7,
);
commands.spawn((
BoardStatic,
super::cast_shadow(
art,
(pos + Vec2::new(0.0, -TILE * 0.12)).extend(z::TILE_FEATURE - 0.06),
super::Caster {
footprint: TILE * 0.55,
height: TILE * 0.14,
alpha: 0.3,
},
),
));
commands.spawn((
BoardStatic,
image_sprite(&art.kelp, Color::WHITE, Vec2::splat(TILE * 0.94)),
Transform::from_translation(pos.extend(z::TILE_FEATURE)),
));
}
TileKind::Pool => {}
TileKind::Turnstile { .. } => {
commands.spawn((
BoardStatic,
image_sprite(&art.shadow, Color::WHITE, Vec2::splat(TILE * 0.8)),
Transform::from_translation((pos + layout::SUN).extend(z::POOL)),
));
}
}
}
pub fn spawn_static_board(commands: &mut Commands, board: &Board, art: &Art) {
spawn_beach(commands, board, art);
spawn_board_grid(commands, board, art);
spawn_weather(commands, board, art);
}
pub fn spawn_board_grid(commands: &mut Commands, board: &Board, art: &Art) {
commands.spawn((
BoardStatic,
art.shaded.quad(
&art.shaded.board_sand,
Transform::from_translation(Vec3::new(0.0, 0.0, z::SAND)),
layout::board_extent(board),
),
));
for (x, y, kind) in board.tiles() {
let pos = layout::tile_center(board, x, y);
spawn_tile_feature(commands, art, (x, y), pos, kind);
}
spawn_pools(commands, board, art);
spawn_walls(commands, board, art);
}
fn spawn_walls(commands: &mut Commands, board: &Board, art: &Art) {
let plank_size = Vec2::new(TILE + 8.0, super::FENCE_DEPTH);
let plank = |commands: &mut Commands, center: Vec2, vertical: bool| {
let rotation = if vertical {
Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)
} else {
Quat::IDENTITY
};
commands.spawn((
BoardStatic,
image_sprite(&art.plank, Color::srgba(0.10, 0.07, 0.05, 0.34), plank_size),
Transform::from_translation((center + layout::SUN).extend(z::TILE_FEATURE - 0.06))
.with_rotation(rotation),
));
commands.spawn((
BoardStatic,
image_sprite(&art.plank, Color::WHITE, plank_size),
Transform::from_translation(center.extend(z::WALL)).with_rotation(rotation),
));
};
let mut corner_hits = vec![0u8; (board.width() as usize + 1) * (board.height() as usize + 1)];
let corner_index = |cx: u16, cy: u16| cy as usize * (board.width() as usize + 1) + cx as usize;
for (x, y, _) in board.tiles() {
let pos = layout::tile_center(board, x, y);
if board.wall_at(x, y, Direction::Up) {
plank(commands, pos + Vec2::new(0.0, TILE / 2.0), false);
corner_hits[corner_index(u16::from(x), u16::from(y))] += 1;
corner_hits[corner_index(u16::from(x) + 1, u16::from(y))] += 1;
}
if board.wall_at(x, y, Direction::Left) {
plank(commands, pos + Vec2::new(-TILE / 2.0, 0.0), true);
corner_hits[corner_index(u16::from(x), u16::from(y))] += 1;
corner_hits[corner_index(u16::from(x), u16::from(y) + 1)] += 1;
}
if y == board.height() - 1 && board.wall_at(x, y, Direction::Down) {
plank(commands, pos + Vec2::new(0.0, -TILE / 2.0), false);
}
if x == board.width() - 1 && board.wall_at(x, y, Direction::Right) {
plank(commands, pos + Vec2::new(TILE / 2.0, 0.0), true);
}
}
let extent = layout::board_extent(board);
let (w, h) = (extent.x, extent.y);
for cy in 1..u16::from(board.height()) {
for cx in 1..u16::from(board.width()) {
if corner_hits[corner_index(cx, cy)] == 0 {
continue;
}
let corner = Vec2::new(
f32::from(cx) * TILE - w / 2.0,
h / 2.0 - f32::from(cy) * TILE,
);
commands.spawn((
BoardStatic,
image_sprite(&art.post, Color::WHITE, Vec2::splat(15.0)),
Transform::from_translation(corner.extend(z::WALL + 0.1)),
));
}
}
for side in Side::ALL {
let (along, across) = side.span(extent);
let offset = side.normal() * (across / 2.0 + 9.0);
commands.spawn((
BoardStatic,
image_sprite(&art.wet, Color::WHITE, Vec2::new(along + 36.0, 18.0)),
Transform::from_translation(offset.extend(z::WET))
.with_rotation(Quat::from_rotation_z(side.rotation())),
));
}
}
#[derive(Component)]
pub struct TurnstileSprite {
x: u8,
y: u8,
right: bool,
}
impl OnTile for TurnstileSprite {
fn tile(&self) -> (u8, u8) {
(self.x, self.y)
}
}
pub fn sync_turnstiles(
mut commands: Commands,
sim: Res<Sim>,
art: Res<Art>,
mut covered: Local<Vec<bool>>,
mut existing: Query<(Entity, &mut TurnstileSprite)>,
) {
let board = &sim.0;
clear_stale(
&mut commands,
board,
&mut covered,
&mut existing,
|sprite| {
match board.tile_at(sprite.x, sprite.y) {
TileKind::Turnstile { next_right } => {
if next_right != sprite.right {
sprite.right = next_right;
}
true
}
TileKind::Empty
| TileKind::Rock
| TileKind::Castle(_)
| TileKind::Spawner(_)
| TileKind::Kelp
| TileKind::Pool => false,
}
},
);
for (x, y, kind) in board.tiles() {
let TileKind::Turnstile { next_right } = kind else {
continue;
};
if covered[usize::from(board.index_of(x, y))] {
continue;
}
spawn_log(&mut commands, &art, board, (x, y), next_right);
}
}
pub(crate) fn spawn_log(
commands: &mut Commands,
art: &Art,
board: &Board,
(x, y): (u8, u8),
next_right: bool,
) -> Entity {
let tilt = if next_right { -0.6 } else { 0.6 };
commands
.spawn((
TurnstileSprite {
x,
y,
right: next_right,
},
image_sprite(&art.log, Color::WHITE, Vec2::splat(TILE * 0.98)),
Transform::from_translation(
layout::tile_center(board, x, y).extend(z::TILE_FEATURE + 0.1),
)
.with_rotation(Quat::from_rotation_z(tilt)),
))
.id()
}
#[derive(Component)]
pub struct SpawnerSprite {
period: u32,
at: (u8, u8),
}
pub fn pulse_spawners(sim: Res<Sim>, mut holes: Query<(&SpawnerSprite, &mut Transform)>) {
let tick = sim.0.ticks();
let called = sim.0.golden_call().map(|(_, x, y)| (x, y));
for (hole, mut transform) in &mut holes {
if called == Some(hole.at) {
transform.scale = Vec3::splat(1.3);
continue;
}
let period = sim.0.hole_period(hole.period);
if period == 0 {
continue;
}
let phase = (tick % u64::from(period)) as f32 / period as f32;
let swell = if phase > 0.75 {
1.0 + (phase - 0.75) * 0.6
} else {
1.0
};
transform.scale = Vec3::splat(swell);
}
}
pub fn animate_turnstiles(time: Res<Time>, mut logs: Query<(&TurnstileSprite, &mut Transform)>) {
for (sprite, mut transform) in &mut logs {
let target = if sprite.right { -0.6 } else { 0.6 };
let (_, _, current) = transform.rotation.to_euler(EulerRot::XYZ);
let next = current + (target - current) * (time.delta_secs() * 14.0).min(1.0);
if (next - current).abs() > 0.0005 {
transform.rotation = Quat::from_rotation_z(next);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Side {
Top,
Bottom,
Left,
Right,
}
impl Side {
const ALL: [Side; 4] = [Side::Top, Side::Bottom, Side::Left, Side::Right];
fn normal(self) -> Vec2 {
match self {
Side::Top => Vec2::Y,
Side::Bottom => Vec2::NEG_Y,
Side::Left => Vec2::NEG_X,
Side::Right => Vec2::X,
}
}
fn rotation(self) -> f32 {
match self {
Side::Top => std::f32::consts::PI,
Side::Bottom => 0.0,
Side::Left => -std::f32::consts::FRAC_PI_2,
Side::Right => std::f32::consts::FRAC_PI_2,
}
}
fn span(self, extent: Vec2) -> (f32, f32) {
match self {
Side::Top | Side::Bottom => (extent.x, extent.y),
Side::Left | Side::Right => (extent.y, extent.x),
}
}
}