use crate::sim::{Board, Direction, Pose};
use bevy::prelude::*;
pub const TILE: f32 = 64.0;
pub const SIGN_SIZE: f32 = TILE * 0.94;
pub const SPRITE_PX: f32 = 192.0;
pub const SMALLEST_FIT: Vec2 = Vec2::new(12.0 * TILE, 9.0 * TILE);
pub const SMALL_BOARD_ZOOM: f32 = 1.25;
pub const MAX_ZOOM: f32 = SPRITE_PX / TILE;
pub const SUN: Vec2 = Vec2::new(4.5, -6.3);
pub mod z {
pub const SAND: f32 = 0.0;
pub const WET: f32 = 0.2;
pub const POOL: f32 = 0.5;
pub const TILE_FEATURE: f32 = 1.0;
pub const CASTLE: f32 = TILE_FEATURE + 0.2;
pub const SIGNPOST: f32 = 2.0;
pub const CREATURE: f32 = 3.0;
pub const WALL: f32 = 4.0;
pub const FLAG: f32 = 4.15;
pub const CROWN: f32 = 4.2;
pub const WATER: f32 = 4.5;
pub const FOAM: f32 = 4.6;
pub const ARRIVING_GULL: f32 = 4.8;
pub const CURSOR: f32 = 5.0;
pub const CONFETTI: f32 = 5.4;
pub const FLASH: f32 = 5.45;
pub const PARTICLE: f32 = 5.5;
pub const PIP: f32 = 5.7;
}
pub fn tint(sprite: &mut impl std::ops::DerefMut<Target = Sprite>, color: Color) {
if sprite.color != color {
sprite.color = color;
}
}
pub fn image_sprite(image: &Handle<Image>, tint: Color, size: Vec2) -> Sprite {
Sprite {
image: image.clone(),
color: tint,
custom_size: Some(size),
..default()
}
}
pub fn board_extent(board: &Board) -> Vec2 {
Vec2::new(
f32::from(board.width()) * TILE,
f32::from(board.height()) * TILE,
)
}
pub fn tile_center(board: &Board, x: u8, y: u8) -> Vec2 {
let w = f32::from(board.width());
let h = f32::from(board.height());
Vec2::new(
(f32::from(x) - (w - 1.0) / 2.0) * TILE,
((h - 1.0) / 2.0 - f32::from(y)) * TILE,
)
}
pub fn nearest_tile(board: &Board, pos: Vec2) -> (u8, u8) {
let w = f32::from(board.width());
let h = f32::from(board.height());
let x = (pos.x / TILE + (w - 1.0) / 2.0).round().clamp(0.0, w - 1.0);
let y = ((h - 1.0) / 2.0 - pos.y / TILE).round().clamp(0.0, h - 1.0);
(x as u8, y as u8)
}
pub fn creature_pos(board: &Board, tile: u16, dir: Direction, progress: u16) -> Vec2 {
let (x, y) = board.coords_u8(tile);
let (dx, dy) = dir.offset();
let t = f32::from(progress) / f32::from(crate::sim::SUBUNITS_PER_TILE) * TILE;
tile_center(board, x, y) + Vec2::new(dx as f32 * t, -dy as f32 * t)
}
pub fn pose_pos(board: &Board, pose: Pose) -> Vec2 {
creature_pos(board, pose.tile, pose.dir, pose.progress)
}
pub fn dir_rotation(dir: Direction) -> Quat {
let angle = match dir {
Direction::Right => 0.0,
Direction::Up => std::f32::consts::FRAC_PI_2,
Direction::Left => std::f32::consts::PI,
Direction::Down => -std::f32::consts::FRAC_PI_2,
};
Quat::from_rotation_z(angle)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::Direction::{Down, Left, Right, Up};
#[test]
fn tinting_a_sprite_its_own_colour_leaves_it_untouched() {
#[derive(Resource)]
struct Want(Color);
#[derive(Resource, Default)]
struct Seen(Vec<bool>);
let mut app = App::new();
app.insert_resource(Want(Color::WHITE));
app.init_resource::<Seen>();
app.add_systems(
Update,
(
|want: Res<Want>, mut sprites: Query<&mut Sprite>| {
for mut sprite in &mut sprites {
tint(&mut sprite, want.0);
}
},
|sprites: Query<Ref<Sprite>>, mut seen: ResMut<Seen>| {
seen.0.extend(sprites.iter().map(|s| s.is_changed()));
},
)
.chain(),
);
app.world_mut()
.spawn(Sprite::from_color(Color::WHITE, Vec2::ONE));
app.update();
app.update();
app.world_mut().resource_mut::<Want>().0 = Color::BLACK;
app.update();
app.update();
assert_eq!(app.world().resource::<Seen>().0, [true, false, true, false]);
let sprite = app.world_mut().query::<&Sprite>().single(app.world());
assert_eq!(sprite.expect("one sprite").color, Color::BLACK);
}
#[test]
fn tile_centers_round_trip_through_nearest_tile() {
let board = Board::new(12, 9, 1);
for y in 0..board.height() {
for x in 0..board.width() {
let pos = tile_center(&board, x, y);
assert_eq!(nearest_tile(&board, pos), (x, y));
}
}
}
#[test]
fn creature_pos_at_rest_is_the_tile_center() {
let board = Board::new(7, 5, 1);
for tile in 0..(7 * 5u16) {
let (x, y) = board.coords_u8(tile);
assert_eq!(
creature_pos(&board, tile, Right, 0),
tile_center(&board, x, y)
);
}
}
use crate::sim::Board;
#[test]
fn tile_centers_are_symmetric_about_the_board_middle() {
let board = Board::new(5, 3, 0);
let a = tile_center(&board, 0, 0);
let b = tile_center(&board, 4, 2);
assert_eq!(a, -b, "opposite corners mirror through the origin");
assert_eq!(tile_center(&board, 2, 1), Vec2::ZERO, "centre tile is 0,0");
}
#[test]
fn a_point_off_the_board_clamps_onto_it() {
let board = Board::new(9, 7, 1);
let far = 10_000.0;
assert_eq!(nearest_tile(&board, Vec2::new(-far, far)), (0, 0));
assert_eq!(nearest_tile(&board, Vec2::new(far, -far)), (8, 6));
assert_eq!(nearest_tile(&board, Vec2::new(far, far)), (8, 0));
}
#[test]
fn a_full_stride_arrives_on_the_neighbour_and_flips_the_axis() {
let board = Board::new(9, 7, 1);
let tile = board.index_of(4, 3);
let full = crate::sim::SUBUNITS_PER_TILE;
let step = |dir| creature_pos(&board, tile, dir, full);
assert_eq!(step(Right), tile_center(&board, 5, 3));
assert_eq!(step(Left), tile_center(&board, 3, 3));
assert_eq!(step(Down), tile_center(&board, 4, 4));
assert!(step(Down).y < tile_center(&board, 4, 3).y, "down is down");
assert!(step(Up).y > tile_center(&board, 4, 3).y, "and up is up");
}
#[test]
fn a_pose_reads_the_same_as_its_parts() {
let board = Board::new(12, 9, 1);
for progress in [0, 17, 64, crate::sim::SUBUNITS_PER_TILE - 1] {
for dir in [Up, Down, Left, Right] {
let tile = board.index_of(6, 4);
let pose = Pose {
tile,
dir,
progress,
};
assert_eq!(
pose_pos(&board, pose),
creature_pos(&board, tile, dir, progress)
);
}
}
}
#[test]
fn a_heading_turns_the_art_from_facing_right() {
let ahead = Vec3::X;
let turn = |dir| (dir_rotation(dir) * ahead).truncate();
let close = |a: Vec2, b: Vec2| a.distance(b) < 1e-5;
assert!(close(turn(Right), Vec2::X), "{:?}", turn(Right));
assert!(close(turn(Left), -Vec2::X), "{:?}", turn(Left));
assert!(close(turn(Up), Vec2::Y), "{:?}", turn(Up));
assert!(close(turn(Down), -Vec2::Y), "{:?}", turn(Down));
}
#[test]
fn the_sun_casts_down_and_to_the_right() {
const { assert!(SUN.x > 0.0, "shadows fall to the right") };
const { assert!(SUN.y < 0.0, "and downward, in world space") };
assert!(
SUN.x.abs() > 1.0 && SUN.y.abs() > 1.0,
"{SUN:?} is too flat"
);
}
}