Skip to content

W-076: demo-anno ANNO-F01 island, treasury, place and demolish

Type: feature · Size: M · Epic: E-anno-m1 · Target: apps/demo-anno · Breaking: no Depends: W-069, W-070 Refs: brief ANNO-F01; demo-anno spec ANNO-001..007, 009..016

Goal

Build the first playable slice of apps/demo-anno: the content (building kinds, island 16×16, start gold, goal, one starting Warehouse), the world traits of ANNO-001 (Treasury, Settlement, GameClock with defaults and no logic yet, Builder, Messages, Stock), setupSettlement with bounds checks, placeBuilding and demolishAt with their rejection cases, the builder actions (cursor, selected kind), the labelled island with cell labels and cursor, the focusable game area with the key rule and the focus rule, the Build palette, the Demolish button, and the "Gold: N" text. No goods, carriers, houses, pause, or messages yet (F02 to F05). Ends with the acceptance checklist A1 (ANNO-016).

Steps

Constraints for every step: no new dependencies, no packages/ecs-* change, no edit to existing tests (App.test.tsx keeps the "demo-anno" heading and the shared world export). Tests are Vitest with a fresh createWorld() per test and world.destroy() in afterEach; test names start with the ID.

  • S1 (ANNO-001, ANNO-002, ANNO-003, ANNO-006): content, traits, and setupSettlement; files: src/content/buildings.ts, src/content/settlement.ts, src/content/clock.ts, src/content/index.ts (island 16×16, startGold 300, goal 50, one Warehouse in startBuildings, kinds with cost, upkeep, capacity, recipe, carriersPerWarehouse; clock values are plain data only), src/ecs/traits.ts (Name, Position, Building, Storage, Warehouse, Producer; world traits Treasury, Settlement, GameClock, Builder, Stock, Messages, and NameCounter, an AoS world trait holding the next number per kind, so Settlement keeps the exact shape of ANNO-001), src/ecs/spawn.ts (the one spawn function; names are "Kind N" with N from the per-kind counter in NameCounter on the world, starting at 1, incremented on each spawn and never reused after a demolish; no module-level counter), src/ecs/setup.ts (setupSettlement(world, content?) adds NameCounter before spawning startBuildings; throws naming setupSettlement and the entry for out-of-bounds, overlapping, or island over 24×24), tests src/ecs/setup.test.ts (defaults and world-trait shapes with Settlement exactly { population, goal, status }, custom content with other numbers and names, the three throw cases, spawned traits per kind, no Housing, Badge, or Carrier; naming: the starting Warehouse is "Warehouse 1", two spawned Sawmills are "Sawmill 1" and "Sawmill 2", counters are per kind; after destroying "Sawmill 1" the next Sawmill is "Sawmill 3" when "Sawmill 2" exists and "Sawmill 2" otherwise, never "Sawmill 1" again; two worlds in one test keep independent counters, and a fresh world starts again at 1).
  • S2 (ANNO-004, ANNO-005, ANNO-007, ANNO-009): placeBuilding and demolishAt as createActions actions; files: src/ecs/actions.ts, tests src/ecs/build.test.ts (success subtracts cost via world.set(Treasury, …) and names the building "Kind N" from NameCounter; place "Sawmill 1", demolishAt it, place again gives "Sawmill 2" on a new entity; a rejected placeBuilding does not advance the counter (ANNO-005 "change nothing"); one rejection test each for unknown kind, out of bounds, occupied cell, gold = cost − 1 vs gold = cost leaving 0, status not 'ongoing' set with world.set, and that nothing changes on false; demolish returns true only with a building on the cell, no refund, false and no change otherwise; place, demolish, place again gives a new entity and never two buildings on one cell). No refreshStock call and no goods or carrier handling yet.
  • S3 (ANNO-010): builder actions; files: src/ecs/actions.ts, tests src/ecs/builder.test.ts (selectBuilding(kind | null); moveCursor for the four directions returns false and stays at each island edge; setCursor returns false outside the island; placeAtCursor equals placeBuilding at the cursor and returns false without a selected kind; demolishAtCursor equals demolishAt at the cursor).
  • S4 (ANNO-011, ANNO-012): island view and focusable game area with the key rule; files: src/ui/island/Island.tsx (region "Island", width × height cells, labels "3,2 Sawmill 1" / "4,4 empty", cursor cell with aria-current="true" and the text "cursor", buildings read through useQuery, no colour-only state), src/ui/input/GameArea.tsx (tabIndex=0, label "Game", keydown on the container only when event.target is the container, ignores event.repeat except arrows, preventDefault only for handled keys; arrows, 1..5 in content palette order, Enter, Delete or x, Escape; focus on mount), src/App.tsx (renders GameArea and keeps the heading; calls setupSettlement(world) once, guarded by world.has(Treasury), so the existing App.test.tsx passes unchanged), tests src/ui/island/Island.test.tsx and src/ui/input/GameArea.test.tsx (labels, aria-current, focus after mount, each key runs its action, a key pressed on a focused button or an input does not run an action and is not default-prevented, held non-arrow keys are ignored). The focus-after-click part of ANNO-012 is tested in S5 where the buttons and cell clicks exist.
  • S5 (ANNO-012, ANNO-013, ANNO-014, ANNO-015): Build palette, cell click, Demolish button, and Gold text; files: src/ui/build/BuildPalette.tsx (group "Build", one button per content kind with name and cost, aria-pressed, aria-disabled plus "Need N gold" when unaffordable, disabled when status is not 'ongoing'), src/ui/build/DemolishButton.tsx (aria-disabled when no building on the cursor cell), src/ui/hud/Gold.tsx ("Gold: N" via useTrait(Treasury)), src/ui/island/Island.tsx (cell click: setCursor, then placeAtCursor when a kind is selected), the shared focus-to-game-area helper in src/ui/input/, tests src/ui/build/BuildPalette.test.tsx, src/ui/island/IslandClick.test.tsx, src/ui/hud/Gold.test.tsx (palette button and cell click and Demolish each end with document.activeElement on the game area and an ArrowRight keydown moving the cursor; "Need N gold" with gold = cost − 1; Gold text changes after a successful placement and after a demolition; status not 'ongoing' disables the palette).
  • S5b (test quality): investigate the root cause of React act(...) warnings in apps/demo-anno tests (223 occurrences under pnpm turbo run test, from BuildPalette, DemolishButton, Island and others: world updates made outside act, plus possibly missing IS_REACT_ACT_ENVIRONMENT in src/test/setup.ts) and fix them so the test run is warning-free. Editing existing demo-anno tests and src/test/setup.ts is allowed for this; assertions must not be weakened. Prefer a shared test helper (e.g. wrapping world writes and actions in act) over per-test hacks.
  • S5c (UI): redo the layout and styling of the demo-anno UI with the shared primitives in packages/ui/src/components (e.g. card, toggle-group or button-group for the palette, separator, badge, item, tooltip, empty) via @workspace/ui/components/<name>, instead of raw divs and ad-hoc classes. Keep all accessibility contracts and existing test queries (roles, labels, aria-pressed, aria-disabled, "Need N gold", "Gold: N", Island region and cell labels), 1280x720 without page scroll, and non-colour-only state. Screenshot feedback to fix: the Demolish button spans the full width, the island is tiny and top-left, palette text is cramped.
  • S6 (acceptance) (ANNO-016): run A1 of the spec's acceptance checklist in pnpm --filter demo-anno dev (1280×720 without page scroll, content island temporarily 24×24 and reverted, Lighthouse accessibility in light and dark with d); run pnpm turbo run typecheck lint test --filter=demo-anno.

Log

  • 2026-11-02 created from the approved spec (Triage). Size M; held in Later until W-069 (done) and W-070 (done) are both in Done (Triage)
  • 2026-11-02 Planner note: the spec's F01 group is ANNO-001..007 and 009..016 (ANNO-008 is re-homed to F02). ANNO-003 needs content and no literals in systems. OQ4 (no refund on demolish) and OQ11 (one starting Warehouse) are the defaults used here. OQ13 (key map) is the default. (Triage)
  • 2026-11-02 all dependencies done (W-069, W-070), moved from Later to Next (Triage)
  • 2026-11-02 moved up in Next: demo-anno first feature starts the M1 path (Triage)
  • 2026-11-02 set active: moved from Next to Now on the board (Triage)
  • 2026-11-02 steps written (Planner): 6 steps. Open questions in the planner report: building name numbering after demolish, and where setupSettlement is called so the existing App.test.tsx stays green
  • 2026-11-02 plan approved by the user. Decisions: names use a per-kind counter that never reuses numbers after demolish, kept in a per-world NameCounter trait (not module-level, not in Settlement), S1 and S2 updated; setupSettlement stays called in App guarded by world.has(Treasury) so App.test.tsx stays unchanged (Planner)
  • 2026-11-02 S1 reviewed APPROVED (Reviewer)
  • 2026-11-02 S2 reviewed APPROVED (Reviewer)
  • 2026-11-02 S3 reviewed APPROVED (Reviewer)
  • 2026-11-02 S4 reviewed APPROVED (Reviewer); nits: GameArea ignores modifier keys not handled, handled non-arrow keys lack preventDefault assertions, Island gridcell aria-label hides the sr-only cursor text
  • 2026-11-02 S5 reviewed APPROVED after 1 fix round (selected-kind and Demolish disabled styles) (Reviewer); nits: clicking an unaffordable aria-disabled palette button does not call selectBuilding while keys 1..5 do (ANNO-013 says activating MUST select; spec should state whether affordability gates selection), act(...) warnings under turbo, Gold text cannot change on demolish (no refund)
  • 2026-11-02 user feedback after S5: added S5b (test quality, act warnings) and S5c (UI redo with shared packages/ui primitives) before S6 (Triage)
  • 2026-11-02 decision: unaffordable palette click does not select the kind (keep as is, user decision); spec ANNO-013 to be clarified by SpecWriter (Triage)
  • 2026-11-02 S5b root cause verified by Reviewer: vitest runs afterEach in reverse so world.destroy ran before RTL unmount (241 act warnings with AI_AGENT/COPILOT_AGENT unset, 0 with sequence.hooks 'list'); note vitest hides stderr when those env vars are set, so verify with them unset
  • 2026-11-02 ANNO-013 tests added (3) after spec clarification (TestWriter)
  • 2026-11-02 S5b and S5c reviewed APPROVED after 1 fix round (Reviewer); nits: fractional cell size, selected and hover share bg-muted, act wrappers are inline per test file rather than a shared helper
  • 2026-11-02 final review APPROVED (Reviewer)
  • 2026-11-02 acceptance A1 approved by the user
  • 2026-11-02 set review, accepted, Done (Triage)