- 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
7.2 KiB
Dorf
A Dwarf Fortress-inspired simulation game built in Rust with Bevy. The focus is on a fully data-oriented chunk engine: procedural terrain, hierarchical pathfinding, GPU-baked sprite rendering, and dynamic chunk loading/unloading — all designed to scale to a full DF-style world with z-levels, entities, and simulation.
This is an engine foundation, not a complete game. Many features (crafting, needs, combat, world history) exist only as stubs or are entirely absent. The chunk rendering and pathfinding systems are production-quality; the simulation layer above them is early.
Architecture
src/
├── camera.rs # Panning camera, z-level control (Q/E, scroll)
├── config.rs # GameConfig, TileRegistry resources
├── constants.rs # Tile size, pathfinding constants
├── entities/ # Dorfs, pigs, rabbits — ambulatory entities
│ ├── sentient/
│ ├── livestock/
│ └── shared_*/ # Ambulatory, pathfinding, occupancy, digging systems
├── game.rs # ZIndex resource, frame timing
├── main.rs # Bevy app bootstrap
└── world/
├── chunks/ # ChunkMap, chunk loading/unloading, connectivity
├── generation/ # Terrain blobs, forestry, foliage (stub), fauna (stub)
└── tiles/ # TileMap, TileRegistry, rendering, visibility, tile change events, drop tables
Chunk Architecture
- World chunks: 8×8 tiles (
CHUNK_SIZE = 8,CHUNK_SIZE_TILE = 128px) - Render chunks: 32×32 tiles (
CHUNK_TILES = 32,512px). One render chunk = 4×4 world chunks. This mismatch is intentional — a single GPU texture covers multiple world chunks, which means chunk unloading must mark the render chunk dirty (not the world chunk). - Z-levels: 5 below, 15 above, 21 total (
Z_BELOW = 5,Z_ABOVE = 15). Terrain is rendered as quilted sprites — one texture per z-level, all stacked in the shader. - Loading:
setup_initial_chunksruns atStartup, emitsGenerateChunkEventfor all chunks ininitial_chunk_radius.handle_chunk_eventsprocesses events and fires terrain/forestry/foliage/fauna generation events. - Unloading:
unload_chunk(chunk_pos)is the canonical function. Currently disabled —dynamic_chunk_unloading_systemis a no-op stub. To enable: implement a system that diffs wanted chunks (player/NPC interest radius) againstChunkMap.loaded_chunksand callsunload_chunkfor each evictable chunk.
Rendering Pipeline
handle_tile_occlusion_updates— raycasts from each tile upward to computevisible_rangebitmask (which z-levels can see it). Tiles in loaded chunks with no floor entry are treated as air (open space).build_quilted_terrain_sprites— GPU bake: tiles bucketed by(render_chunk, z_level), blitted into pooled pixel buffers, spawned asTerrainSpriteentitiesupdate_tile_visibility— togglesVisibilityon sprites based on currentZIndex(camera z-level)
Pathfinding
Three tiers, gated by distance:
- Tier 1 (<4 chunks): synchronous A* on
TileMap - Tier 2 (4–8 chunks): provisional path + full path via async queue
- Tier 3 (>8 chunks): hierarchical chunk graph → async segmented A*
Stage 2 Collision — when two entities target the same tile, the movement system resolves in four cases:
- Convoy: entity ahead moves in ~same direction → follow-through, no avoidance
- Head-on: dot product < −0.7 → both yield left (existing sidestep chain)
- E/S yield: entity moving east or south → sidestep chain first, excuse-me on failure
- W/N right-of-way: entity moving west or north → advance with excuse-me delay
PathfindingBenchmark resource tracks sync vs async performance per frame.
Tile Change Invalidation — TileChangedEvent fires whenever a tile's standability changes at runtime. PathfindingDirtyChunks collects affected chunk positions; invalidate_paths_on_tile_change clears paths for entities stepping through changed chunks within 8 steps.
Entity Indexing
Static terrain entities (floor tiles, fixtures, trees) are indexed in chunk_entity_index: HashMap<IVec2, Vec<Entity>> at spawn time. When a chunk unloads, the index is retrieved and entities are despawned. This is O(entities in chunk), not O(total entities).
Mobile entities (dorfs, pigs, rabbits) are not in chunk_entity_index. They are spawned once at game start and persist. They will need per-chunk lifecycle management once fauna generation is implemented.
Controls
| Key | Action |
|---|---|
| Click + drag | Pan camera |
| Scroll | Pan camera |
| Q / E | Move camera z-level down / up |
| Mouse wheel | Pan camera |
Configuration
config.toml:
initial_chunk_radius: chunks loaded around origin at startup (default: 7 → 15×15 = 225 chunks)spawn_counts: dorfs, pigs, rabbits spawned at game start
tiles.toml: tile registry — floor tiles, fixture tiles. Each floor tile has id, astar_weight, can_stand_in, can_stand_on, transparent flags.
Status
Implemented
- Procedural terrain (blob-based, simplex noise heightmap)
- Tile registry (
tiles.toml) with A* weights - Floor tiles, fixture tiles, item tiles
- Z-levels with quilted GPU sprite rendering
- Tile occlusion / visibility raycasting (treats absent tiles in loaded chunks as air)
- Forestry: tree trunks + leaves spawned per chunk
- Dorfs, pigs, rabbits spawning at startup
- Hierarchical async pathfinding (3 tiers)
- Chunk loading infrastructure
- Chunk unloading infrastructure (
unload_chunk,dynamic_chunk_unloading_systemstub) - Camera panning + z-level control
- GPU texture pooling (pixel buffer reuse across bakes)
- Render chunk dirty-tracking for incremental rebakes
- Stage 2 collision resolution (convoy, E/S yield, W/N right-of-way)
- TileChangedEvent + path invalidation on tile change
- Generic tile digging —
Diggercomponent on any entity,dig_systemhandles tile removal, item drops via drop tables, occlusion refresh, path invalidation - Drop tables — tiles carry
DropTabledefining item drops on dig (grass=5% coin, rock=10% coin, dirt/air=none). Pseudo-RNG per position until WyRand is accessible. - Rabbit digging —
Digger::new(5.0)on rabbits; removes floor tile, drops items, fires occlusion + path invalidation
Stubs (exist as empty functions, wired into schedules)
- Foliage generation per chunk (
generate_chunk_foliage) - Fauna spawning per chunk (
generate_chunk_fauna) - Weathering and precipitation (
generate_chunk_weathering_and_precipitation)
Not Started
- Crafting, stockpiles, resource gathering
- Needs, mood, stress systems
- Job assignment and task automation
- Building and construction (generic digging in place; no task system yet)
- Combat and squad management
- Item persistence and inventory
- Dynamic chunk loading (player/NPC-centered, not just radius-at-startup)
- Save/load world state
- World history and procedural storytelling
- UI (menus, z-level overlay, workshop assignment)
Building
cargo build # dev profile
cargo run # dev mode
[profile.dev.package."*"] opt-level = 3 gives near-release perf in dev builds.