refactor: extract generic digging system, drop tables, remove debug code

- New src/entities/item/drop_table.rs: DropEntry (chance, min/max count,
  pseudo-RNG roll) + DropTable wrapper. grass=5% coin 1-2, rock=10% coin 1,
  dirt/air=none.
- New src/entities/shared_systems/digging.rs: Digger component + dig_system.
  Any entity with Digger digs the tile below on its interval. Replaces
  rabbit_dig_system/rabbit_fall_debug_system/RabbitDigTimer/RabbitFallDebug.
- New TileMap::dig_floor: remove_floor + insert air. Used by dig_system.
- FloorTileData: added drop_table field. Lost Copy derive (Vec field).
  Updated all 6 terrain.rs call sites with per-tile drop tables.
- Pig: removed PigDropTimer + pig_drop_system. Drops were debug placeholder.
  TODO added for future loot-on-death/butcher system.
- Rabbit: removed all debug components/systems. Now uses Digger::new(5.0).
- main.rs: removed rabbit_dig_system/rabbit_fall_debug_system/pig_drop_system,
  added shared_systems::digging::dig_system.
This commit is contained in:
2026-03-21 15:40:34 +00:00
parent 1a170ea9a9
commit ce3746366c
9 changed files with 238 additions and 301 deletions
+73
View File
@@ -0,0 +1,73 @@
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
use bevy::prelude::*;
#[derive(Clone, Debug)]
pub struct DropEntry {
pub prefab: MiscPrefab,
pub chance: f32,
pub min_count: u32,
pub max_count: u32,
}
impl DropEntry {
pub fn always(prefab: MiscPrefab) -> Self {
Self {
prefab,
chance: 1.0,
min_count: 1,
max_count: 1,
}
}
pub fn chance(prefab: MiscPrefab, chance: f32, min_count: u32, max_count: u32) -> Self {
Self {
prefab,
chance,
min_count,
max_count,
}
}
pub fn roll(&self, pos: IVec3) -> u32 {
if self.chance < 1.0 {
let r = pseudo_rand_f32(pos);
if r >= self.chance {
return 0;
}
}
pseudo_rand_u32(pos) % (self.max_count - self.min_count + 1) + self.min_count
}
}
#[derive(Clone, Debug, Default)]
pub struct DropTable(pub Vec<DropEntry>);
impl DropTable {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
fn hash3(pos: IVec3) -> u32 {
let mut h: u32 = 0;
h = h.wrapping_mul(374761393).wrapping_add(pos.x as u32);
h = h.wrapping_mul(374761393).wrapping_add(pos.y as u32);
h = h.wrapping_mul(374761393).wrapping_add(pos.z as u32);
h ^= h >> 13;
h = h.wrapping_mul(1274126177);
h ^= h >> 16;
h
}
fn pseudo_rand_u32(pos: IVec3) -> u32 {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u32;
hash3(pos) ^ timestamp
}
fn pseudo_rand_f32(pos: IVec3) -> f32 {
let val = pseudo_rand_u32(pos);
(val as f32) / (u32::MAX as f32)
}
+2
View File
@@ -1,6 +1,7 @@
pub mod container;
pub mod core;
pub mod decorations;
pub mod drop_table;
pub mod material;
pub mod perishable;
pub mod prefabs;
@@ -11,6 +12,7 @@ pub mod types;
pub use container::*;
pub use core::*;
pub use decorations::*;
pub use drop_table::*;
pub use material::*;
pub use perishable::*;
pub use prefabs::*;
-44
View File
@@ -1,10 +1,8 @@
use crate::config::GameConfig;
use crate::constants::TILE_SIZE;
use crate::constants::*;
use crate::entities::item::{spawn_prefab, MiscPrefab};
use crate::entities::shared_components::Ambulatory;
use crate::game::SpawnDelay;
use crate::world::tiles::tilemap;
use crate::world::VisibleGameEntity;
use bevy::prelude::*;
use bevy_rand::prelude::*;
@@ -16,7 +14,6 @@ pub struct Pig {
sprite: Sprite,
transform: Transform,
visibility: Visibility,
drop_timer: PigDropTimer,
}
impl Pig {
@@ -39,14 +36,10 @@ impl Pig {
},
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
visibility: Visibility::Hidden,
drop_timer: PigDropTimer(Timer::from_seconds(5.0, TimerMode::Repeating)),
}
}
}
#[derive(Component, Deref, DerefMut)]
pub struct PigDropTimer(pub Timer);
pub fn spawn_pigs(
mut commands: Commands,
asset_server: Res<AssetServer>,
@@ -81,40 +74,3 @@ pub fn spawn_pigs(
}
}
}
pub fn pig_drop_system(
mut commands: Commands,
asset_server: Res<AssetServer>,
time: Res<Time>,
mut pigs: Query<(&mut PigDropTimer, &Transform)>,
mut tilemap: ResMut<tilemap::TileMap>,
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
) {
for (mut timer, transform) in &mut pigs {
timer.tick(time.delta());
if timer.just_finished() {
if let Ok(mut rng) = rng_q.single_mut() {
if rng.random_range(0..10) <= 5 {
// Example: pig drops raw meat
spawn_prefab(
&mut commands,
&asset_server,
MiscPrefab::RawMeat,
transform.translation,
&mut tilemap,
);
} else {
//example: pig drops a coin
spawn_prefab(
&mut commands,
&asset_server,
MiscPrefab::Coin,
transform.translation,
&mut tilemap,
);
}
}
}
}
}
+4 -235
View File
@@ -1,51 +1,20 @@
use crate::config::{GameConfig, TileRegistry};
use crate::constants::ITILE_SIZE;
use crate::constants::TILE_SIZE;
use crate::config::GameConfig;
use crate::constants::*;
use crate::entities::shared_components::Ambulatory;
use crate::entities::shared_systems::digging::Digger;
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,
digger: Digger,
}
impl Rabbit {
@@ -68,7 +37,7 @@ impl Rabbit {
},
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
visibility: Visibility::Hidden,
dig_timer: RabbitDigTimer::default(),
digger: Digger::new(5.0),
}
}
}
@@ -110,203 +79,3 @@ 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 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>();
}
}
}
+93
View File
@@ -0,0 +1,93 @@
use crate::constants::ITILE_SIZE;
use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
use crate::world::chunks::{Z_ABOVE, Z_BELOW};
use crate::world::tiles::tile_changed::TileChangedEvent;
use crate::world::tiles::visibility::TileOcclusionEvent;
use crate::world::tiles::TileMap;
use bevy::prelude::*;
use rand::RngExt;
#[derive(Component)]
pub struct Digger {
pub secs_remaining: f32,
pub interval_secs: f32,
}
impl Digger {
pub fn new(interval_secs: f32) -> Self {
Self {
secs_remaining: interval_secs,
interval_secs,
}
}
}
pub fn dig_system(
mut commands: Commands,
asset_server: Res<AssetServer>,
time: Res<Time>,
mut query: Query<(&Transform, &mut Digger)>,
mut tilemap: ResMut<TileMap>,
mut tile_changed: MessageWriter<TileChangedEvent>,
mut occlusion: MessageWriter<TileOcclusionEvent>,
) {
for (transform, mut digger) in &mut query {
let pos = transform.translation.as_ivec3();
if !tilemap.is_standable(pos) {
digger.secs_remaining = digger.interval_secs;
continue;
}
digger.secs_remaining -= time.delta_secs();
if digger.secs_remaining > 0.0 {
continue;
}
digger.secs_remaining = digger.interval_secs;
let below_pos = pos - IVec3::new(0, 0, ITILE_SIZE);
if below_pos.z < -(Z_BELOW as i32 - 1) * ITILE_SIZE {
continue;
}
let drop_table = tilemap
.floor_tiles
.get(&below_pos)
.map(|t| t.drop_table.clone());
if let Some(_removed) = tilemap.dig_floor(&below_pos) {
if let Some(table) = drop_table {
for entry in table.0 {
let count = entry.roll(below_pos);
for _ in 0..count {
spawn_prefab(
&mut commands,
&asset_server,
entry.prefab.clone(),
below_pos.as_vec3(),
&mut tilemap,
);
}
}
}
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 tiles above each tile,
// so removing one tile can affect visibility arbitrarily deep below.
for dz in 0..=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),
});
}
}
}
}
}
}
+1
View File
@@ -1,2 +1,3 @@
pub mod digging;
pub mod occupancy;
pub mod pathfinding;
+3 -7
View File
@@ -3,7 +3,7 @@ use bevy_rand::prelude::*;
use crate::entities::{
item::{initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer},
livestock::pig::pig_drop_system,
shared_systems::digging::dig_system,
};
use crate::game::SpawnDelay;
@@ -45,10 +45,6 @@ fn main() {
.insert_resource(ClearColor(Color::srgb(0., 0., 0.)))
.add_plugins(world::WorldPlugin)
.add_plugins(entities::pathfinding::PathfindingPlugin)
.add_systems(
FixedUpdate,
entities::livestock::rabbit::rabbit_fall_debug_system,
)
.add_systems(
Startup,
(camera::spawn_panning_camera, cursor::setup_cursor),
@@ -63,10 +59,10 @@ fn main() {
entities::sentient::dorf::spawn_dorfs,
entities::livestock::pig::spawn_pigs,
entities::livestock::rabbit::spawn_rabbits,
entities::livestock::rabbit::rabbit_dig_system,
dig_system,
),
)
.add_systems(Update, pig_drop_system)
.insert_resource(ItemRotationTimer::default())
.add_systems(Update, initialize_item_rotation_state)
.add_systems(
+22
View File
@@ -4,6 +4,9 @@ use bevy_platform::time::Instant;
use noise::{NoiseFn, Perlin};
use std::sync::{Arc, Mutex};
use crate::entities::item::drop_table::{DropEntry, DropTable};
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
use crate::{
config::TileRegistry,
constants::{SEED, TILE_SIZE},
@@ -14,6 +17,19 @@ use crate::{
},
};
fn drop_table_for(tile_name: &str) -> DropTable {
match tile_name {
"grass" => DropTable(vec![
DropEntry::chance(MiscPrefab::Coin, 0.05, 1, 2),
]),
"dirt" => DropTable::default(),
"rock" => DropTable(vec![
DropEntry::chance(MiscPrefab::Coin, 0.1, 1, 1),
]),
_ => DropTable::default(),
}
}
/// Thread-safe storage for completed terrain blobs.
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
@@ -114,6 +130,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("air"),
),
));
chunk_data.set_floor_tile(
@@ -136,6 +153,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("rock"),
),
));
chunk_data.set_floor_tile(
@@ -158,6 +176,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("dirt"),
),
));
chunk_data.set_floor_tile(
@@ -182,6 +201,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("grass"),
),
));
chunk_data.set_floor_tile(
@@ -205,6 +225,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("dirt"),
),
));
chunk_data.set_floor_tile(
@@ -228,6 +249,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent,
tile.astar_weight,
[0; 8],
drop_table_for("air"),
),
));
chunk_data.set_floor_tile(
+26 -1
View File
@@ -33,16 +33,18 @@ use rustc_hash::FxHashMap;
use super::chunk_data::ChunkData;
use crate::constants::ITILE_SIZE;
use crate::entities::item::drop_table::DropTable;
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)]
#[derive(Clone, Debug)]
pub struct FloorTileData {
pub id: u8,
/// bit0=can_stand_in, bit1=can_stand_on, bit2=visibly_transparent
pub flags: u8,
pub astar_weight: u8,
pub visible_range: [u32; 8],
pub drop_table: DropTable,
}
impl Default for FloorTileData {
@@ -52,6 +54,7 @@ impl Default for FloorTileData {
flags: 0b001,
astar_weight: 0,
visible_range: [0; 8],
drop_table: DropTable::default(),
}
}
}
@@ -64,6 +67,7 @@ impl FloorTileData {
visibly_transparent: bool,
astar_weight: u8,
visible_range: [u32; 8],
drop_table: DropTable,
) -> Self {
let mut flags = 0u8;
if can_stand_in {
@@ -80,6 +84,7 @@ impl FloorTileData {
flags,
astar_weight,
visible_range,
drop_table,
}
}
@@ -214,6 +219,26 @@ impl TileMap {
self.floor_tiles.insert(pos, tile);
}
pub fn dig_floor(&mut self, pos: &IVec3) -> Option<FloorTileData> {
let removed = self.remove_floor(pos);
if removed.is_some() {
let air = crate::config::TileRegistry::global().floor("air");
self.insert_floor(
*pos,
FloorTileData::new(
air.id,
air.can_stand_in,
air.can_stand_on,
air.transparent,
air.astar_weight,
[0; 8],
DropTable::default(),
),
);
}
removed
}
#[inline]
pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) {
let chunk_pos = world_to_chunk(pos);