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,startGold300,goal50, one Warehouse instartBuildings, kinds with cost, upkeep, capacity, recipe,carriersPerWarehouse; clock values are plain data only),src/ecs/traits.ts(Name,Position,Building,Storage,Warehouse,Producer; world traitsTreasury,Settlement,GameClock,Builder,Stock,Messages, andNameCounter, an AoS world trait holding the next number per kind, soSettlementkeeps 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 inNameCounteron 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?)addsNameCounterbefore spawningstartBuildings; throws namingsetupSettlementand the entry for out-of-bounds, overlapping, or island over 24×24), testssrc/ecs/setup.test.ts(defaults and world-trait shapes withSettlementexactly{ population, goal, status }, custom content with other numbers and names, the three throw cases, spawned traits per kind, noHousing,Badge, orCarrier; 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):
placeBuildinganddemolishAtascreateActionsactions; files:src/ecs/actions.ts, testssrc/ecs/build.test.ts(success subtracts cost viaworld.set(Treasury, …)and names the building "Kind N" fromNameCounter; place "Sawmill 1",demolishAtit, place again gives "Sawmill 2" on a new entity; a rejectedplaceBuildingdoes not advance the counter (ANNO-005 "change nothing"); one rejection test each for unknown kind, out of bounds, occupied cell,gold = cost − 1vsgold = costleaving 0, status not'ongoing'set withworld.set, and that nothing changes onfalse; demolish returnstrueonly with a building on the cell, no refund,falseand no change otherwise; place, demolish, place again gives a new entity and never two buildings on one cell). NorefreshStockcall and no goods or carrier handling yet. - S3 (ANNO-010): builder actions; files:
src/ecs/actions.ts, testssrc/ecs/builder.test.ts(selectBuilding(kind | null);moveCursorfor the four directions returnsfalseand stays at each island edge;setCursorreturnsfalseoutside the island;placeAtCursorequalsplaceBuildingat the cursor and returnsfalsewithout a selected kind;demolishAtCursorequalsdemolishAtat 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 × heightcells, labels"3,2 Sawmill 1"/"4,4 empty", cursor cell witharia-current="true"and the text "cursor", buildings read throughuseQuery, no colour-only state),src/ui/input/GameArea.tsx(tabIndex=0, label "Game",keydownon the container only whenevent.targetis the container, ignoresevent.repeatexcept arrows,preventDefaultonly for handled keys; arrows,1..5in content palette order,Enter,Deleteorx,Escape; focus on mount),src/App.tsx(rendersGameAreaand keeps the heading; callssetupSettlement(world)once, guarded byworld.has(Treasury), so the existingApp.test.tsxpasses unchanged), testssrc/ui/island/Island.test.tsxandsrc/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-disabledplus "Need N gold" when unaffordable, disabled when status is not'ongoing'),src/ui/build/DemolishButton.tsx(aria-disabledwhen no building on the cursor cell),src/ui/hud/Gold.tsx("Gold: N" viauseTrait(Treasury)),src/ui/island/Island.tsx(cell click:setCursor, thenplaceAtCursorwhen a kind is selected), the shared focus-to-game-area helper insrc/ui/input/, testssrc/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 withdocument.activeElementon the game area and anArrowRightkeydown moving the cursor; "Need N gold" withgold = 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 underpnpm turbo run test, from BuildPalette, DemolishButton, Island and others: world updates made outside act, plus possibly missing IS_REACT_ACT_ENVIRONMENT insrc/test/setup.ts) and fix them so the test run is warning-free. Editing existing demo-anno tests andsrc/test/setup.tsis 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 withd); runpnpm 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
setupSettlementis called so the existingApp.test.tsxstays 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
NameCountertrait (not module-level, not inSettlement), S1 and S2 updated;setupSettlementstays called inAppguarded byworld.has(Treasury)soApp.test.tsxstays 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/uiprimitives) 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)