Switch from add-then-XOR pattern to proper sequential multiply-add
avalanche. Each coordinate feeds into the next multiply, preventing
correlation between adjacent tiles when rare drops are introduced.
- SmallVec<[DropEntry; 2]> replaces Vec in DropTable: zero heap allocation for
all current tiles (0 or 1 entries), spills to heap only at 3+
- DropEntry fields packed to u8 (chance_pct, min_count, max_count): ~12 bytes ->
4 bytes; derives Copy so no extra clones
- Replaced SystemTime pseudo-RNG with dig_rng(pos): PCG-style hash of world SEED
+ tile position. Deterministic per world, same dig = same roll every time
- TOML-driven drop tables: assets/drop_tables.toml (grass=5%/1-2, rock=10%/1,
dirt/air=none). TOML parsed at startup into DropTableRegistry resource
- OnceLock global map for async terrain generation tasks to access drop tables
without Bevy resource borrowing
- terrain.rs: DropTableRegistry::global_get("tile") replaces hardcoded drop_table_for
- Add generic digging + drop tables to implemented list
- Rabbit digging: now using Digger component, not debug code
- Building and construction: updated to reflect generic system in place
- Architecture: added digging to shared_systems, drop_table to tiles
- 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.
- 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.
Clearing stand_in_floor when removing a floor tile made the dug position
impassable. An entity falling into the dug slot found (false || false) && true
= false and kept falling. Now only stand_on_floor is cleared — the space
reverts to air (passable), and only the tile above loses its platform.
- calculate_visibility: None from floor_tiles.get() now checks tilemap.chunks
to confirm the neighbour's chunk is loaded before treating it as air.
Without this, unloaded world borders and pending chunks were incorrectly
seen as open space, causing underground tiles to render as black.
- rabbit_dig_system: skip and reset timer when entity is not on standable
ground, preventing rapid-dig cascade as rabbit falls through multiple levels.
- calculate_visibility: None from floor_tiles.get() now breaks touches_air=true.
Absent tile = open space = air for visibility purposes. Previously, removed
tiles left their below-neighbours with all-zero visible_range (black).
- rabbit_dig_system: fire TileOcclusionEvent for full Z_BELOW+1 level column
below the dug tile + 8 XY neighbours each, not just 3x3x3. calculate_visibility
traces up to Z_TOTAL+Z_BELOW+1 tiles above each tile, so removing one tile
can expose visibility arbitrarily deep below.
- Entities spawn at z=35*TILE=560 but Z_ABOVE max is 15*TILE=240. Snap to
Z_ABOVE*TILE instead of falling one tile per tick through unloaded space.
- Skip rabbit dig when below_pos would be outside ChunkData z-bounds,
preventing remove_floor assert panic.
- 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
Fix 1 — insert_fixture now updates ChunkData bitsets:
Log trunks (can_stand_in=false) now block movement in is_standable.
Leaf canopy (can_stand_in=true) remains walkable.
Fix 2 — Diagonal-aware collision avoidance:
Sidestep priority: left+forward, left, right+forward, right.
If all blocked: push through (excuse me), advance path, apply one-step
delay (speed-modulated recovery). Does not stack delays.
Fix 3 — Movement direction tracking:
Ambulatory now has move_direction (Vec2) and step_history ([i16; 4]).
TileOccupancy tracks per-tile direction hints for Stage 2 collision
(convoy skip, E/S yield) via direction_at().
move_direction is smoothed from: 2 historical steps, current confirmed
step, and 2-step path lookahead.
Ignore: add *.patch and *.diff to .gitignore
Two entities on the same tile (16,-0.0) each saw count=2 and blocked each
other with occupied=true since 2>1. The collision check was treating the
entity's own presence as a blocker.
Now: only trigger avoidance when next_tile has strictly more entities than
current_tile. Handles path[0]=current_pos (count equal → move), two entities
passing through same tile (counts stay equal → move), and genuinely crowded
destinations (next has more → dodge). Also raise crowded threshold to 4+.
collision_delay was causing entities to skip step_recovery ticks, freezing
movement whenever the next tile was occupied. Combined with the ordering fix
(collision_delay placed before step_recovery), entities were stuck at
path_index=0 indefinitely in spawn clusters.
Remove collision_delay entirely — step_recovery already provides the
per-tick pacing, and the natural next-tick retry (via left-step or
normal movement) handles occupied tiles without an artificial delay.
Actual thresholds (grass weight=50): dorfs move after 6 ticks, pigs after
26 ticks, rabbits after 2 ticks. Without collision_delay interference,
movement is now governed purely by step_recovery.
collision_delay was placed before the walk_speed/step_recovery block,
causing step_recovery to never tick when collision_delay was active —
entities stayed permanently pre-empted at path_index=0.
Correct order: step_recovery (must tick every frame) before
collision_delay (only checked when entity is ready to move).
- Add TileOccupancy resource and rebuild_tile_occupancy system to track
per-tile entity counts for soft collision avoidance
- Add collision_delay to Ambulatory; entities try relative-left step on
occupied tiles, wait 1 tick, then push through
- Fix leaf canopy standability: leaves are now can_stand_in=true,
can_stand_on=false (walkable, not standable-on)
- Delete FloorTilePrefab / FixtureTilePrefab / FloorTile / FixtureTile /
TileState (all fully dead — TileMap + ChunkData are sole truth)
- Delete tile_spawns from TerrainBlob (populated but never consumed)
- Delete leaf ghost entity spawn in forestry (orphaned invisible ECS entity)
- Replace log prefab spawn with inline commands.spawn(Transform, Visibility)
- Add TileMap::remove_fixture for future digging/explosion use
- Skip collision avoidance when current tile has >2 entities (handles spawn
cluster deadlock)
Replace hard-coded chunk-centre waypoints with directional edge waypoints.
When a path segment crosses into the next chunk, directional_chunk_waypoint()
samples standable tiles along the entry edge and picks the one closest to the
straight-line projection from the entity's current position toward the goal.
Falls back to chunk centre if no standable edge tile is found.
This preserves tile-locked DF movement feel while removing the forced dogleg
at every chunk boundary that the chunk-centre approach introduced.
Prints DORF SYSTEM box to stdout before game loop begins:
OS, CPU (model + cores), GPU (WEBGPU_ADAPTER_NAME env var),
RAM total + free (from /proc/meminfo), display vsync mode,
initial chunk radius, world seed.
512 chunks/frame in debug, 1024 in release. In release, recolor runs
~2x faster so 1024 still costs ~1ms but clears the dirty queue twice
as fast, closing the p99 gap during rapid z-scrolling.
Negative world z (below ground) was excluded by the clamp, making the
visible window asymmetrically large. Dirty count now proportional to
actual scroll distance. Frame time dropped from ~13ms to ~8.3ms avg.
Boundary: mark all z-levels where z_diff changed, not just 4 boundary
indices. For multi-step scrolls, every z-level between
min(old,new)-8 and max(old,new) inclusive needs recoloring.
Also fixes last_z_change_dirty_count to use actual new_keys.len().
Recolor: update tileset_index alongside color so sky tile switch at
t>=0.98 actually renders the sky texture, not sky-coloured grass.
Cleanup: removed solid_bits (was unused after recolor fix).
Three optimizations for z-scroll performance:
Option 1 — Fast in-place recolor (no HashMap lookups):
- TilemapChunkStates stores solid_bits + underground_bits per chunk
- recolor_chunk_for_depth iterates flat Vec<Option<TileData>> — pure cache-friendly sequential reads
- tile_fade_color computes color from tileset_idx + z_diff without any HashMap access
- vs repopulate_chunk_tiles: 32K Vec reads vs 32K HashMap lookups per frame
Option 2 — Targeted boundary dirty (from 9×chunks to 2×chunks):
- on_camera_z_changed now only marks the 4 boundary z-levels:
old_entered, old_exited, new_entered, new_exited
- Registry.z_change_keys tracks z-changed keys separately from occlusion dirty_keys
- populate_tilemap_chunk_data uses fast recolor for z_change_keys,
full repopulate for dirty_keys (occlusion — rare)
Option 3 — Separate budgets:
- z_change_keys processed with MAX_POPULATE_PER_FRAME budget (fast recolor)
- dirty_keys processed with MAX_POPULATE_PER_FRAME budget (full repopulate)
- Both pipelines tracked separately in benchmark
Also:
- populate_chunk_tiles now returns underground_bits for state storage
- compute_solid_bits helper extracts solid tiles from TileData Vec
- spawn_tilemap_chunks computes and stores both bitmasks on spawn
- despawn_tilemap_chunks clears all state for despawned chunks
on_camera_z_changed: only dirty z-levels whose z_diff changed
(both old and new camera positions, within 0..=8 window).
Previously dirtied all 20k entities on every z-change.
populate_tilemap_chunk_data: process up to MAX_POPULATE_PER_FRAME
(512) keys per frame, sorted by z-distance from current camera.
Spreads z-scroll cost across multiple frames instead of one spike.
TilemapBenchmark: added z-change spike instrumentation
(last_z_change_dirty_ms, last_z_change_dirty_count,
last_z_change_populate_ms, z_change_history_ms).
F9 report now shows z-scroll metrics.
PreviousZIndex resource tracks prior camera z for targeted dirty calc.