From 4d702bc8f503bb0bdaf04e1bc764c9d8b594e68b Mon Sep 17 00:00:00 2001 From: popertots Date: Sat, 21 Mar 2026 12:09:49 +0000 Subject: [PATCH] fix: rabbit dig occlusion refresh, bounds guard panic, world floor clamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fire TileOcclusionEvent for 27-tile neighbourhood (3x3x3) so tiles above the dug tile recalculate visibility and stop rendering black - Clamp gravity in movement to world minimum z — rabbits digging the floor below them now stop at world floor instead of falling past it - Assert bounds in remove_floor/remove_fixture before pos_to_index to panic with z/coords info instead of silently overflowing - Hoist ITILE_SIZE, CHUNK_SIZE, Z_BELOW, Z_ABOVE to module-level imports in tilemap.rs --- src/entities/livestock/rabbit.rs | 16 ++++++-- src/entities/shared_systems/pathfinding.rs | 7 +++- src/world/tiles/tilemap.rs | 44 +++++++++++++++++++--- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/entities/livestock/rabbit.rs b/src/entities/livestock/rabbit.rs index 8de484c..e22b05d 100644 --- a/src/entities/livestock/rabbit.rs +++ b/src/entities/livestock/rabbit.rs @@ -128,9 +128,19 @@ pub fn rabbit_dig_system( if tilemap.remove_floor(&below_pos).is_some() { tile_changed.write(TileChangedEvent { pos: below_pos }); - occlusion.write(TileOcclusionEvent { - tile_position: below_pos, - }); + + // Refresh visibility for the dug tile and all 26 neighbours. + // Removing a tile changes what is visible from every adjacent position. + for dz in -1..=1i32 { + for dy in -1..=1i32 { + for dx in -1..=1i32 { + occlusion.write(TileOcclusionEvent { + tile_position: below_pos + + IVec3::new(dx * ITILE_SIZE, dy * ITILE_SIZE, dz * ITILE_SIZE), + }); + } + } + } } } } diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs index c11bb93..1c4b94c 100644 --- a/src/entities/shared_systems/pathfinding.rs +++ b/src/entities/shared_systems/pathfinding.rs @@ -609,7 +609,12 @@ pub fn movement( .for_each(|(mut ambulatory, mut transform)| { let current_pos = transform.translation; if !is_standable_tile(&tilemap, current_pos.as_ivec3()) { - transform.translation.z -= TILE_SIZE; + // Don't fall below world minimum z — rabbits digging can leave no floor + // to stand on, but they should stop at the world floor rather than + // falling into oblivion and panicking pos_to_index overflow. + if transform.translation.z > -(crate::world::chunks::Z_BELOW as f32) * TILE_SIZE { + transform.translation.z -= TILE_SIZE; + } ambulatory.current_path = None; ambulatory.target = None; return; diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs index b3c1e7a..52f4e52 100644 --- a/src/world/tiles/tilemap.rs +++ b/src/world/tiles/tilemap.rs @@ -32,7 +32,8 @@ use bevy::prelude::*; use rustc_hash::FxHashMap; use super::chunk_data::ChunkData; -use crate::world::chunks::world_to_chunk; +use crate::constants::ITILE_SIZE; +use crate::world::chunks::{world_to_chunk, CHUNK_SIZE, Z_ABOVE, Z_BELOW}; /// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple. #[derive(Clone, Copy, Debug)] @@ -255,12 +256,27 @@ impl TileMap { /// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets. /// BOTH must be cleared — leaving ChunkData stale causes is_standable bugs. pub fn remove_fixture(&mut self, pos: &IVec3) -> Option { - use super::chunk_data::ChunkData; - let removed = self.fixture_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); + // Guard bounds before pos_to_index — out-of-range z panics with overflow. + assert!( + z >= -(Z_BELOW as i32) && z <= Z_ABOVE as i32, + "remove_fixture: z={} out of bounds [{}, {}] at pos={:?}", + z, + -Z_BELOW, + Z_ABOVE, + pos + ); + assert!( + (0..CHUNK_SIZE).contains(&lx) && (0..CHUNK_SIZE).contains(&ly), + "remove_fixture: local coords ({}, {}) out of bounds [0, {}) at pos={:?}", + lx, + ly, + CHUNK_SIZE, + pos + ); let idx = ChunkData::pos_to_index(lx, ly, z); let word = idx / 32; let clear_mask = !(1u32 << (idx % 32)); @@ -278,6 +294,25 @@ impl TileMap { 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); + // Guard bounds before pos_to_index — out-of-range z panics with overflow. + // This fires when a rabbit digs the floor below the world minimum z, + // leaving no standable tile and causing the entity to fall past the floor. + assert!( + z >= -(Z_BELOW as i32) && z <= Z_ABOVE as i32, + "remove_floor: z={} out of bounds [{}, {}] at pos={:?}", + z, + -Z_BELOW, + Z_ABOVE, + pos + ); + assert!( + (0..CHUNK_SIZE).contains(&lx) && (0..CHUNK_SIZE).contains(&ly), + "remove_floor: local coords ({}, {}) out of bounds [0, {}) at pos={:?}", + lx, + ly, + CHUNK_SIZE, + pos + ); let idx = ChunkData::pos_to_index(lx, ly, z); let word = idx / 32; let clear_mask = !(1u32 << (idx % 32)); @@ -292,9 +327,6 @@ impl TileMap { /// Iterates all positions in the chunk volume and removes from HashMaps. /// Used during chunk unloading to clean up tile data. pub fn remove_chunk_data(&mut self, chunk_pos: IVec2) { - use crate::constants::ITILE_SIZE; - use crate::world::chunks::{CHUNK_SIZE, Z_ABOVE, Z_BELOW}; - for local_x in 0..CHUNK_SIZE { for local_y in 0..CHUNK_SIZE { for z in -Z_BELOW as i32..=Z_ABOVE as i32 {