popertots ec1ec6b7d4 Fix bug: prevent multiple job assignments to same dorf in one frame
- Add assigned_this_frame HashSet to track dorfs receiving jobs
- Filter out already-assigned dorfs from subsequent job assignments
- Add debug logging for spawn position (z-level)
- Add debug logging for is_standable failures
- Add debug logging for falling/snapping movement behavior

This fixes the critical bug where one dorf could be assigned multiple
jobs in the same frame, causing earlier assignments to be lost due to
queue.clear(). Each dorf now receives at most one job per frame.
2026-04-05 18:09:37 +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%