use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{
Commands, Component, DetectChanges, Query, Ref, RemovedComponents, Res,
ResMut, Without,
};
use serde::{Deserialize, Serialize};
use super::{
ActionMap, CharacterMove, Collider, ColliderShape, FrameTime, MeshRenderer,
PhysicsBody, PhysicsWorld, RigidBody, RigidBodyKind, RuntimeInput,
PLAYER_JUMP, PLAYER_LEFT, PLAYER_RIGHT,
};
use crate::assets::{
AlphaMode, AssetServer, Handle, MaterialAsset, MaterialModel,
PrimitiveShape,
};
use crate::Transform;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct TileKind {
pub color: [f32; 4],
pub texture: Option<PathBuf>,
pub solid: bool,
}
impl Default for TileKind {
fn default() -> Self {
Self {
color: [1.0; 4],
texture: None,
solid: true,
}
}
}
pub const MAX_TILE_CELLS: usize = 4096;
#[derive(Component, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct TileMap {
pub tile_size: f32,
pub rows: Vec<String>,
pub tiles: BTreeMap<String, TileKind>,
}
impl Default for TileMap {
fn default() -> Self {
Self {
tile_size: 1.0,
rows: vec!["#...#".into(), "#####".into()],
tiles: BTreeMap::from([("#".into(), TileKind::default())]),
}
}
}
impl TileMap {
#[must_use]
pub fn cell_center(&self, column: usize, row: usize) -> [f32; 2] {
[
(column as f32 + 0.5) * self.tile_size,
-(row as f32 + 0.5) * self.tile_size,
]
}
#[must_use]
pub fn cell_at(&self, local: [f32; 2]) -> Option<(usize, usize)> {
let column = (local[0] / self.tile_size).floor();
let row = (-local[1] / self.tile_size).floor();
let range = 0.0..MAX_TILE_CELLS as f32;
(range.contains(&column) && range.contains(&row))
.then_some((column as usize, row as usize))
}
#[must_use]
pub fn cell(&self, column: usize, row: usize) -> char {
self.rows
.get(row)
.and_then(|text| text.chars().nth(column))
.unwrap_or('.')
}
pub fn set_cell(
&mut self,
column: usize,
row: usize,
character: char,
) -> bool {
if self.cell(column, row) == character {
return false;
}
if self.rows.len() <= row {
self.rows.resize(row + 1, String::new());
}
let mut cells: Vec<char> = self.rows[row].chars().collect();
if cells.len() <= column {
cells.resize(column + 1, '.');
}
cells[column] = character;
self.rows[row] = cells.into_iter().collect();
true
}
pub fn fill_rect(
&mut self,
from: (usize, usize),
to: (usize, usize),
character: char,
) -> bool {
let columns = from.0.min(to.0)..=from.0.max(to.0);
let mut changed = false;
for row in from.1.min(to.1)..=from.1.max(to.1) {
if columns
.clone()
.all(|column| self.cell(column, row) == character)
{
continue;
}
if self.rows.len() <= row {
self.rows.resize(row + 1, String::new());
}
let mut cells: Vec<char> = self.rows[row].chars().collect();
if cells.len() <= *columns.end() {
cells.resize(columns.end() + 1, '.');
}
cells[columns.clone()].fill(character);
self.rows[row] = cells.into_iter().collect();
changed = true;
}
changed
}
pub fn fill_line(
&mut self,
from: (usize, usize),
to: (usize, usize),
character: char,
) -> bool {
let mut changed = false;
for (column, row) in Self::line_cells(from, to) {
changed |= self.set_cell(column, row, character);
}
changed
}
#[must_use]
pub fn line_cells(
from: (usize, usize),
to: (usize, usize),
) -> Vec<(usize, usize)> {
let (mut x, mut y) = (from.0 as i64, from.1 as i64);
let (end_x, end_y) = (to.0 as i64, to.1 as i64);
let (dx, dy) = ((end_x - x).abs(), -(end_y - y).abs());
let (step_x, step_y) = ((end_x - x).signum(), (end_y - y).signum());
let mut error = dx + dy;
let mut cells = vec![(from.0, from.1)];
while (x, y) != (end_x, end_y) {
let doubled = 2 * error;
if doubled >= dy {
error += dy;
x += step_x;
}
if doubled <= dx {
error += dx;
y += step_y;
}
cells.push((x as usize, y as usize));
}
cells
}
pub fn fill(&mut self, column: usize, row: usize, character: char) -> bool {
let target = self.cell(column, row);
let width = self
.rows
.iter()
.map(|text| text.chars().count())
.max()
.unwrap_or(0);
let height = self.rows.len();
if target == character || column >= width || row >= height {
return false;
}
let mut open = vec![(column, row)];
let mut changed = false;
while let Some((column, row)) = open.pop() {
if column >= width
|| row >= height
|| self.cell(column, row) != target
{
continue;
}
changed |= self.set_cell(column, row, character);
open.extend([(column + 1, row), (column, row + 1)]);
open.extend(column.checked_sub(1).map(|left| (left, row)));
open.extend(row.checked_sub(1).map(|up| (column, up)));
}
changed
}
#[must_use]
pub fn kind_at(&self, column: usize, row: usize) -> Option<&TileKind> {
let character = self.rows.get(row)?.chars().nth(column)?;
self.tiles.get(character.encode_utf8(&mut [0; 4]) as &str)
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
pub struct TileOf(pub Entity);
pub(super) fn build_tile_maps(
mut commands: Commands,
assets: Option<ResMut<AssetServer>>,
maps: Query<(Entity, Ref<TileMap>, Ref<Transform>)>,
mut removed: RemovedComponents<TileMap>,
tiles: Query<(Entity, &TileOf)>,
) {
let mut stale: BTreeSet<Entity> = removed.read().collect();
let changed: Vec<_> = maps
.iter()
.filter(|(_, map, transform)| {
map.is_changed() || transform.is_changed()
})
.collect();
stale.extend(changed.iter().map(|(entity, ..)| *entity));
stale.extend(
tiles
.iter()
.map(|(_, owner)| owner.0)
.filter(|owner| !maps.contains(*owner)),
);
if stale.is_empty() {
return;
}
for (tile, owner) in &tiles {
if stale.contains(&owner.0) {
commands.entity(tile).despawn();
}
}
let Some(mut assets) = assets else {
return;
};
let quad = assets.builtin_primitive(PrimitiveShape::Quad);
for (entity, map, transform) in changed {
let origin = transform.position;
let size = map.tile_size;
let mut materials = BTreeMap::new();
for (row, text) in map.rows.iter().enumerate() {
let mut run: Option<usize> = None;
let columns = text.chars().count();
for column in 0..=columns {
let kind = map.kind_at(column, row);
if let Some(kind) = kind {
let key = text.chars().nth(column).unwrap_or(' ');
let material = *materials
.entry(key)
.or_insert_with(|| tile_material(&mut assets, kind));
let [x, y] = map.cell_center(column, row);
commands.spawn((
Transform {
position: [origin[0] + x, origin[1] + y, origin[2]],
scale: [size, size, 1.0],
..Transform::default()
},
MeshRenderer {
mesh: quad,
material,
cast_shadows: false,
receive_shadows: false,
},
TileOf(entity),
));
}
let solid = kind.is_some_and(|kind| kind.solid);
match (solid, run) {
(true, None) => run = Some(column),
(false, Some(start)) => {
run = None;
let width = (column - start) as f32 * size;
let [x, y] = map.cell_center(start, row);
commands.spawn((
Transform::new([
origin[0] + x - size / 2.0 + width / 2.0,
origin[1] + y,
origin[2],
]),
PhysicsBody::default(),
RigidBody {
kind: RigidBodyKind::Fixed,
..RigidBody::default()
},
Collider {
shape: ColliderShape::Box {
half_extents: [
width / 2.0,
size / 2.0,
size / 2.0,
],
},
..Collider::default()
},
TileOf(entity),
));
}
_ => {}
}
}
}
}
}
fn tile_material(
assets: &mut AssetServer,
kind: &TileKind,
) -> Handle<MaterialAsset> {
let texture = kind.texture.as_ref().and_then(|path| {
assets
.textures
.handle_for_path(path)
.map(Ok)
.unwrap_or_else(|| assets.load_texture(path))
.map_err(|error| eprintln!("tile texture: {error}"))
.ok()
});
let material = MaterialAsset {
model: MaterialModel::Unlit,
alpha_mode: if texture.is_some() {
AlphaMode::Mask { cutoff: 0.5 }
} else {
AlphaMode::Opaque
},
base_color: kind.color,
base_color_texture: texture,
..MaterialAsset::default()
};
let existing = assets.materials.iter().find_map(|(handle, existing)| {
(*existing == material).then_some(handle)
});
existing.unwrap_or_else(|| assets.materials.insert(material))
}
pub const DEFAULT_PLATFORMER_SHAPE: ColliderShape = ColliderShape::Capsule {
half_height: 0.2,
radius: 0.3,
};
#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct PlatformerController {
pub run_speed: f32,
pub jump_speed: f32,
pub gravity: f32,
pub collision_mask: u32,
#[serde(skip)]
pub vertical_speed: f32,
#[serde(skip)]
pub grounded: bool,
#[serde(skip)]
pub jump_buffer: f32,
#[serde(skip)]
pub air_time: f32,
#[serde(skip)]
pub floor: Option<(Entity, [f32; 3])>,
}
pub const JUMP_GRACE: f32 = 0.1;
impl Default for PlatformerController {
fn default() -> Self {
Self {
run_speed: 5.0,
jump_speed: 8.0,
gravity: 20.0,
collision_mask: u32::MAX,
vertical_speed: 0.0,
grounded: false,
jump_buffer: 0.0,
air_time: 0.0,
floor: None,
}
}
}
pub(super) fn platformer_jump(
input: Res<RuntimeInput>,
actions: Res<ActionMap>,
mut players: Query<&mut PlatformerController>,
) {
if actions.just_pressed(&input, PLAYER_JUMP) {
for mut player in &mut players {
player.jump_buffer = JUMP_GRACE;
}
}
}
#[allow(clippy::type_complexity)]
pub(super) fn platformer_move(
time: Res<FrameTime>,
input: Res<RuntimeInput>,
actions: Res<ActionMap>,
physics: Res<PhysicsWorld>,
mut players: Query<(
Entity,
&mut PlatformerController,
&mut Transform,
Option<&Collider>,
)>,
floors: Query<
(
&Transform,
Option<&super::Parent>,
Option<&super::GlobalTransform>,
),
Without<PlatformerController>,
>,
) {
let floor_at = |floor: Entity| {
let (t, parent, global) = floors.get(floor).ok()?;
Some(super::cpu_physics::world_position(t, parent, global))
};
let dt = time.fixed_delta.as_secs_f32();
let run = f32::from(u8::from(actions.held(&input, PLAYER_RIGHT)))
- f32::from(u8::from(actions.held(&input, PLAYER_LEFT)));
for (entity, mut player, mut transform, collider) in &mut players {
if player.jump_buffer > 0.0 && player.air_time <= JUMP_GRACE {
player.vertical_speed = player.jump_speed;
player.jump_buffer = 0.0;
player.air_time = f32::INFINITY;
}
player.jump_buffer = (player.jump_buffer - dt).max(0.0);
player.vertical_speed -= player.gravity * dt;
let shape = collider
.map_or(DEFAULT_PLATFORMER_SHAPE, |collider| collider.shape);
let start = CharacterMove::ride(
&physics,
shape,
transform.position,
player.floor,
player.collision_mask,
entity,
floor_at,
);
let moved = physics.move_character(
shape,
start,
[run * player.run_speed * dt, player.vertical_speed * dt, 0.0],
player.collision_mask,
Some(entity),
);
player.grounded = moved.grounded;
if moved.grounded && player.vertical_speed < 0.0 {
player.vertical_speed = 0.0;
player.air_time = 0.0;
} else {
player.air_time += dt;
}
if moved.ceiling && player.vertical_speed > 0.0 {
player.vertical_speed = 0.0;
}
player.floor = moved
.floor
.and_then(|floor| Some((floor, floor_at(floor)?)));
transform.position = moved.position;
}
}