popertots 7cf1b83427 Fix: Allow dorfs running Idle task to be job candidates
The bug: We excluded ALL Active state dorfs, but Idle tasks get
immediately promoted from Pending to Active by task_executor_system.
So by the time job_pathfinding runs, all dorfs are Active with Idle.

The fix: Check if dorf is 'busy' = Active AND non-Idle task.
Dorfs with Idle tasks (even in Active state) can be interrupted
for real jobs.
2026-04-04 11:53:29 +01:00
2026-03-22 16:05:33 +00:00
2026-03-27 10:29:33 +00:00
2026-03-21 17:51:05 +00:00
2026-03-20 11:19:54 +00:00
2026-03-19 20:17:15 +00:00

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, structural constants
├── entities/
│   ├── item/constants.rs    # Item sprite constants
│   ├── livestock/
│   ├── sentient/
│   └── shared_systems/
│       ├── constants.rs     # Pathfinding + dig constants
│       ├── occupancy.rs
│       └── pathfinding.rs
├── game.rs            # ZIndex resource, frame timing
├── main.rs            # Bevy app bootstrap
└── world/
    ├── chunks/        # ChunkMap, chunk loading/unloading, connectivity
    ├── generation/
    │   ├── forestry/constants.rs  # Tree constants
    │   ├── terrain/constants.rs   # Terrain + cave constants
    │   ├── forestry.rs
    │   └── terrain.rs
    └── tiles/
        ├── constants.rs          # A*, benchmark constants
        ├── chunk_data.rs
        ├── rendering.rs
        ├── tilemap.rs
        ├── tile_changed.rs
        └── visibility.rs
├── 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_chunks runs at Startup, emits GenerateChunkEvent for all chunks in initial_chunk_radius. handle_chunk_events processes events and fires terrain/forestry/foliage/fauna generation events.
  • Unloading: unload_chunk(chunk_pos) is the canonical function. Currently disabled — dynamic_chunk_unloading_system is a no-op stub. To enable: implement a system that diffs wanted chunks (player/NPC interest radius) against ChunkMap.loaded_chunks and calls unload_chunk for each evictable chunk.

Rendering Pipeline

  1. handle_tile_occlusion_updates — raycasts from each tile upward to compute visible_range bitmask (which z-levels can see it). Tiles in loaded chunks with no floor entry are treated as air (open space).
  2. build_quilted_terrain_sprites — GPU bake: tiles bucketed by (render_chunk, z_level), blitted into pooled pixel buffers, spawned as TerrainSprite entities
  3. update_tile_visibility — toggles Visibility on sprites based on current ZIndex (camera z-level)

Pathfinding

Three tiers, gated by distance:

  • Tier 1 (<4 chunks): synchronous A* on TileMap
  • Tier 2 (48 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 InvalidationTileChangedEvent 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_system stub)
  • 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 — Digger component on any entity, dig_system handles tile removal, item drops via drop tables, occlusion refresh, path invalidation
  • Drop tables — tiles carry DropTable defining 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.

S
Description
A DF inspired civ-sim RTS sandboxy thing
Readme
1.5 MiB
Languages
Rust 100%