- TileMap::insert_floor now syncs ChunkData bits (set_floor_tile) so is_standable returns correct values for tiles inserted via insert_floor. Previously the HashMap was updated but ChunkData remained stale, causing is_standable to return false for valid tiles. - rabbit_dig_system inserts air tile at the dug position after remove_floor. Without this, the HashMap entry is absent and ChunkData bits stay 0 (both false), making is_standable return false at that position. The entity then falls through multiple levels. Air's can_stand_in=true means the space is passable — the entity falls through it and lands on solid ground below.
313 lines
11 KiB
Rust
313 lines
11 KiB
Rust
use crate::config::{GameConfig, TileRegistry};
|
|
use crate::constants::ITILE_SIZE;
|
|
use crate::constants::TILE_SIZE;
|
|
use crate::constants::*;
|
|
use crate::entities::shared_components::Ambulatory;
|
|
use crate::game::SpawnDelay;
|
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
|
use crate::world::tiles::{FloorTileData, TileChangedEvent, TileMap};
|
|
use crate::world::VisibleGameEntity;
|
|
use bevy::prelude::*;
|
|
use bevy_rand::prelude::*;
|
|
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;
|
|
|
|
/// Temporary debug component. Attached to a rabbit for N ticks after it digs.
|
|
/// Removed automatically when tick_count reaches 0.
|
|
#[derive(Component)]
|
|
pub struct RabbitFallDebug {
|
|
pub ticks_remaining: u8,
|
|
}
|
|
|
|
/// 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)]
|
|
pub struct Rabbit {
|
|
ambulatory: Ambulatory,
|
|
sprite: Sprite,
|
|
transform: Transform,
|
|
visibility: Visibility,
|
|
dig_timer: RabbitDigTimer,
|
|
}
|
|
|
|
impl Rabbit {
|
|
pub fn new(asset_server: &Res<AssetServer>, position: Vec3) -> Self {
|
|
Rabbit {
|
|
ambulatory: Ambulatory {
|
|
walk_speed: 1.,
|
|
run_speed: 6.,
|
|
target: None,
|
|
current_path: None,
|
|
path_index: 0,
|
|
step_recovery: 0,
|
|
validation_cooldown: 0,
|
|
move_direction: Vec2::ZERO,
|
|
step_history: [0i16; 4],
|
|
},
|
|
sprite: Sprite {
|
|
image: asset_server.load("rabbit.png"),
|
|
..Default::default()
|
|
},
|
|
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
|
|
visibility: Visibility::Hidden,
|
|
dig_timer: RabbitDigTimer::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn spawn_rabbits(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
|
config: Res<GameConfig>,
|
|
mut delay: ResMut<SpawnDelay>,
|
|
mut has_spawned: Local<bool>,
|
|
) {
|
|
if *has_spawned {
|
|
return;
|
|
}
|
|
|
|
if delay.0 < 60 {
|
|
delay.0 += 1;
|
|
return;
|
|
}
|
|
|
|
*has_spawned = true;
|
|
|
|
if let Ok(mut rng) = rng_q.single_mut() {
|
|
for _ in 0..config.spawn_counts.rabbits {
|
|
let raw_x = rng.random_range(-32.0f32..32.0f32);
|
|
let raw_y = rng.random_range(-32.0f32..32.0f32);
|
|
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
|
|
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
|
|
let grid_z = 35.0 * TILE_SIZE;
|
|
|
|
let rab = commands
|
|
.spawn(Rabbit::new(
|
|
&asset_server,
|
|
Vec3::new(grid_x, grid_y, grid_z),
|
|
))
|
|
.id();
|
|
commands.entity(rab).insert(VisibleGameEntity);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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 commands: Commands,
|
|
mut query: Query<(Entity, &Transform, &mut RabbitDigTimer)>,
|
|
mut tilemap: ResMut<TileMap>,
|
|
mut tile_changed: MessageWriter<TileChangedEvent>,
|
|
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
|
time: Res<Time>,
|
|
) {
|
|
for (entity, transform, mut dig_timer) in query.iter_mut() {
|
|
if !tilemap.is_standable(transform.translation.as_ivec3()) {
|
|
dig_timer.secs_remaining = DIG_INTERVAL_SECS;
|
|
continue;
|
|
}
|
|
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);
|
|
|
|
// Don't dig below world floor — would panic in remove_floor bounds check.
|
|
if below_pos.z < -(crate::world::chunks::Z_BELOW as i32 - 1) * ITILE_SIZE {
|
|
continue;
|
|
}
|
|
|
|
// --- DEBUG: pre-dig state ---
|
|
let (lx, ly, lz) =
|
|
crate::world::tiles::ChunkData::world_to_local(transform.translation.as_ivec3());
|
|
let chunk_pos = crate::world::chunks::world_to_chunk(transform.translation.as_ivec3());
|
|
let standable_now = tilemap.is_standable(transform.translation.as_ivec3());
|
|
println!(
|
|
"[DIG PRE] entity={:?} world_pos=({:.1},{:.1},{:.1}) ivec={:?} \
|
|
tile_local=({},{},{}) chunk={:?} standable={}",
|
|
entity,
|
|
transform.translation.x,
|
|
transform.translation.y,
|
|
transform.translation.z,
|
|
entity_pos,
|
|
lx,
|
|
ly,
|
|
lz,
|
|
chunk_pos,
|
|
standable_now
|
|
);
|
|
println!(
|
|
"[DIG PRE] below_pos={:?} below_tile_z={}",
|
|
below_pos,
|
|
below_pos.z / ITILE_SIZE
|
|
);
|
|
|
|
let removed = tilemap.remove_floor(&below_pos);
|
|
|
|
// Replace the dug tile with air. The slot must become passable — if the entry
|
|
// is simply absent, ChunkData bits remain 0 (impassable), and is_standable
|
|
// returns false. Air has can_stand_in=true so entities can fall through it.
|
|
if removed.is_some() {
|
|
let air = TileRegistry::global().floor("air");
|
|
tilemap.insert_floor(
|
|
below_pos,
|
|
FloorTileData::new(
|
|
air.id,
|
|
air.can_stand_in,
|
|
air.can_stand_on,
|
|
air.transparent,
|
|
air.astar_weight,
|
|
[0; 8],
|
|
),
|
|
);
|
|
}
|
|
println!(
|
|
"[DIG] remove_floor({:?}) => {}",
|
|
below_pos,
|
|
if removed.is_some() {
|
|
"REMOVED"
|
|
} else {
|
|
"NOT FOUND"
|
|
}
|
|
);
|
|
|
|
// --- DEBUG: post-dig standability ---
|
|
let standable_after = tilemap.is_standable(transform.translation.as_ivec3());
|
|
println!(
|
|
"[DIG POST] standable at entity pos after dig: {}",
|
|
standable_after
|
|
);
|
|
|
|
if removed.is_some() {
|
|
tile_changed.write(TileChangedEvent { pos: below_pos });
|
|
|
|
// Refresh the full column below the dig and its XY neighbours.
|
|
// calculate_visibility traces up to Z_TOTAL+Z_BELOW+1 tiles above each tile,
|
|
// so removing one tile can affect visibility arbitrarily deep below.
|
|
for dz in 0..=crate::world::chunks::Z_BELOW as i32 {
|
|
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),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Attach fall debug component — tracks for 12 ticks
|
|
commands.entity(entity).insert(RabbitFallDebug {
|
|
ticks_remaining: 12,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Temporary debug system. Prints rabbit position and standability for N ticks
|
|
/// after a dig event. Removes the RabbitFallDebug component when done.
|
|
pub fn rabbit_fall_debug_system(
|
|
mut commands: Commands,
|
|
mut query: Query<(Entity, &Transform, &mut RabbitFallDebug)>,
|
|
tilemap: Res<TileMap>,
|
|
) {
|
|
use crate::world::chunks::world_to_chunk;
|
|
use crate::world::tiles::ChunkData;
|
|
|
|
for (entity, transform, mut debug) in query.iter_mut() {
|
|
let world_pos = transform.translation;
|
|
let ivec = world_pos.as_ivec3();
|
|
let (lx, ly, lz) = ChunkData::world_to_local(ivec);
|
|
let lz_euclid = ivec.z.div_euclid(ITILE_SIZE); // compare truncation vs floor
|
|
let chunk_pos = world_to_chunk(ivec);
|
|
let standable = tilemap.is_standable(ivec);
|
|
|
|
// Read ChunkData bits directly if chunk is loaded
|
|
let (stand_in, stand_on_below) = if let Some(chunk) = tilemap.chunks.get(&chunk_pos) {
|
|
let in_f = if lz >= -(crate::world::chunks::Z_BELOW as i32)
|
|
&& lz <= crate::world::chunks::Z_ABOVE as i32
|
|
&& lx >= 0
|
|
&& lx < crate::world::chunks::CHUNK_SIZE
|
|
&& ly >= 0
|
|
&& ly < crate::world::chunks::CHUNK_SIZE
|
|
{
|
|
let idx = ChunkData::pos_to_index(lx, ly, lz);
|
|
let word = idx / 32;
|
|
let mask = 1u32 << (idx % 32);
|
|
let in_floor = (chunk.stand_in_floor[word] & mask) != 0;
|
|
let in_fix = (chunk.stand_in_fixture[word] & mask) != 0;
|
|
format!("in_floor={} in_fixture={}", in_floor, in_fix)
|
|
} else {
|
|
format!("OUT_OF_BOUNDS(lz={})", lz)
|
|
};
|
|
|
|
let on_f = if lz - 1 >= -(crate::world::chunks::Z_BELOW as i32)
|
|
&& lz - 1 <= crate::world::chunks::Z_ABOVE as i32
|
|
&& lx >= 0
|
|
&& lx < crate::world::chunks::CHUNK_SIZE
|
|
&& ly >= 0
|
|
&& ly < crate::world::chunks::CHUNK_SIZE
|
|
{
|
|
let idx = ChunkData::pos_to_index(lx, ly, lz - 1);
|
|
let word = idx / 32;
|
|
let mask = 1u32 << (idx % 32);
|
|
let on_floor = (chunk.stand_on_floor[word] & mask) != 0;
|
|
let on_fix = (chunk.stand_on_fixture[word] & mask) != 0;
|
|
format!("on_floor[z-1]={} on_fixture[z-1]={}", on_floor, on_fix)
|
|
} else {
|
|
"BELOW_BOUNDS".to_string()
|
|
};
|
|
|
|
(in_f, on_f)
|
|
} else {
|
|
(
|
|
"chunk_not_loaded".to_string(),
|
|
"chunk_not_loaded".to_string(),
|
|
)
|
|
};
|
|
|
|
println!(
|
|
"[FALL {:2}] entity={:?} world_z={:.1} ivec_z={} lz_trunc={} lz_euclid={} \
|
|
standable={} | {} | {}",
|
|
debug.ticks_remaining,
|
|
entity,
|
|
world_pos.z,
|
|
ivec.z,
|
|
lz,
|
|
lz_euclid,
|
|
standable,
|
|
stand_in,
|
|
stand_on_below
|
|
);
|
|
|
|
debug.ticks_remaining -= 1;
|
|
if debug.ticks_remaining == 0 {
|
|
commands.entity(entity).remove::<RabbitFallDebug>();
|
|
}
|
|
}
|
|
}
|