2026 10 19 classic dungeon temples - xopherdeep/do-it-for-the-xp GitHub Wiki

Classic Dungeon Temples Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Remake the nine shipped temples as Zelda homages: Wind keeps the Eagle (Zelda 1 Level 1) with 1:1 contents, and eight temples are converted from A Link to the Past dungeons. To support them, fight rooms gain drops, the solver learns in-temple Bombs, and chores wake floor by floor.

Architecture: A Python converter reads pinned, MIT-licensed tables once and writes one plain JSON per dungeon. A TypeScript dress step turns each JSON into src/lib/engine/temples/<id>.ts under our rules and themes, like tools/temples/regenerate.ts does today. Three engine changes land first, each in its own pure module with jest specs:

  • fight-room drops (roomGrants, the room sheet, the chest reveal);
  • Bomb sources in the solver;
  • floor-by-floor wake (wakeOnEntry).

Tech Stack: Vue 3 + Ionic 7 + Pinia, TypeScript, jest, Cypress 8; Python 3 (stdlib only) for the converter; esbuild to run TS tools.

Spec: docs/superpowers/specs/2026-10-18-classic-dungeon-temples-design.md (docs 25be956). Board: epic #1453.

Global Constraints

  • Work on branch feat/classic-temples in its own git worktree off origin/master (superpowers:using-git-worktrees), never in the shared checkout. The PR's Tests check is the gate.
  • Use chemx only. While working, run the one spec you touched: chemx test tests/unit/<Name>.spec.ts. Never use raw pnpm test|typecheck|lint|jest or npx jest. Don't run chemx typecheck, chemx lint or a build during a wave; the lead runs them once after the wave. Don't run chemx verify or a bare chemx test locally; CI runs them on the PR.
  • Builders read their brief with chemx team task show <id> (the lead pastes it into the task's comments) and report with chemx team task comment.
  • Temple ids, names, regions and themes don't change (wind-temple, earth-temple, …). Theme = id.replace("-temple", "") against BEAST_LIBRARY[].theme.
  • Game rules go in pure modules under src/lib/services/** (or src/lib/engine/**) with jest specs; screens stay thin. New components use <script setup lang="ts">; pages keep defineComponent with a sibling .ts/.scss.
  • In-world text comes from the copy catalogue (src/lib/services/copy/catalog/, three voices: simple, adventure, classic) through useCopy(). In-world prompts use useRevealPrompt(), never Ionic alerts. Sounds use play$fx(<key of src/assets/fx/index.ts>).
  • Original beasts 001–093 are family art: never overwrite or re-point them.
  • Quest difficulty is Fibonacci: must-clear monsters 1–2, minibosses 3–5, boss 8–13. Schedules follow SCHEDULES in tools/temples/regenerate.ts (daily 19:00, am 08:00, bed 20:30, school weekdays, weekly 18:00, monthly custom).
  • Every door into a boss room is a Boss door (isBossRoom in authoredDoors.ts). Bombs never open locked doors.
  • Commits are small, one per task, and end with Co-Authored-By: Claude Opus 5.5 <[email protected]> (or the builder's own model line).

Review Focus

  1. A family copy refreshed under a hero mid-run. The saved position may be a wall in the new layout, or the saved openedChests and keys may belong to the old one. The hero must start at the entrance with an empty run, never stuck in a wall. Pinned in Task 7.
  2. A 2Γ—2 LTTP hall with enemies in several screens. One LTTP room holds at most one fight; otherwise one shutter hall would seal three cells separately. Pinned in Task 7 (the dress rule and its test).
  3. One-way LTTP passages and extra entrances. Ledges and drops are one-way, and Skull Woods, Desert Palace and Turtle Rock have several overworld doors. Our sides are two-way and a temple has one entrance, so a converted temple must never leave a part reachable only from outside. Pinned in Task 5 (the converter's teleport rule) and Task 7 (the audit reaches every fight and chest).
  4. A drop in a room whose chore is someone else's, or that comes back tomorrow. The drop lies there for the hero with no fight, and never drops twice in one run. Pinned in Task 3.
  5. Arriving on a floor with nothing to wake (by pit or teleport, or as a Game Master). No reveal and no wake. The memory is still written once, so a later arrival doesn't recompute. Pinned in Task 4.

Task 1: Drops and Bombs in the grants and the solver (board #1455, first half)

Files:

  • Modify: src/lib/engine/core/dungeons/types.ts (RoomContent), src/lib/services/temples/roomGrants.ts, src/lib/engine/dungeons/generator/templeGraph.ts, src/lib/engine/dungeons/generator/templeSolver.ts, src/lib/services/temples/templeAudit.ts:201-204, src/app/Admin/XpCompendium/views/XpTemples/hooks/templeData/dungeonCounts.ts:64-66
  • Create: src/lib/services/temples/fightDrops.ts
  • Test: tests/unit/FightDrops.spec.ts (new), tests/unit/TempleSolver.spec.ts, tests/unit/ShippedTemplesAudit.spec.ts (Wind no longer blown by hand)

Interfaces:

  • Produces:

    • export type RoomDrops = Pick<RoomContent, 'chest' | 'dungeon' | 'items' | 'gold'> and RoomContent.drops?: RoomDrops (types.ts).
    • In fightDrops.ts:
      • export const FIGHT_ROOM_TYPES: readonly string[] = ['monster', 'miniboss', 'boss']
      • export const roomDrops = (room: GrantingRoom | null | undefined): RoomDrops | null: the non-empty content.drops of a fight room, else null.
      • export const dropKey = (level: string, position: readonly number[]): string: ${roomPositionKey(level, position)}:drop.
      • export const BOMB_ITEM = 'Bomb'
    • In roomGrants.ts:
      • smallKeysGranted and grantsBossKey also count roomDrops(room), with ids normalised by dungeonItemId.
      • export const grantedDungeonItems = (room): string[]: the chest's dungeon item plus the drop's.
      • export const isBombSource = (room): boolean: a shop room, or a loot chest or fight drop whose items include BOMB_ITEM.
      • chestDungeonItem stays chest-only.
    • templeGraph.ts: LockKind gains "bomb". A side drawn bombable becomes a door with lock: "bomb"; a side drawn wall stays no door. GraphRoom.bombs: boolean = isBombSource(room).
    • templeSolver.ts: a bomb door opens once the hero's region has reached any room with bombs.
  • Step 1: Write failing tests in tests/unit/FightDrops.spec.ts:

    • roomDrops({ type: 'loot', content: { drops: { chest: 'dungeon', dungeon: 'small-key' } } }) is null.
    • The same drops on type: 'monster' return them.
    • dropKey('1F', [2, 3]) is '1F:2,3:drop'.
    • smallKeysGranted of a monster with drop small-key is 1.
    • grantsBossKey of a miniboss with drop master-key is true.
    • A miniboss with content.dungeon: 'master-key' (no drops) still grants nothing (MinibossBossKey.spec stays green).
    • grantedDungeonItems of a boss dropping map is ['map'].
    • isBombSource({ type: 'shop' }) and isBombSource({ type: 'loot', content: { chest: 'consumable', items: ['Bomb'] } }) are true; isBombSource({ type: 'empty' }) is false.

    In TempleSolver.spec.ts:

    • "a key dropped by a monster opens the small lock": a 1Γ—4 floor, entrance β†’ monster (drop small key) β†’ locked side β†’ boss β†’ master room. solveTemple(layout).solvable is true.
    • "a cracked wall opens once a Bomb chest is reached, never without one": the same floor with the only way to the boss through a bombable side. Solvable with a Bomb chest before the wall, not solvable without it.
  • Step 2: Run chemx test tests/unit/FightDrops.spec.ts and chemx test tests/unit/TempleSolver.spec.ts. Expected: FAIL.

  • Step 3: Implement the interfaces above. Update the header notes of roomGrants.ts and linkNeighbours (fight rooms can hand out keys through drops; a cracked wall is a door opened by a Bomb found inside the temple). The audit and dungeonCounts count maps and compasses with grantedDungeonItems.

  • Step 4: Update ShippedTemplesAudit.spec.ts: the Wind Temple is read as shipped, no wall blown by hand, since its shop is a Bomb source. Fix the file's header comment to match.

  • Step 5: Run both specs above plus chemx test tests/unit/MinibossBossKey.spec.ts, chemx test tests/unit/templeAudit.spec.ts and chemx test tests/unit/ShippedTemplesAudit.spec.ts. Expected: PASS.

  • Step 6: Commit feat(temples): fight rooms can drop keys, and a Bomb found inside a temple opens its cracked walls.

Task 2: The Temple Creator's Drops picker (board #1455, second half)

Files:

  • Create: src/app/Admin/XpCompendium/views/XpTemples/components/XpTempleCreator/components/RoomDropsConfig.vue
  • Modify: .../XpTempleCreator/components/index.ts, .../XpTempleCreator/XpRoomEditorPage.vue (next to RoomMonsterConfig, ~line 61)
  • Test: .../XpTempleCreator/components/__tests__/RoomDropsConfig.spec.ts

Interfaces:

  • Consumes: RoomDrops (Task 1); DUNGEON_ITEMS and CONSUMABLE_ITEMS from ../hooks (the lists RoomLootConfig.vue shows).

  • Produces: RoomDropsConfig, which takes props { drops: RoomDrops | null } and emits update:drops with RoomDrops | null. It's mounted for room types in FIGHT_ROOM_TYPES with v-model:drops bound to roomData.content.drops.

  • Step 1: Write failing tests:

    • Picking "Small Key" emits { chest: 'dungeon', dungeon: 'small-key' }.
    • Picking "None" with no gold or items emits null.
    • Typing gold 30 onto a key drop emits { chest: 'dungeon', dungeon: 'small-key', gold: 30 }.
    • Ticking Bomb emits items: ['Bomb'].
  • Step 2: Run chemx test <that spec>. Expected: FAIL.

  • Step 3: Implement a "Drops" section with the dungeon-item row (None first), the consumables row including Bomb, and a gold field, styled like RoomLootConfig.vue. Mount it in the room editor for fight rooms, both classic and quest rooms.

  • Step 4: Run the spec. Expected: PASS.

  • Step 5: Commit feat(temple-creator): fight rooms pick what their beast drops.

Task 3: Taking a drop in play (board #1455, play)

Files:

  • Modify:
    • src/lib/engine/core/dungeons/templeSystem/chests.ts, src/lib/engine/core/dungeons/TempleSystem.ts
    • src/lib/services/temples/RoomInteractions.ts (the monster/miniboss/boss case, ~lines 222-260), src/lib/services/temples/roomInteractions/vocabulary.ts (RoomIntent, RoomSheet)
    • src/hooks/temples/useTemple.ts (claimChestItems ~393), src/app/Console/TempleGrounds/templeGrounds/templeReveals.ts (useChestReveal), src/app/Console/TempleGrounds/templeGrounds/useRoomOutcomes.ts:211
  • Test: tests/unit/RoomInteractions.spec.ts, tests/unit/FightDrops.spec.ts

Interfaces:

  • Consumes: roomDrops, dropKey, BOMB_ITEM (Task 1); chestLoot (ChestLoot.ts).

  • Produces:

    • markDropTaken(runs, templeId, level, position): void and isDropTaken(runs, templeId, level, position): boolean in chests.ts, filed in TempleState.openedChests under dropKey. TempleSystem has public wrappers with the same names.
    • RoomIntent gains 'takeDrop'; RoomSheet gains drop?: 'waiting'.
    • useTemple: dropContents: ComputedRef<ChestLoot[]> and claimDropItems(selected: string[]): void.
    • useChestReveal: showDropReveal(): void; the reveal's Take routes to claimDropItems while a drop is shown.
  • Step 1: Write failing tests in RoomInteractions.spec.ts, each a monster quest room with a small-key drop:

    • Status lurking: no "Take it" action and no drop.
    • Status cleared: a "Take it" action with intent takeDrop and drop: 'waiting'.
    • Status notYours and status away: "Take it" too.
    • A classic room not yet cleared: no "Take it".
    • After markDropTaken: no "Take it", and "Search the room" is still offered. The keys are separate: isRoomSpent stays false.

    In FightDrops.spec.ts, with TempleSystem:

    • A blood-moon restart (openedChests cleared) makes isDropTaken false again.
    • A daily quest restoring the room does not.
  • Step 2: Run chemx test tests/unit/RoomInteractions.spec.ts. Expected: FAIL.

  • Step 3: Implement.

    • The sheet: offers the drop when roomDrops(room) && !isDropTaken(...) && !fightIsLive. The fight is live for a quest room when questWords.fight; for a classic room, while !isRoomCleared. The sheet line is "Something glints where the beast fell." in the vocabulary file's own style.
    • claimDropItems: hands items out exactly as claimChestItems does, through one extracted local handOutItems(selected), then calls markDropTaken.
      • A drop is taken whole: if any restorative or Bomb has no room in the bag or storage, nothing is taken and the message says the bag is full.
      • A Bomb from a chest or a drop goes into the bag as { name: 'Bomb', kind: 'consumable', faIcon: 'bomb', qty: 1 }, as the shop does. claimChestItems gains that too.
  • Step 4: Run both specs. Expected: PASS.

  • Step 5: Commit feat(temples): a beaten beast drops what its room holds, and it waits there for a hero with no fight.

Task 4: Wake floor by floor (board #1468)

Files:

  • Modify: src/lib/services/temples/wakeOnEntry.ts, src/app/Console/TempleGrounds/TempleGrounds.ts (~line 375), src/app/Console/TempleGrounds/TempleGrounds.vue (mount XpRevealPrompt if absent, as the buildings do)
  • Create: src/lib/services/copy/catalog/temple.ts (registered in catalog/index.ts)
  • Test: tests/unit/wakeOnEntry.spec.ts

Interfaces:

  • Produces:

    • export const wakeKey = (templeId: string, floor: string): string returns ${templeId}@${floor}.
    • export const entranceFloorOf = (layout: PlayedLayout): string: the floor named by entrance[0], or the only floor.
    • wasWokenFor(effects, templeId, floor, entranceFloor): boolean: true for wakeKey, and for the legacy bare templeId when floor === entranceFloor.
    • rememberWake(effects, templeId, floor, now).
    • EntryWakeInput.floor: string. The plan only reads monsterTemplateCells(layout).filter(cell => cell.floor === floor).
    • wakeTempleOnEntry(templeId, heroId, deps?, now = new Date(), floor?): floor defaults to entranceFloorOf. EntryWakeDone gains floor: string and stirred: number (quests woken plus quests joined).
    • Copy key temple.floorWake with params { count, floor }.
  • Step 1: Write failing tests in wakeOnEntry.spec.ts on a two-floor layout:

    • The first entry wakes only 1F's beasts.
    • Arriving on 2F (floor: '2F') wakes 2F's.
    • A second arrival on 2F wakes nothing (alreadyWoken).
    • A legacy templesWokenAt: { 'storm-temple': … } counts as 1F woken but not 2F.
    • A one-floor temple wakes every monster room on entry (today's behaviour).
    • A floor with no monster rooms gives stirred: 0 and still writes its key.
    • A Game Master profile wakes nothing.
  • Step 2: Run chemx test tests/unit/wakeOnEntry.spec.ts. Expected: FAIL.

  • Step 3: Implement the interfaces.

    • In TempleGrounds.ts, keep the mount-time wake. Add a watch on the run's current level that calls wakeTempleOnEntry(props.temple, hero, undefined, new Date(), level) after whenProgressReady.
    • When stirred > 0: refreshQuestRooms, then useRevealPrompt().ask with the temple.floorWake lines and one "Onward" choice.
  • Step 4: Run the spec. Expected: PASS.

  • Step 5: Commit feat(temples): beasts wake floor by floor: the entrance floor on entry, each other floor on first arrival.

Task 5: Sources, schema and the LTTP converter (board #1456)

Files:

  • Create:
    • tools/temples/classic/sources/: Doors.py, Regions.py and EnemyList.py from aerinon/ALttPDoorRandomizer@6f390947159b6d2162b52a60aebfd969ccf9b922 (EnemyList.py from source/dungeon/); LoZ_1.dot from TheVGLC/TheVGLC@0e86a8f31f20ecad4eaa5741ff061af88767f7fb; both MIT licence texts; and SOURCES.md with the URLs and commit ids.
    • tools/temples/classic/classicDungeon.ts: the schema types.
    • tools/temples/classic/convert.py
    • tools/temples/classic/overrides/<id>.json (only where needed)
    • tools/temples/classic/dungeons/<id>.json for the 8 LTTP temples
  • Test: tests/unit/ClassicDungeonJson.spec.ts

Interfaces:

  • Produces (in classicDungeon.ts; the converter writes exactly this):
export type ClassicSide = "open" | "small-key" | "big-key" | "bomb" | "shutter" | "item" | "wall";
export interface ClassicLink { to: string; row: number; col: number } // floor name, then the arrival cell
export interface ClassicCell {
  row: number; col: number;
  room: string; // the LTTP room this screen belongs to; cells of one hall share it
  sides: Record<"north" | "east" | "south" | "west", ClassicSide>;
  enemies: number;
  chest?: "map" | "compass" | "big-key" | "big-chest" | "small-key" | "other";
  keyDrop?: "small" | "big";
  potKey?: true; boss?: true; puzzle?: true; shutter?: true; entrance?: true;
  stairs?: ClassicLink; hole?: ClassicLink; teleport?: ClassicLink;
}
export interface ClassicFloor { name: string; rows: number; cols: number; cells: ClassicCell[] }
export interface ClassicDungeon {
  id: string; source: string; // "LTTP Tower of Hera"
  floors: ClassicFloor[]; // bottom to top
  report: { cellsPerFloor: Record<string, number>; mustClear: number; side: number; smallKeys: number; smallLocks: number };
}

The temples, as id: randomizer dungeon name:

  • storm-temple: "Tower of Hera"
  • earth-temple: "Skull Woods"
  • light-temple: "Desert Palace"
  • water-temple: "Swamp Palace"
  • shadow-temple: "Palace of Darkness"
  • ice-temple: "Ice Palace"
  • fire-temple: "Turtle Rock"
  • moon-temple: "Ganons Tower"

Run with python3 tools/temples/classic/convert.py [id ...] (stdlib only). It writes the JSON, prints the report, and exits 1 naming any area it cannot place.

  • Step 1: Write the failing spec ClassicDungeonJson.spec.ts. It loads every dungeons/*.json and asserts:
    • cells fit inside rows Γ— cols and no position repeats on a floor;
    • every side agrees with its neighbour's opposite side, and a side with no neighbour is wall;
    • exactly one entrance and one boss;
    • exactly one Big Key (chest: 'big-key' or keyDrop: 'big'), one big-chest, one map and one compass;
    • every stairs, hole and teleport names a real cell;
    • the cells of one room are connected through open sides;
    • report.smallKeys equals the cells with chest: 'small-key', keyDrop: 'small' or potKey;
    • every cell is reachable from the entrance through non-wall sides, stairs, holes and teleports (Review Focus 3).
  • Step 2: Run chemx test tests/unit/ClassicDungeonJson.spec.ts. Expected: FAIL (no files).
  • Step 3: Implement convert.py. These rules are decided; the code is yours:
    • Cells: a supertile id gives row id >> 4 and column id & 0xf. Each screen is one cell at (2Β·row + top/bottom, 2Β·col + left/right).
    • Door slots: Left/Top is the first half and Right/Bot the second. A Mid door takes the first half, on both ends.
    • Rooms: areas (Regions.py) joined by Intr doors share a screen set. Screens of one area are joined by open sides; screens of different areas are split by wall. An enemy's screen is its tile x/y split at 0x20. A chest with no screen sits in its area's first screen.
    • Door kinds: small_key() β†’ small-key; big-key doors β†’ big-key; bomb and dash walls β†’ bomb; .trap(...) β†’ shutter, which sets shutter on the room; hookshot, boots, torch and flipper gaps β†’ item; anything else β†’ open.
    • Floors: connected sets of plain doors share a floor; Up/Dn stairs move Β±1; drop-down exits move down. Name the floors B2, B1, 1F, 2F… with the entrance's floor as 1F, and trim each floor's grid to its cells.
    • Several entrances (Review Focus 3): the main door is the entrance. Each other overworld entrance becomes a teleport from its cell to the main entrance cell, and the other way. A hole dropping two or more floors is a teleport.
    • Overrides: overrides/<id>.json may pin a screen for an area or chest, or add or remove a side. The report lists every override it applied.
  • Step 4: Run the converter for all eight, then the spec. Expected: PASS. Paste the report into the board task as a comment.
  • Step 5: Commit feat(tools): pinned LTTP sources and a converter that writes one JSON per dungeon.

Task 6: The Eagle as JSON (board #1456, Wind half)

Files:

  • Modify: tools/temples/classic/convert.py (the wind-temple path)
  • Create: tools/temples/classic/overrides/wind-temple.json, tools/temples/classic/dungeons/wind-temple.json
  • Test: tests/unit/ClassicDungeonJson.spec.ts

Interfaces:

  • Consumes: the ClassicDungeon schema (Task 5).

  • Produces: dungeons/wind-temple.json, one floor 1F of 6Γ—6. Its 17 Eagle cells are the non-wall cells of today's wind-temple.ts, minus the owner's 4: (3,5), (4,4), (4,5), (5,5).

  • Step 1: Write failing tests:

    • The Wind JSON has exactly the 17 Eagle cells.
    • The entrance is at (5,2), the boss at (1,4) and the Triforce cell (the future Master Room) at (1,5).
    • It has 6 small keys and 6 small-key sides.
    • There's one map and one compass.
  • Step 2: Run. Expected: FAIL.

  • Step 3: Implement.

    • Match LoZ_1.dot's nodes onto the Eagle cells by search, keeping every non-stairway edge between orthogonal neighbours. The anchors are: s = entrance, b = boss, t = Triforce.
    • The bow's basement links to the push-block room at (0,1) as a teleport.
    • Tags map as follows: k with e β†’ keyDrop: 'small'; k alone β†’ chest: 'small-key'; I β†’ big-chest; i β†’ map or compass in graph order. Edge labels: k β†’ small-key, b β†’ bomb, l β†’ shutter.
    • A node that fits no cell stops the run and is named. It's never guessed.
  • Step 4: Run. Expected: PASS.

  • Step 5: Commit feat(tools): the Eagle's rooms and doors as JSON, matched from Zelda 1 Level 1.

Task 7: Dress step, shared plan tools, and the Storm Temple from Tower of Hera (board #1457)

Files:

  • Create:
    • tools/temples/questPlan.ts: Schedule, PlanQuest, SCHEDULES and templateFor, moved out of regenerate.ts.
    • tools/temples/renderModule.ts: literal, camel, and renderModule(id, temple, header: string[]), moved out of regenerate.ts.
    • tools/temples/classic/dress.ts
    • tests/unit/ClassicTemples.spec.ts
    • tests/e2e/specs/classic-hera.spec.js
  • Modify:
    • tools/temples/regenerate.ts (imports the moved pieces; PLANS drops storm-temple)
    • src/lib/engine/temples/storm-temple.ts (generated)
    • tests/unit/ShippedTemplesAudit.spec.ts (split the regenerated block from a new classic block)
    • the seed refresh in src/lib/engine/core/dungeons/importAllTemples.ts if Step 1's refresh test fails

Interfaces:

  • Consumes: ClassicDungeon (Task 5); RoomDrops, isBombSource (Task 1); generateTemple's GeneratedTemple shape (rooms keyed by token, maze per floor).
  • Produces, from dress.ts:
    • interface ClassicPlan { name: string; theme: string; element: string; relic: string; shop?: string; trap: [string, number]; puzzle: string; mustClear: PlanQuest[]; minibosses: PlanQuest[]; boss: PlanQuest }
    • CLASSIC_PLANS: Record<string, ClassicPlan>, CLASSIC_IDS: string[]
    • dressClassic(id: string): GeneratedTemple
    • renderClassicModule(id: string, temple: GeneratedTemple): string, with a header naming tools/temples/classic/dress.ts and the dungeon's source
    • classicReport(id): { mustClear: number; repeats: string[] }, the beasts a must-clear room had to repeat
    • Run with esbuild exactly as regenerate.ts is: … --outfile=/tmp/dress-temples.cjs && node /tmp/dress-temples.cjs [id]

Dress rules (from the spec's Dressing table):

  • Fights:

    • One fight per LTTP room: the screen with the most enemies (Review Focus 2). A shutter or enemy key-drop room is a must-clear monster with lockOnEnter; the key goes in drops, and a big key drop means drops: { chest: 'dungeon', dungeon: 'master-key' }.
    • A shutter room whose other side leads straight to the Big Key chest or the Big Chest is a miniboss.
    • If the plan has more minibosses than that rule finds, the must-clear rooms nearest the Big Key chest, then the Big Chest, become the extra minibosses.
    • Other enemy rooms are side monsters without a seal.
    • The boss cell is boss with lockOnEnter. master (the Pegasus chest) goes in a free cell next to the boss, growing that floor's grid by one row or column if none is free.
  • Chests:

    • big-key β†’ loot { chest: 'dungeon', dungeon: 'master-key' }.
    • big-chest β†’ loot { chest: 'dungeon', dungeon: plan.relic }.
    • map, compass and small-key β†’ loot dungeon chests of the same item.
    • potKey β†’ a key room.
    • other β†’ loot { chest: 'gold', gold: 25 }.
  • Other cells:

    • puzzle β†’ puzzle { puzzleType: plan.puzzle }.
    • stairs and hole β†’ stairs-up/stairs-down/pit with targetLevel/targetCoords; teleport β†’ a teleport pair.
    • Empty cells, nearest the entrance first: one shop (with specialItem: plan.shop), one save, one camp { healthPoints: 15 } per floor, and the rest empty.
  • Sides go into sides: open β†’ door, small-key β†’ locked, big-key β†’ masterlocked (and every side into the boss cell), bomb β†’ bombable, shutter β†’ door, item β†’ door, wall β†’ wall.

  • Bomb chests: when solveTemple fails because of bombable sides and the temple has no reachable Bomb source, the nearest reachable empty cell before each such wall gets loot { chest: 'consumable', items: ['Bomb'] }.

  • Chores:

    • mustClear templates go to must-clear monster rooms in path order from the entrance, easiest first.
    • Side rooms take the beasts of their floor's must-clear rooms in turn; a floor with none borrows the nearest floor's.
    • A plan short of beasts repeats one and lists it in classicReport().repeats.
    • Today's quests in regenerate.ts PLANS[id].quests must all stay in the plan, so starter quests still fill rooms.
  • Step 1: Write failing tests in ClassicTemples.spec.ts, over CLASSIC_IDS:

    • the file is exactly renderClassicModule(id, dressClassic(id));
    • every JSON cell is a room at the same position, and every side matches the JSON per the side rules;
    • each LTTP room holds at most one fight;
    • every must-clear room has lockOnEnter;
    • must-clear beasts are distinct and of the theme, and side-room beasts come from must-clear rooms;
    • every key drop sits in drops;
    • every side into the boss cell is masterlocked;
    • the audit passes and solveTemple(...).solvable holds with no wall blown by hand;
    • every chest and every fight cell is reachable (Review Focus 3).

    In ShippedTemplesAudit.spec.ts:

    • split "the eight regenerated temples" into the remaining SHIPPED_IDS and a classic block;
    • the size checks read templeSize from the dressed rooms (β‰₯ 21);
    • "used once" applies to must-clear rooms only.

    A refresh test (Review Focus 1): a stored, unedited copy of the old storm layout, with a hero's saved run positioned on a cell that is a wall in the new one. After seed sync and whenProgressReady, the hero stands on the entrance with no keys and empty openedChests.

  • Step 2: Run chemx test tests/unit/ClassicTemples.spec.ts. Expected: FAIL.

  • Step 3: Move questPlan.ts and renderModule.ts out of regenerate.ts. Then run chemx test tests/unit/ShippedTemplesAudit.spec.ts: the eight regenerated temples must still render byte-for-byte.

  • Step 4: Implement dress.ts and the Storm plan. The theme is bedroom and clutter, the relic is Storm's current relic chest item, and the plan keeps every beast in today's Storm plan. Generate storm-temple.ts and drop storm-temple from PLANS.

  • Step 5: Run ClassicTemples.spec.ts, ShippedTemplesAudit.spec.ts and ClassicDungeonJson.spec.ts. Expected: PASS.

  • Step 6: Write classic-hera.spec.js:

    • cy.seedWorld(), then log a hero in with window.__xp.loginUser.
    • Open the Storm Temple and walk solveTemple's order with the arrow controls, winning each sealed fight through the battle seam the quest-rooms.spec.js e2e uses.
    • Assert the Master Room's Pegasus chest opens.
    • Run: pnpm vue-cli-service test:e2e --headless --browser chrome --url http://localhost:<worktree port> --spec tests/e2e/specs/classic-hera.spec.js against the worktree's own server, which the builder stops when done. Expected: PASS.
  • Step 7: Commit feat(temples): the Storm Temple follows the Tower of Hera, dressed from its JSON.

Task 8: The other seven LTTP temples (boards #1458–#1464, one commit each)

Files per temple: tools/temples/classic/dress.ts (its CLASSIC_PLANS entry), tools/temples/regenerate.ts (PLANS loses it), src/lib/engine/temples/<id>.ts (generated), tools/temples/classic/overrides/<id>.json if the converter needs one.

Interfaces: consumes Task 7's ClassicPlan, dressClassic and renderClassicModule; produces nothing new.

Order (smallest first): Earth/Skull Woods, Light/Desert Palace, Water/Swamp Palace, Shadow/Palace of Darkness, Ice/Ice Palace, Fire/Turtle Rock, Moon/Ganon's Tower.

  • Step 1: Add the plan:
    • the theme and relic from today's PLANS entry;
    • mustClear sized to the report's count, with distinct beasts of the theme, keeping every beast of today's plan;
    • minibosses at 3–5, and the boss at 8 (13 for Fire, Ice) as today.
  • Step 2: Generate the temple. Then run chemx test tests/unit/ClassicTemples.spec.ts and chemx test tests/unit/ShippedTemplesAudit.spec.ts. Expected: PASS.
  • Step 3: Paste classicReport(id).repeats into the board task. For Moon, also paste the floor count and must-clear count: the owner decides on the Misery Mire fallback from those numbers.
  • Step 4: Commit feat(temples): the <Name> Temple follows <Dungeon>.

Task 9: Wind's 1:1 pass (board #1465)

Files:

  • Modify: tools/temples/classic/dress.ts (a wind-temple plan), tools/temples/classic/overrides/wind-temple.json
  • Generated: src/lib/engine/temples/wind-temple.ts
  • Test: tests/unit/WindTemple.spec.ts, tests/unit/ClassicTemples.spec.ts (Wind joins CLASSIC_IDS)

Interfaces: consumes Task 6's Wind JSON and Task 7's dress step.

  • Step 1: Rewrite the failing tests in WindTemple.spec.ts:
    • the 17 Eagle cells hold the JSON's contents: 6 key drops or key chests, map, compass, big-chest β†’ Wind's relic, the boss at (1,4) and the Master Room at (1,5);
    • the old man's cell (2,0) is the shop;
    • the owner's 4 rooms (3,5), (4,4), (4,5), (5,5) are exactly today's rooms: the Boss Key chest, the key room, the cobweb-widow miniboss, the teleport;
    • every beast is a wind beast, and the plan keeps all 9 of today's quests. dust-bunny-king (3) is the miniboss in the Eagle's must-clear room nearest the boss door, placed by the override.
  • Step 2: Run chemx test tests/unit/WindTemple.spec.ts. Expected: FAIL.
  • Step 3: Implement the Wind plan. The override re-adds the owner's 4 rooms and their sides verbatim. The Big Key stays in the owner's east chest, so no Big Key comes from Zelda (Level 1 has none).
  • Step 4: Run WindTemple.spec.ts, ClassicTemples.spec.ts and ShippedTemplesAudit.spec.ts. Expected: PASS.
  • Step 5: Commit feat(temples): the Wind Temple's rooms follow Zelda's Eagle one to one.

Task 10: Beast art list (board #1466)

Files: none in the repo. It's one new board task.

  • Step 1: Collect classicReport(id).repeats for all nine temples.
  • Step 2: chemx team task add --title="Paint the classic temples' missing beasts" --desc="<per theme: count and the chores from docs/Quest-Rooms-Spec.md 'Library gaps'>" --needs=light --sprint=classic-temples --parent=1453. Use the --flag=value form only. Say in the description that beasts 001–093 are never touched.
  • Step 3: Close #1466 with --as=<@handle>.

Waves (for the lead)

Wave Tasks (board) Builders
1 Task 1 (#1455), Task 4 (#1468), Task 5 (#1456) Sonnet, Sonnet, Fable (deep)
2 Task 2, Task 3 (#1455), Task 6 (#1456, edits convert.py after Task 5) Sonnet, Sonnet, Sonnet
3 Task 7 (#1457) Fable (deep)
4 Task 8 Γ—7 (#1458–#1464), Task 9 (#1465), Task 10 (#1466) up to 5 Sonnet at once; Task 8's temples one per builder

After each wave: the lead runs chemx typecheck and chemx lint once, commits, pushes the branch, and reads the PR's Tests check.

⚠️ **GitHub.com Fallback** ⚠️