# CLAUDE.md DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — check `docs/bevy/` before guessing APIs). ## User rules - Vision: fully simulated colony. Every job colonists can be ordered to do, they must also be able to decide to do autonomously (driven by colony demand); manual orders are a bonus/override. - Code is `cargo fmt`ed; run it before committing. - Commit after each logical chunk; short commit msgs. - Minimal code comments. "Why" explanations go into a test asserting the behaviour, comment lives in the test. - Track work in `tasks.md` (done + planned). - This file: dense notes for Claude, not user facing. ## Layout - `src/actions.rs` `Action` message enum + `KEYBINDS` table (single source of truth for keys; `keyboard_actions` in PreUpdate after `InputSystems`). All non-camera input goes through actions: consumers read `MessageReader` (sim speed/pause, worldgen NewWorld, zones tool/Cancel, selection Cancel, ui ToggleUi). New keybind = add Action variant + KEYBINDS entry + label + TOOLBAR entry (test enforces every keybind has a button). - `src/ui.rs` `HudPlugin`: bottom toolbar of `ui_widgets::Button`s (`ActivateOnPress` — click-on-release was flaky when the pointer moves and clicks in one frame) with `ActionButton(Action)`; global `On` observer writes the action. `style_buttons` highlights active state (`is_active`), hover, pressed. `UiVisible` + `HideableUi` marker (toolbar, info panel) toggled by H/button; `ShowUiButton` appears when hidden. `PointerOverUi` from picking `HoverMap` (any hovered entity with `Node`; layout containers are `Pickable::IGNORE`); `update_cursor` clears world/tile and selection/orders/zone tool ignore clicks while over UI. - `src/main.rs` app wiring: DefaultPlugins (nearest sampling, canvas `#bevy`, fit_canvas_to_parent), adds plugins. - `src/world.rs` `Terrain` enum (index = tileset layer, `ALL` order matters; rock = Stone/IronOre/GoldOre, impassable; Gravel = mined floor, Path/Paved from traffic, never generated), `Plant` enum (Tree, BerryBush, Crop; separate layer with per-tile growth 0..1, adds move cost; `ripe_plant`). Worldgen scatters berry bushes (2%) with random growth. `WorldMap` resource: per-tile terrain, `Option`, wall flag (blocks movement), roof flag, plant growth, traffic wear, height in meters (0..`MAX_HEIGHT`=100); row-major, (0,0)=bottom-left, y up. `move_cost(pos)` = terrain + plant cost (None = impassable); `step_cost(from,to)` adds `UPHILL_COST_PER_METER` × climb — use step_cost for anything that moves. `TILE_SIZE`=16 world units, `hash2` deterministic int hash. - `src/noise.rs` seeded 2D gradient (Perlin-style, quintic fade) noise + `fbm` (octave i uses seed+i). Range [-1,1]. - `src/worldgen.rs` `WorldGenPlugin` inserts `WorldSeed` + `WorldMap` (256x256) at build time and writes `WorldGenerated` message at Startup; R → seed+1, regenerate, write message. `generate(size, seed)`: elevation fbm → height (min-max normalised to meters) and deep/shallow/sand/rock by **quantile** thresholds (fixed terrain shares per seed); ore = quantiles of two ore noises among rock tiles (gold 3%, iron 10%); fertility fbm top 20% → soil; rivers; trees (forest noise top 35% → 45% tree chance, else 3%; only Grass/Soil, placed after rivers). Rivers: `FlowField` = priority flood from water + non-rock edge tiles over non-rock tiles, each tile gets a downstream neighbour (spills out of depressions at lowest rim); `pick_rivers` traces 40 random mountain-foot sources, keeps the 4 longest spaced ≥40 apart; `carve_river` → ShallowWater, wider downstream, never over rock/deep water. `largest_region` (4-conn flood of walkable), `spawn_tiles(map, n)` = BFS from largest-region tile nearest map center. Everything reacting to a new world runs `.run_if(on_message::)`: tilemap refresh, colonist respawn, camera recenter, selection clear. - `src/tilemap.rs` renders map as `TilemapChunk`s (bevy::sprite_render) of `CHUNK_SIZE`=64; chunk transform = chunk center (TilemapChunk is centered on its transform). Tile (x,y) world center = (pos+0.5)*TILE_SIZE, so map spans [0, size*TILE_SIZE]. Tileset is generated in code (no assets): stacked 16x16 layers -> `reinterpret_stacked_2d_as_array`. Per-tile tint via `TileData.color`. Chunk layers (`Layer::Terrain` z=0, `Layer::Zones` z=0.3, `Layer::Plants` z=0.5 (tileset per `PlantLook`: tree, bush, ripe bush, sprout, young crop, ripe crop), `Layer::Roofs` z=0.85 translucent striped overlay, alpha blend, tree texture drawn in code), spawned once at Startup (`Chunk{origin, layer}`); terrain tint = jitter × `hillshade` (light from NW, `RELIEF` exaggeration) × height brightness, water unshaded; ore = stone texture with coloured specks; world changes overwrite `TilemapChunkTileData` in place (full on WorldGenerated, per-chunk on `TileChanged`) (bevy's `update_tilemap_chunk_indices` re-uploads via change detection). - `src/camera.rs` `CameraController` on Camera2d: WASD/arrows pan (shift = fast, speed scaled by ortho scale), middle/right drag pan (from cursor delta, not AccumulatedMouseMotion — that doesn't fire for web), scroll zoom toward cursor (`zoom_towards`), camera center clamped to world rect. - `src/selection.rs` (`update_cursor` is pub; other modules order `.after(update_cursor)`) `Cursor` resource (screen/world/tile) and `Selection` enum (None/Tile/Colonist(Entity)). PostUpdate after `TransformSystems::Propagate` (fresh camera GlobalTransform). LMB: pick nearest colonist within `PICK_RADIUS` of cursor world pos, else tile, off-map clears; Esc clears. RMB click (press/release within `CLICK_SLOP_PX`, else it was a drag-pan) writes `MoveOrder` for selected colonist. Gizmos: hover rect, selected rect / colonist circle + remaining path linestrip. Top-left UI text (clock + speed/PAUSED, seed, colonist count, material and food stock-group/learned-goal lines, recent colony events, zone tool hint, hover incl. stockpile/items, selection incl. job/carrying). Progress bars for Work/ExpandStockpile/Build (`progress_fraction`). Clock shows day/dawn/dusk/night; hover shows structures with remaining fuel, plant growth, farm, animals. `describe_colonist(&ColonistView)` includes rest and food. Chop progress bars (gizmo lines) above working colonists; selected colonist's target tree outlined. Tests use headless `App` + `ButtonInput`/`Cursor` resources, `Messages::drain`. - `src/pathfinding.rs` generic `astar(map, start, is_goal, heuristic)` (start may itself be blocked so movers can leave a tile that became a wall; `follow_path` also drops a route whose next tile became impassable), 8-dir, no corner cutting, cost = (10|14) * `map.step_cost` (100 = normal flat grass), octile heuristic. `find_path(start, goal)`, `find_path_adjacent(start, target)` (ends on a tile Chebyshev-adjacent to target; works for impassable targets; empty if already adjacent), `nearest_tile(start, max_tiles, pred)` BFS over walkable tiles. Paths exclude start, include goal. - `src/colonist.rs` `Colonist{name}` (`spawn_colonist(index)` picks name/colour; 3 spawned on `WorldGenerated` at `spawn_tiles`) requires `TilePos` (world.rs; last reached tile), `Path(VecDeque)`, `CurrentJob`, `IdleTimer`. `follow_path` (SimSystems::Move) moves `SimPos`, speed / step cost. `repath` keeps the tile being entered at the front. - `src/jobs/mod.rs` job system (tests in `src/jobs/tests.rs`). `CurrentJob(Option)`, `Task::{MoveTo, Work{site, kind: WorkKind::{Chop, Mine}, progress}, Haul{pickup: Option, to}, ExpandStockpile{site, progress}, Build{site, kind: StructureKind, pickup: Option, progress}}`. Progress is in work units: `work_rate` = 2/tick when the colonist's tile is lit, 1 in the dark (`WorkKind::work`, `EXPAND_WORK`, `build_work`). `site_work(map, tile)` is the single source of what a tile yields (tree → 10 wood; Stone/IronOre/GoldOre → 10 stone / 10 iron / 5 gold, mined tile becomes Gravel). `Reservations` keyed by `Target::{Site, Pickup, Storage}(tile)` and `Target::StockpileExpansion` (one expander at a time); cleared on WorldGenerated. `ColonyGoals` (economy.rs) = learned demand per stock group. `choose_jobs` (SimSystems::Decide), idle colonists (exhausted/starving ones skip the idle timer), priority: sleep if exhausted → eat if hungry (drops carried load) → sleep if `wants_sleep` → deliver carried load (or drop if no storage) → `find_expansion` (no stockpile + loose items → found a NEW_STOCKPILE_SIZE² stockpile at `find_stockpile_site` near the central loose item; or stockpile has < MIN_EMPTY_STOCKPILE_TILES empty tiles → grow EXPANSION_TILES via `plan_expansion`) → if `dark_soon`: `find_lighting` (`BuildContext` (shared by lighting and beds; `buildable` = dry, not stockpile/plant/structure/planned/item, unreserved): campfire within HOME_RADIUS of home = stockpile tile nearest its centroid unless one burns with ≥ CAMPFIRE_MIN_FUEL or is planned; else a torch next to the stockpile tile nearest home that existing + planned fires leave dark; needs a reachable unreserved wood stack ≥ cost; Build fetches wood, walks adjacent, builds, consumes wood) → cooking if meals wanted and a raw stack ≥ COOK_INPUT exists (`find_cooking` at a campfire with ≥ COOK_MIN_FUEL, else `find_campfire`) → `find_bed_site` → `find_blueprint_work` → `find_work` (nearest stand tile adjacent to an unreserved site whose yield is wanted, counting yields of work, hunts and cooks in progress) → `find_hunt` if meat wanted → `find_sowing` → `find_haul` → wander. `find_bed_site`: one bed per colonist incl. planned; next to nearest bed, else campfire, else home; ≥ BED_STOCKPILE_CLEARANCE from stockpile and farm tiles so a wall ring fits. `find_blueprint_work`: nearest unreserved blueprint with material available. Cook: fetch COOK_INPUT raw units of one kind, cook COOK_WORK next to the campfire (`Target::Station`), drop 1 Meal (0.9 nutrition vs 0.6 raw). Orders: animal → ordered Hunt (checked first), work site → Work, loose item → Haul, else MoveTo. `run_jobs` (SimSystems::Act) per-task state machine → `Step::{Continue, Done}`; Done releases reservations, restarts idle timer. Orders: work site (tree/rock) → ordered Work; loose item → ordered Haul; else MoveTo. Orders keep `Carrying`. New job = Task variant + reserve_task arm + choose_jobs candidate + run_jobs arm (+ `site_work` entry for site work). - `src/items.rs` `Item{kind: ItemKind::{Wood, Stone, Iron, Gold, Berries, Meat, Vegetables, Meal}, count, spoil}` (`Item::new`, `merge` averages spoil by count, `with_count`; `drop_items` takes a whole Item) (`nutrition`, `is_food`, `is_raw_food`) + `TilePos` entities (Mesh2d rect, z=0.7), one stack per tile, `STACK_LIMIT`=50; `ItemQuery` alias; `drop_items` stacks, `take_items`, `room_for(tile, kind)` (0 if other kind present), `stock()`. `Carrying(Option)` on colonists, shown via hidden/visible `CarriedVisual` child. Items cleared on WorldGenerated. - `src/zones.rs` `Zones` resource: stockpile and farm tile sets (`ZoneKind`; stockpile = walkable dry land, farm = `can_farm` fertile; never overlap; farm rendered as zone layer index 1), cleared on WorldGenerated — worlds start without stockpiles, colonists found one when needed. `plan_expansion(map, zones, excluded, count)` (excluded = blueprint tiles) grows one tile at a time next to stockpile/planned tiles, preferring tree-free tiles near the stockpile centroid (deterministic tiebreak). `find_stockpile_site` = BFS for nearest fully open (zonable, tree-free) square, fallback any zonable tile. `ZoneTool{mode: Option}` (Z stockpile / F farm toggle, Cancel exits): left-drag adds, shift-drag removes (`zone_tool` input + `draw_zone_preview` gizmo, PostUpdate after `update_cursor`). Rendered as tilemap `Layer::Zones` (z=0.3), refreshed on `resource_changed::`. - `src/sim.rs` `SimPlugin`: `SimSystems::{Decide, Act, Move}` chained sets in FixedUpdate; fixed timestep `Time` 60 Hz = sim ticks; all sim logic in `FixedUpdate` (Res