Bump MVP map to 80², lock world-view camera, resolve doc rot

Resolves the rimlike docs audit. Decisions made this session:

- Map: 40² → 80² for MVP slice; architecture sized to ~120² ceiling.
  Pawn count unchanged (3 start / 6 cap) — 'split-the-difference'
  sizing rather than frontier-feel scale.
- World-view camera (locked): pinch + drag-pan + double-tap-centre.
  Storyteller alerts include 'Go there' tap. No minimap, no
  follow-cam in MVP. New ui.md section captures this.
- Storyteller cooldowns: per-event AND per-category — both gates
  must pass. Resolves design.md/architecture.md disagreement.
- MAX_STACKS_PER_THOUGHT = 5 (was named-but-unset).
- Hauling no-destination fallback: drop after 3 retry passes,
  surface a passive 'No stockpile accepts X' alert.
- Container all-neighbors-blocked: hold then drop after ~5 sim s
  to avoid deadlock.
- Auto-roof big-room UX: emits room_too_large signal when BFS hits
  the ≤8-cell cap; UI surfaces a 'split with an interior wall'
  banner. Threshold + exact wording added to memory.md TODOs.

Doc rot cleaned in design.md and architecture.md: removed the stale
'8 work categories' intermediate sections (canonical 9-list lives
later in each file). Updated architecture.md perf claims for the new
map size.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-05-10 19:45:29 +01:00
parent d233cef12f
commit c5dadedab0
4 changed files with 52 additions and 19 deletions

View file

@ -97,15 +97,17 @@ Multipliers drive sim tick stepping per render frame. At 60 Hz render with 20 Hz
- Ultra (12×): 240 ticks/sec (4 ticks per render frame)
- Pause: zero ticks queued
Per-tick budget at Ultra: ~4 ms to stay in 60 fps render. Achievable for our scope (36 pawns, < 500 entities, 60×60 tiles).
Per-tick budget at Ultra: ~4 ms to stay in 60 fps render. Achievable for our scope (36 pawns, < 500 entities, **80×80 MVP map; sized to a 120² ceiling**). At 80² the AStarGrid2D has 6.4k nodes; longest path 160 tiles; full A* runs sub-millisecond on a mid-range phone. At 120² (22.5k nodes) it's still well under a millisecond per query.
### Pawn movement
| Speed | Real-time crossing of 40-tile map | Feel |
| Speed | Real-time crossing of 80-tile map (corner-to-corner) | Feel |
|---|---|---|
| 1× (1 tile / 0.5 s) | ~20 s | Watchable, deliberate |
| Fast (5×) | ~4 s | Snappy, default cadence |
| Ultra (12×) | ~1.7 s | Almost teleporting |
| 1× (1 tile / 0.5 s) | ~80 s | Watchable, deliberate |
| Fast (5×) | ~16 s | Snappy, default cadence |
| Ultra (12×) | ~7 s | Almost teleporting |
Travel times are roughly 2× the original 40-tile estimates. A wolf spawning at the map edge gives ~20 s real-time lead at Fast — meaningful warning, not tedious.
This mostly absorbs the "walking-speed problem" surfaced earlier — Fast as default keeps a 5-min day from feeling like watching paint dry.
@ -260,6 +262,10 @@ walk_to(item.cell) → pick_up(item, capped_at_carry_capacity)
Carry capacity capped at one stack of one type. Multi-type carry is v2.
**No-destination fallback.** If `find_best_destination_for(item)` returns null on three consecutive periodic-flow passes (~15 sim seconds), the item is dropped where it lies, taken off the dirty set, and a passive "*No stockpile accepts X*" alert is queued. This prevents `_dirty_items` from cycling forever when the player has zero matching storage. The alert clears the moment a matching destination opens up (it re-enters the dirty set on the next storage-change event).
**Container all-neighbors-blocked.** `best_drop_position(from)` returns the closest free 4-neighbor of the container. If all four are blocked, the hauler holds the carry and re-tries on the next pass; if still blocked after ~5 sim seconds the carry gets dropped on the hauler's current tile (same fallback as above) so it doesn't deadlock.
### Container build flow
1. Player taps Build → Furniture → Crate.
@ -322,6 +328,8 @@ func compute_mood(p: Pawn) -> float:
return clamp(m, 0, 100)
```
`MAX_STACKS_PER_THOUGHT = 5`. Caps "saw corpse" / "ate without table" / "slept on floor" pile-ups so a single thought type can't deterministically tank mood by itself; the player still loses points from variety, which matches the Rimworld feel.
Thought registry (data file) drives content. Each thought entry knows:
- Trigger: what game event creates it (saw_corpse from EntityProximity, slept_well from JobComplete, hungry from NeedThreshold, …)
- Lifetime: persistent (driven by ongoing state) or event (decays after N hours)
@ -375,7 +383,7 @@ func recompute_light(affected_cells: Array[Vector2i]):
Recomputed only when a light source is added/removed/state-changes (turned off due to fuel, etc.). Cheap.
**Render side:** at night, world rendered with darkness shader; the shader samples `light_map` to brighten tiles. Phone GPU runs this comfortably for 60×60 tiles.
**Render side:** at night, world rendered with darkness shader; the shader samples `light_map` to brighten tiles. Phone GPU runs this comfortably for the MVP 80×80 map (and the 120² ceiling) — light recompute touches only cells inside the affected light's radius (max_radius=8 → ≤ 200 cells), not the whole map.
**Mood integration:** `is_lit(cell)` returns `light_map[cell] > 0.2`. Fires "In darkness" thought for pawns in unlit tiles at night.
@ -450,10 +458,6 @@ Decay: zero — dirtiness only goes down via Cleaning.
8th WorkProvider in the existing pawn AI. Scans dirty tiles in/near rooms with priority bias toward indoor tiles. Job toils: walk → clean (timed by Manual Labor skill) → set dirtiness to 0.
### Updated WorkProvider list (8)
Construction · Mining · Hauling · **Cleaning** · Cooking · Plant · Doctor · Combat
## Production: workbenches, recipes, bills
For player rules and recipe lists, see [`design.md`](./design.md). This section is data + AI plug-in.
@ -889,6 +893,9 @@ func build_weighted_pool() -> Dictionary:
var pool = {}
for ev in EVENT_REGISTRY.values():
if not ev.trigger.matches(World.state, current_day): continue
# Both gates must pass: per-event AND per-category cooldown.
# Per-event prevents the *same* event repeating within its individual cooldown.
# Per-category enforces the design.md pacing rule (no two threats within 3 days, etc.).
if recently_fired(ev.id, ev.cooldown_days): continue
if recently_fired_category(ev.category, CATEGORY_COOLDOWN[ev.category]): continue
var w = ev.weight * tension_modifier(ev.category)
@ -982,6 +989,8 @@ func on_wall_change(cell: Vector2i):
`bfs_finds_exit(start)`: flood from `start`, treating walls as blockers; return true if we reach the map edge or a No-Roof designated cell within 8 steps. Fast at our scale (8² = 64 cells worst case per BFS, called on each candidate within the affected radius — still microseconds).
**Big-room feedback.** When `bfs_finds_exit` returns true *because the BFS ran out of steps without escaping or roofing* (i.e. the area is enclosed but larger than the cap), the EnclosureDetector emits a `room_too_large` signal carrying the centroid cell. The UI raises a one-shot ambient banner: "*This area is too large to roof. Split it with an interior wall.*" Threshold and exact UX wording TBD — see `memory.md` Open questions.
When a wall is destroyed: candidates are re-evaluated; some may flip from roof to no-roof.
### No-Roof designation
@ -1070,7 +1079,7 @@ Children of a `World` node, tile-aligned positions:
### Pathfinding
`AStarGrid2D`, one grid the size of the map. Walkable derived from TileMap data + furniture occupancy. Update affected cells on build/destroy/door-state-change. Microseconds at 60×60.
`AStarGrid2D`, one grid the size of the map. Walkable derived from TileMap data + furniture occupancy. Update affected cells on build/destroy/door-state-change (one cell per change → O(1)). Sub-millisecond per path query at 80²; still well under a millisecond at the 120² ceiling.
### Saving
@ -1078,7 +1087,7 @@ Per layer, `tilemap.get_used_cells_by_id(layer)` → JSON. Plus furniture entiti
### Performance
60×60 × 5 layers = 18k tiles, GPU-batched in one draw call per layer. ~100500 entity nodes at peak. Comfortable 60fps on a mid-range phone with headroom. iOS export needs Mac/Xcode; Android export from Linux is fine.
80×80 × 5 layers = 32k tiles (MVP); 120×120 × 5 = 72k tiles (ceiling). GPU-batched in one draw call per layer regardless of cell count, so the bump is essentially free for the renderer. ~100500 entity nodes at peak. Comfortable 60 fps on a mid-range phone with headroom. iOS export needs Mac/Xcode; Android export from Linux is fine.
## What lives elsewhere

View file

@ -175,10 +175,6 @@ Floor tiles accumulate `dirtiness: float (0..100)`:
Mostly low-skill chore work — gives Manual-Labor pawns something to do when not building/mining/hauling.
### Updated work category list (now 8)
Construction · Mining · Hauling · **Cleaning** · Cooking · Plant · Doctor · Combat
## Production chains & workbenches
2-step chains where it makes medieval sense; 1-step where simpler is fine. Going Medieval / Banished flow without runaway recipe authoring.
@ -540,6 +536,8 @@ When walls form an enclosed area ≤ 8 cells across, the interior **auto-roofs**
Matches the player's mental model: *"I built four walls, now there's a room."*
**Big-room UX (open):** the ≤8-cell cap silently fails on rooms larger than 8×8. When the player encloses a too-large area, we should surface "this area is too large to roof — split it with an interior wall." Exact threshold + UI treatment is TBD (see `memory.md` Open questions).
### "No-roof" designation
Designation type: paint cells inside an enclosure to forbid auto-roofing. Useful for courtyards, fire pits, gardens. Same paint UX as stockpile zones.

View file

@ -610,11 +610,24 @@ Top bar also shows season + day-of-season ("Spring 4/12"). Tap → seasonal fore
Wet pawns have a subtle drip particle + slight sprite tint until they dry off. Players can quickly spot "Bob is soaked, why?" without opening the pawn detail screen.
## World view camera (locked)
The camera/navigation model on the main world view, decided 2026-05-10 alongside the bump from a 40² to an 80² map.
- **Pinch-zoom**: between a "strategic" zoom (whole-map-ish on tablet, ~1/4 of the map on phone) and a "close" zoom (~16 tiles wide on phone, sprite-readable). No fixed zoom levels — smooth.
- **Drag-pan**: one-finger drag on empty world tiles. Drag on a pawn = select-and-drag-issue-order (long-press-then-drag for multi-select rectangle).
- **Double-tap to centre**: double-tap on a pawn portrait, alert banner, or the world centres the camera there with a brief animated pan.
- **No follow-camera**: selecting a pawn does **not** lock the view to them. Selection persists across pans so the player can scroll to a build site and issue an order without losing their pawn.
- **Jump-to-alert**: every storyteller alert / banner / event modal includes a *Go there* tap that pans-and-centres the camera on the relevant tile (raid spawn, downed pawn, fire, etc.). Replaces the "where is this happening?" minimap need.
- **No minimap in MVP**. Reasoning: phone screen real estate is precious, and *Jump-to-alert* + double-tap-on-portrait covers the navigation needs at 80². Revisit if playtest shows people getting lost.
- **Speed / pause buttons** stay fixed at the top regardless of camera state.
Layered on the world view: select (tap empty pawn), inspect (long-press anything), build mode (bottom-sheet → paint), designation paint (bottom-sheet → designate type → paint).
## Screens still to design
These are open work for future sessions. Listed roughly in importance.
- **World view** — camera/select/orders. The screen the player sees most. Tap targets, long-press menus, pinch-zoom, multi-select (for multi-pawn orders).
- **Build drawer** — bottom-sheet tabs (Walls / Floors / Furniture / Production / Designate). Designation paint mode. Material-pick UI when a build can use multiple materials.
- **Alerts / storyteller event** — modal vs. ambient, dismissal, history of past prompts.
- **Day-summary / end-of-day** — a recap card showing what changed today; gives short sessions a stopping point.