pinch-points 0.9.0

A fast, kid-friendly crab-routing game: route streams of crabs into your sandcastle before time runs out
//! Everything drawn from board state, split by what it draws:
//!
//! - [`statics`]: the sand, the walls, the terrain, and the sprites that
//!   track tile contents (turnstiles, spawner holes).
//! - [`signposts`]: the posts players plant, and how worn and settled
//!   each one is drawn.
//! - [`pools`]: the ponds over pool tiles, and their ripples.
//! - [`surround`]: the beach, the sea and the scenery around the board,
//!   and the cloud shadows over it.
//! - [`castles`]: the keeps, their pennants, and the shudder a bank sets
//!   off.
//!
//! The shared bits, the sprite helper and the wall colour, live here, and
//! every module's public items are re-exported so the schedule and the
//! teardown code see one `board_render`.

mod castles;
mod pools;
mod signposts;
mod statics;
mod surround;
mod tide;

pub use castles::{
    CastleFlight, CastleKick, CastleSprite, castle_body, cheer_tier_ups, crown_the_leader,
    flinch_castles, fly_castles, kick_castles, steady_castle_shadows, sync_castles, wave_pennants,
};
pub(crate) use castles::{castle_parts, kick_keep};
pub use pools::ripple_pools;
pub(crate) use pools::{POOL_LAYERS, spawn_glint, spawn_layer};
pub use signposts::{SignpostSprite, dress_signposts, sync_signposts};
pub use statics::{
    BoardStatic, TurnstileSprite, animate_turnstiles, pulse_spawners, spawn_board_grid,
    spawn_static_board, sync_turnstiles,
};
pub use surround::{drift_cloud_shadows, lay_the_sea, sea_uniform};

/// The parts of a board sprite the round's systems find it by and redraw
/// it from: what a still picture of a board takes off the sprites it
/// borrows ([`crate::app::match_setup::render_previews`]), so nothing
/// keeps it in step with the round's own board.
pub(crate) type LiveBoard = (
    BoardStatic,
    CastleSprite,
    CastleKick,
    castles::CastleFlinch,
    TurnstileSprite,
    SignpostSprite,
    statics::SpawnerSprite,
    pools::PoolRipple,
);

/// What sits on the board's tiles and changes in play: each castle at its
/// tier, each turnstile log at its lean, each post planted, by the
/// spawners the round's sync systems draw them with ([`sync_castles`],
/// [`sync_turnstiles`], [`sync_signposts`]). Laid over
/// [`spawn_board_grid`], it is the board as a round shows it: what a map
/// card's preview is drawn from.
pub fn spawn_board_pieces(
    commands: &mut Commands,
    board: &crate::sim::Board,
    art: &crate::app::art::Art,
) {
    use crate::sim::TileKind;
    for (x, y, kind) in board.tiles() {
        match kind {
            TileKind::Castle(owner) => {
                castles::spawn_castle(commands, art, board, (x, y), owner);
            }
            TileKind::Turnstile { next_right } => {
                statics::spawn_log(commands, art, board, (x, y), next_right);
            }
            // Drawn by the grid: none of them changes in play.
            TileKind::Empty
            | TileKind::Rock
            | TileKind::Spawner(_)
            | TileKind::Kelp
            | TileKind::Pool => {}
        }
        if let Some(sp) = board.signpost_at(x, y) {
            signposts::spawn_signpost(commands, art, board, (x, y), sp);
        }
    }
}

use crate::app::layout::image_sprite;
use bevy::prelude::*;

/// A thing that throws a shadow along the sun: how wide it stands on the
/// sand, how tall it is, and how dark its shadow falls.
pub(super) struct Caster {
    pub footprint: f32,
    pub height: f32,
    pub alpha: f32,
}

/// The shadow `caster` throws along the sun from its footprint centred on
/// `at` (its depth in `at.z`): an ellipse stretched the way the light falls
/// and pushed out by the height. A blob straight under a tall thing hides
/// behind it, and only its soft, nearly clear edge peeks out: a rock and
/// its shadow read as one grey shape.
pub(super) fn cast_shadow(
    art: &crate::app::art::Art,
    at: Vec3,
    caster: Caster,
) -> (Sprite, Transform) {
    let Caster {
        footprint,
        height,
        alpha,
    } = caster;
    let sun = crate::app::layout::SUN.normalize();
    let ground = at.truncate() + crate::app::layout::SUN + sun * height;
    (
        image_sprite(
            &art.shadow,
            Color::srgba(1.0, 1.0, 1.0, alpha),
            Vec2::new(footprint * 1.15, footprint * 0.55),
        ),
        Transform::from_translation(ground.extend(at.z))
            .with_rotation(Quat::from_rotation_z(sun.y.atan2(sun.x))),
    )
}

/// Whether a sprite's remembered tile exists on this board.
///
/// The sync systems diff sprites against the sim by the coordinates each
/// sprite was built at, and the sim's `tile_at` and `signpost_at` assert on
/// coordinates off the board, so a sprite left over from a bigger board is
/// a crash rather than a stale picture. Every probe checks here first and
/// despawns a stranded sprite.
fn on_board(board: &crate::sim::Board, x: u8, y: u8) -> bool {
    x < board.width() && y < board.height()
}

/// A sprite drawn for one tile, which remembers the tile it was built at.
trait OnTile {
    fn tile(&self) -> (u8, u8);
}

/// The clearing half of a sync system: what the tile-tracking sprites
/// (castles, signposts, turnstiles) all do before spawning what is missing.
///
/// Every sprite whose tile is off this board is despawned first, before
/// anything asks the board about it: that is the crash guard (see
/// [`on_board`]). Then every one that `current` says no longer matches its
/// tile goes too. The tiles whose sprite stands are marked in `covered`,
/// indexed as [`crate::sim::Board::index_of`], for the spawning half to
/// skip. `current` may retarget a sprite in place as it answers.
fn clear_stale<S: OnTile, T: std::ops::Deref<Target = S>>(
    commands: &mut Commands,
    board: &crate::sim::Board,
    covered: &mut Vec<bool>,
    sprites: impl IntoIterator<Item = (Entity, T)>,
    mut current: impl FnMut(&mut T) -> bool,
) {
    covered.clear();
    covered.resize(board.width() as usize * board.height() as usize, false);
    for (entity, mut sprite) in sprites {
        let (x, y) = sprite.tile();
        if !on_board(board, x, y) {
            commands.entity(entity).despawn();
            continue;
        }
        if current(&mut sprite) {
            covered[usize::from(board.index_of(x, y))] = true;
        } else {
            commands.entity(entity).despawn();
        }
    }
}

const WALL_COLOR: Color = Color::srgb(0.35, 0.28, 0.22);
/// How far past the sand the camera keeps clear round the board, in world
/// units: the wooden frame, and the strip of beach between it and the
/// interface. `boot::fit_camera` keeps this band inside the screen room it
/// has at 1:1, so a zoomed-in board does not push its edge under the
/// interface.
///
/// Its width sets the size every board is drawn at: changing it redraws
/// every board larger or smaller.
pub(crate) const RIM: f32 = 51.0;

/// How thick a driftwood plank of the fence is, in world units, across
/// the edge it stands on: the fence round the board reaches half of it
/// past the sand.
pub(crate) const FENCE_DEPTH: f32 = 13.0;

pub(super) use crate::app::layout::z;