- 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.
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 resource
├── constants.rs # Tile size, pathfinding constants
├── entities/ # Dorfs, pigs, rabbits — ambulatory entities
│ ├── sentient/
│ ├── livestock/
│ └── shared_*/ # Ambulatory, pathfinding 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
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)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*
PathfindingBenchmark resource tracks sync vs async performance per frame.
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 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
- 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
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 (digging/mining)
- Combat and squad management
- Item persistence and inventory
- Save/load world state
- World history and procedural storytelling
- UI (menus, z-level overlay, workshop assignment)
- Dynamic chunk loading (player/NPC-centered, not just radius-at-startup)
Building
cargo build # dev profile, ~1.2K warnings (pre-existing)
cargo run # dev mode
[profile.dev.package."*"] opt-level = 3 gives near-release perf in dev builds.