feat: stage 2 collision, TileChangedEvent, path invalidation, rabbit digging
- Stage 2 collision: convoy skip when entities move same direction (dot>0.7), E/S yield rule for crossings, W/N right-of-way, head-on unchanged - TileChangedEvent (Message) + PathfindingDirtyChunks (Resource) in new tile_changed module - collect_pathfinding_dirty_chunks / invalidate_paths_on_tile_change / clear_pathfinding_dirty_chunks in PathfindingPlugin FixedUpdate chain - TileMap::remove_floor clears HashMap + ChunkData bitsets + tile_ids - RabbitDigTimer component: rabbits dig floor below every 5s, fires TileChangedEvent and TileOcclusionEvent for path invalidation + rendering
This commit is contained in:
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
initial_chunk_radius = 8
|
initial_chunk_radius = 5
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
vsync = "mailbox"
|
vsync = "mailbox"
|
||||||
|
|
||||||
[spawn_counts]
|
[spawn_counts]
|
||||||
dorfs = 5
|
dorfs = 50
|
||||||
pigs = 5
|
pigs = 5
|
||||||
rabbits = 5
|
rabbits = 5
|
||||||
@@ -1,19 +1,44 @@
|
|||||||
use crate::config::GameConfig;
|
use crate::config::GameConfig;
|
||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::constants::TILE_SIZE;
|
use crate::constants::TILE_SIZE;
|
||||||
use crate::constants::*;
|
use crate::constants::*;
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
use crate::game::SpawnDelay;
|
use crate::game::SpawnDelay;
|
||||||
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
||||||
|
use crate::world::tiles::{TileChangedEvent, TileMap};
|
||||||
use crate::world::VisibleGameEntity;
|
use crate::world::VisibleGameEntity;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rand::prelude::*;
|
use bevy_rand::prelude::*;
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
|
|
||||||
|
/// How often (in seconds) a rabbit digs.
|
||||||
|
/// 5 seconds at normal speed — slow enough to observe, fast enough to test.
|
||||||
|
pub const DIG_INTERVAL_SECS: f32 = 5.0;
|
||||||
|
|
||||||
|
/// Tracks time until next dig action. Debug component — rabbits dig to demonstrate
|
||||||
|
/// the TileChangedEvent + path invalidation pipeline. Remove or replace when
|
||||||
|
/// real digging mechanics are implemented.
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct RabbitDigTimer {
|
||||||
|
/// Seconds remaining until next dig. Reset to DIG_INTERVAL_SECS after each dig.
|
||||||
|
pub secs_remaining: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RabbitDigTimer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
secs_remaining: DIG_INTERVAL_SECS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
pub struct Rabbit {
|
pub struct Rabbit {
|
||||||
ambulatory: Ambulatory,
|
ambulatory: Ambulatory,
|
||||||
sprite: Sprite,
|
sprite: Sprite,
|
||||||
transform: Transform,
|
transform: Transform,
|
||||||
visibility: Visibility,
|
visibility: Visibility,
|
||||||
|
dig_timer: RabbitDigTimer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rabbit {
|
impl Rabbit {
|
||||||
@@ -36,6 +61,7 @@ impl Rabbit {
|
|||||||
},
|
},
|
||||||
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||||
visibility: Visibility::Hidden,
|
visibility: Visibility::Hidden,
|
||||||
|
dig_timer: RabbitDigTimer::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,3 +103,34 @@ pub fn spawn_rabbits(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Debug system: rabbits periodically dig the floor tile beneath them.
|
||||||
|
///
|
||||||
|
/// Removes the FloorTileData at the tile directly below the rabbit's current
|
||||||
|
/// z-level. Also clears the corresponding ChunkData bits via TileMap::remove_floor.
|
||||||
|
/// Fires TileChangedEvent so path invalidation reacts automatically.
|
||||||
|
pub fn rabbit_dig_system(
|
||||||
|
mut query: Query<(&Transform, &mut RabbitDigTimer)>,
|
||||||
|
mut tilemap: ResMut<TileMap>,
|
||||||
|
mut tile_changed: MessageWriter<TileChangedEvent>,
|
||||||
|
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (transform, mut dig_timer) in query.iter_mut() {
|
||||||
|
dig_timer.secs_remaining -= time.delta_secs();
|
||||||
|
if dig_timer.secs_remaining > 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dig_timer.secs_remaining = DIG_INTERVAL_SECS;
|
||||||
|
|
||||||
|
let entity_pos = transform.translation.as_ivec3();
|
||||||
|
let below_pos = IVec3::new(entity_pos.x, entity_pos.y, entity_pos.z - ITILE_SIZE);
|
||||||
|
|
||||||
|
if tilemap.remove_floor(&below_pos).is_some() {
|
||||||
|
tile_changed.write(TileChangedEvent { pos: below_pos });
|
||||||
|
occlusion.write(TileOcclusionEvent {
|
||||||
|
tile_position: below_pos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ use crate::constants::{
|
|||||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||||
};
|
};
|
||||||
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
||||||
|
use crate::world::tiles::tile_changed::{PathfindingDirtyChunks, TileChangedEvent};
|
||||||
use crate::world::tiles::TileMap;
|
use crate::world::tiles::TileMap;
|
||||||
use crate::world::{
|
use crate::world::{
|
||||||
chunks::CHUNK_SIZE,
|
chunks::CHUNK_SIZE,
|
||||||
@@ -217,10 +218,13 @@ impl Plugin for PathfindingPlugin {
|
|||||||
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||||||
.insert_resource(PathRequestQueue::default())
|
.insert_resource(PathRequestQueue::default())
|
||||||
.init_resource::<TileOccupancy>()
|
.init_resource::<TileOccupancy>()
|
||||||
|
.insert_resource(PathfindingDirtyChunks::default())
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(
|
(
|
||||||
rebuild_tile_occupancy,
|
rebuild_tile_occupancy,
|
||||||
|
collect_pathfinding_dirty_chunks,
|
||||||
|
invalidate_paths_on_tile_change,
|
||||||
prepare_paths,
|
prepare_paths,
|
||||||
update_wandering_targets,
|
update_wandering_targets,
|
||||||
movement,
|
movement,
|
||||||
@@ -233,12 +237,58 @@ impl Plugin for PathfindingPlugin {
|
|||||||
merge_benchmark_stats,
|
merge_benchmark_stats,
|
||||||
process_completed_paths,
|
process_completed_paths,
|
||||||
process_path_queue,
|
process_path_queue,
|
||||||
|
clear_pathfinding_dirty_chunks,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.add_systems(Update, bench_report_system);
|
.add_systems(Update, bench_report_system);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects chunk positions from TileChangedEvents into PathfindingDirtyChunks.
|
||||||
|
/// Runs once per frame, O(events). HashSet deduplicates — many tile changes
|
||||||
|
/// in the same chunk produce one entry.
|
||||||
|
pub fn collect_pathfinding_dirty_chunks(
|
||||||
|
mut events: MessageReader<TileChangedEvent>,
|
||||||
|
mut dirty: ResMut<PathfindingDirtyChunks>,
|
||||||
|
) {
|
||||||
|
for event in events.read() {
|
||||||
|
let chunk_pos = world_to_chunk(event.pos);
|
||||||
|
dirty.chunks.insert(chunk_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears paths for entities whose upcoming steps pass through a changed chunk.
|
||||||
|
/// Checks only the next 8 steps (not the full path) for performance.
|
||||||
|
/// O(entities × 8) regardless of how many tiles changed.
|
||||||
|
/// Only runs when dirty chunks exist, which is rare in normal gameplay.
|
||||||
|
pub fn invalidate_paths_on_tile_change(
|
||||||
|
dirty: Res<PathfindingDirtyChunks>,
|
||||||
|
mut query: Query<&mut Ambulatory>,
|
||||||
|
) {
|
||||||
|
if dirty.chunks.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for mut ambulatory in query.iter_mut() {
|
||||||
|
let Some(ref path) = ambulatory.current_path else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let check_end = (ambulatory.path_index + 8).min(path.len());
|
||||||
|
let affected = path[ambulatory.path_index..check_end]
|
||||||
|
.iter()
|
||||||
|
.any(|p| dirty.chunks.contains(&world_to_chunk(p.as_ivec3())));
|
||||||
|
if affected {
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
// Keep target — entity will recompute path to same destination
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears PathfindingDirtyChunks at the end of the frame.
|
||||||
|
/// Must run AFTER invalidate_paths_on_tile_change.
|
||||||
|
pub fn clear_pathfinding_dirty_chunks(mut dirty: ResMut<PathfindingDirtyChunks>) {
|
||||||
|
dirty.chunks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn prepare_paths(
|
pub fn prepare_paths(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut queue: ResMut<PathRequestQueue>,
|
mut queue: ResMut<PathRequestQueue>,
|
||||||
@@ -606,8 +656,29 @@ pub fn movement(
|
|||||||
let advance_path: bool;
|
let advance_path: bool;
|
||||||
|
|
||||||
let move_dir = next_point - transform.translation;
|
let move_dir = next_point - transform.translation;
|
||||||
if occupied && move_dir.length_squared() > 0.0 {
|
let our_dir = Vec2::new(move_dir.x, move_dir.y).normalize();
|
||||||
let forward_2d = Vec2::new(move_dir.x, move_dir.y).normalize();
|
let their_dir = if occupied {
|
||||||
|
occupancy.direction_at(next_point)
|
||||||
|
} else {
|
||||||
|
Vec2::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 1: Convoy — same direction, treat as unoccupied
|
||||||
|
let convoy =
|
||||||
|
occupied && their_dir != Vec2::ZERO && their_dir.dot(our_dir) > 0.7;
|
||||||
|
|
||||||
|
// Case 2: Head-on (directly opposite directions)
|
||||||
|
let head_on = their_dir != Vec2::ZERO && their_dir.dot(our_dir) < -0.7;
|
||||||
|
|
||||||
|
let we_are_es = our_dir.x > 0.3 || our_dir.y < -0.3;
|
||||||
|
|
||||||
|
if !occupied || convoy {
|
||||||
|
// Normal movement
|
||||||
|
actual_move = next_point;
|
||||||
|
advance_path = true;
|
||||||
|
} else if occupied && head_on {
|
||||||
|
// Both yield left — existing sidestep chain
|
||||||
|
let forward_2d = our_dir;
|
||||||
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
||||||
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
||||||
let cur_z = transform.translation.z;
|
let cur_z = transform.translation.z;
|
||||||
@@ -664,18 +735,98 @@ pub fn movement(
|
|||||||
advance_path = true;
|
advance_path = true;
|
||||||
let tile_weight =
|
let tile_weight =
|
||||||
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
let threshold = if ambulatory.walk_speed > 0. {
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
if ambulatory.step_recovery == 0 {
|
if ambulatory.step_recovery == 0 {
|
||||||
ambulatory.step_recovery = threshold;
|
ambulatory.step_recovery = excuse_threshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if occupied && we_are_es {
|
||||||
|
// We yield — sidestep chain first, then excuse-me
|
||||||
|
let forward_2d = our_dir;
|
||||||
|
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
||||||
|
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
||||||
|
let cur_z = transform.translation.z;
|
||||||
|
|
||||||
|
let candidates = [
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
|
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
|
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
right_2d.x * TILE_SIZE,
|
||||||
|
right_2d.y * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let sidestep = candidates.iter().copied().find(|&c| {
|
||||||
|
tilemap.is_standable(c.as_ivec3())
|
||||||
|
&& occupancy.count_at(c) <= current_count
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(step) = sidestep {
|
||||||
|
actual_move = step;
|
||||||
|
advance_path = false;
|
||||||
|
} else {
|
||||||
|
// excuse-me: push through with delay
|
||||||
|
actual_move = next_point;
|
||||||
|
advance_path = true;
|
||||||
|
let tile_weight =
|
||||||
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if ambulatory.step_recovery == 0 {
|
||||||
|
ambulatory.step_recovery = excuse_threshold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// occupied && we_are_wn — right of way, advance with excuse-me delay
|
||||||
actual_move = next_point;
|
actual_move = next_point;
|
||||||
advance_path = true;
|
advance_path = true;
|
||||||
|
let tile_weight =
|
||||||
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
|
let excuse_threshold = if ambulatory.walk_speed > 0. {
|
||||||
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if ambulatory.step_recovery == 0 {
|
||||||
|
ambulatory.step_recovery = excuse_threshold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction = (actual_move - transform.translation).normalize_or_zero();
|
let direction = (actual_move - transform.translation).normalize_or_zero();
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ fn main() {
|
|||||||
entities::sentient::dorf::spawn_dorfs,
|
entities::sentient::dorf::spawn_dorfs,
|
||||||
entities::livestock::pig::spawn_pigs,
|
entities::livestock::pig::spawn_pigs,
|
||||||
entities::livestock::rabbit::spawn_rabbits,
|
entities::livestock::rabbit::spawn_rabbits,
|
||||||
|
entities::livestock::rabbit::rabbit_dig_system,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.add_systems(Update, pig_drop_system)
|
.add_systems(Update, pig_drop_system)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ impl Plugin for WorldPlugin {
|
|||||||
.add_message::<ChunkFoliageEvent>()
|
.add_message::<ChunkFoliageEvent>()
|
||||||
.add_message::<ChunkFaunaEvent>()
|
.add_message::<ChunkFaunaEvent>()
|
||||||
.add_message::<TileOcclusionEvent>()
|
.add_message::<TileOcclusionEvent>()
|
||||||
|
.add_message::<tiles::TileChangedEvent>()
|
||||||
.add_systems(Startup, (setup_chunk_system, setup_initial_chunks).chain())
|
.add_systems(Startup, (setup_chunk_system, setup_initial_chunks).chain())
|
||||||
.add_systems(
|
.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Component, Clone)]
|
|
||||||
pub struct FloorTile {
|
|
||||||
pub id: u32,
|
|
||||||
pub opaque: bool,
|
|
||||||
pub walkable: bool,
|
|
||||||
pub astar_weight: u8,
|
|
||||||
pub visible_range: [u32; 8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FloorTile {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: 0,
|
|
||||||
opaque: true,
|
|
||||||
walkable: true,
|
|
||||||
astar_weight: 0,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component, Clone)]
|
|
||||||
pub struct FixtureTile {
|
|
||||||
pub id: u32,
|
|
||||||
pub solid: bool,
|
|
||||||
pub visible_range: [u32; 8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FixtureTile {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: 0,
|
|
||||||
solid: true,
|
|
||||||
visible_range: [0; 8],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct TileState {
|
|
||||||
pub timer: Timer,
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
pub mod benchmark;
|
pub mod benchmark;
|
||||||
pub mod chunk_data;
|
pub mod chunk_data;
|
||||||
pub mod rendering;
|
pub mod rendering;
|
||||||
|
pub mod tile_changed;
|
||||||
pub mod tilemap;
|
pub mod tilemap;
|
||||||
pub mod tilemap_chunk;
|
pub mod tilemap_chunk;
|
||||||
pub mod visibility;
|
pub mod visibility;
|
||||||
|
|
||||||
pub use benchmark::*;
|
pub use benchmark::*;
|
||||||
pub use chunk_data::*;
|
pub use chunk_data::*;
|
||||||
|
pub use tile_changed::*;
|
||||||
pub use tilemap::*;
|
pub use tilemap::*;
|
||||||
pub use tilemap_chunk::*;
|
pub use tilemap_chunk::*;
|
||||||
pub use visibility::*;
|
pub use visibility::*;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! Tile change events and pathfinding dirty chunk tracking.
|
||||||
|
//!
|
||||||
|
//! This module provides types for tracking tile modifications and
|
||||||
|
//! invalidating pathfinding data when terrain changes.
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_platform::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Message, Clone, Copy)]
|
||||||
|
pub struct TileChangedEvent {
|
||||||
|
pub pos: IVec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct PathfindingDirtyChunks {
|
||||||
|
pub chunks: HashSet<IVec2>,
|
||||||
|
}
|
||||||
@@ -198,6 +198,16 @@ impl TileMap {
|
|||||||
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
||||||
let (lx, ly, z) = ChunkData::world_to_local(pos);
|
let (lx, ly, z) = ChunkData::world_to_local(pos);
|
||||||
chunk.set_fixture_tile(lx, ly, z, tile.can_stand_in(), tile.can_stand_on());
|
chunk.set_fixture_tile(lx, ly, z, tile.can_stand_in(), tile.can_stand_on());
|
||||||
|
|
||||||
|
// If fixture blocks entry, also clear the floor's stand_in bit at this position.
|
||||||
|
// Air floor tiles exist at above-ground positions — without this, air's
|
||||||
|
// can_stand_in=true would override the fixture block via the OR check.
|
||||||
|
if !tile.can_stand_in() {
|
||||||
|
let idx = ChunkData::pos_to_index(lx, ly, z);
|
||||||
|
let word = idx / 32;
|
||||||
|
let mask = !(1u32 << (idx % 32));
|
||||||
|
chunk.stand_in_floor[word] &= mask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.fixture_tiles.insert(pos, tile);
|
self.fixture_tiles.insert(pos, tile);
|
||||||
}
|
}
|
||||||
@@ -260,6 +270,24 @@ impl TileMap {
|
|||||||
removed
|
removed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a floor tile, clearing the HashMap entry, ChunkData bitsets, and tile_ids.
|
||||||
|
/// All three must be cleared — leaving ChunkData stale causes is_standable bugs,
|
||||||
|
/// and leaving tile_ids non-zero causes the renderer to keep drawing the tile.
|
||||||
|
pub fn remove_floor(&mut self, pos: &IVec3) -> Option<FloorTileData> {
|
||||||
|
let removed = self.floor_tiles.remove(pos);
|
||||||
|
let chunk_pos = world_to_chunk(*pos);
|
||||||
|
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
||||||
|
let (lx, ly, z) = ChunkData::world_to_local(*pos);
|
||||||
|
let idx = ChunkData::pos_to_index(lx, ly, z);
|
||||||
|
let word = idx / 32;
|
||||||
|
let clear_mask = !(1u32 << (idx % 32));
|
||||||
|
chunk.stand_in_floor[word] &= clear_mask;
|
||||||
|
chunk.stand_on_floor[word] &= clear_mask;
|
||||||
|
chunk.tile_ids[idx] = 0;
|
||||||
|
}
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove all tile data for a specific chunk from the TileMap.
|
/// Remove all tile data for a specific chunk from the TileMap.
|
||||||
/// Iterates all positions in the chunk volume and removes from HashMaps.
|
/// Iterates all positions in the chunk volume and removes from HashMaps.
|
||||||
/// Used during chunk unloading to clean up tile data.
|
/// Used during chunk unloading to clean up tile data.
|
||||||
|
|||||||
Reference in New Issue
Block a user