2026 10 19 classic dungeon temples - xopherdeep/do-it-for-the-xp GitHub Wiki
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.
- Work on branch
feat/classic-templesin its own git worktree offorigin/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 rawpnpm test|typecheck|lint|jestornpx jest. Don't runchemx typecheck,chemx lintor a build during a wave; the lead runs them once after the wave. Don't runchemx verifyor a barechemx testlocally; 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 withchemx team task comment. - Temple ids, names, regions and themes don't change (
wind-temple,earth-temple, β¦). Theme =id.replace("-temple", "")againstBEAST_LIBRARY[].theme. - Game rules go in pure modules under
src/lib/services/**(orsrc/lib/engine/**) with jest specs; screens stay thin. New components use<script setup lang="ts">; pages keepdefineComponentwith a sibling.ts/.scss. - In-world text comes from the copy catalogue (
src/lib/services/copy/catalog/, three voices: simple, adventure, classic) throughuseCopy(). In-world prompts useuseRevealPrompt(), never Ionic alerts. Sounds useplay$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
SCHEDULESintools/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 (
isBossRoominauthoredDoors.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).
-
A family copy refreshed under a hero mid-run. The saved position may be a wall in the new layout, or the saved
openedChestsand 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. - 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).
- 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).
- 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.
- 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.
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'>andRoomContent.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-emptycontent.dropsof a fight room, elsenull. -
export const dropKey = (level: string, position: readonly number[]): string:${roomPositionKey(level, position)}:drop. export const BOMB_ITEM = 'Bomb'
- In
roomGrants.ts:-
smallKeysGrantedandgrantsBossKeyalso countroomDrops(room), with ids normalised bydungeonItemId. -
export const grantedDungeonItems = (room): string[]: the chest's dungeon item plus the drop's. -
export const isBombSource = (room): boolean: ashoproom, or a loot chest or fight drop whoseitemsincludeBOMB_ITEM. -
chestDungeonItemstays chest-only.
-
-
templeGraph.ts:LockKindgains"bomb". A side drawnbombablebecomes a door withlock: "bomb"; a side drawnwallstays no door.GraphRoom.bombs: boolean=isBombSource(room). -
templeSolver.ts: abombdoor opens once the hero's region has reached any room withbombs.
-
-
Step 1: Write failing tests in
tests/unit/FightDrops.spec.ts:-
roomDrops({ type: 'loot', content: { drops: { chest: 'dungeon', dungeon: 'small-key' } } })isnull. - The same drops on
type: 'monster'return them. -
dropKey('1F', [2, 3])is'1F:2,3:drop'. -
smallKeysGrantedof a monster with dropsmall-keyis1. -
grantsBossKeyof a miniboss with dropmaster-keyistrue. - A miniboss with
content.dungeon: 'master-key'(no drops) still grants nothing (MinibossBossKey.spec stays green). -
grantedDungeonItemsof a boss droppingmapis['map']. -
isBombSource({ type: 'shop' })andisBombSource({ type: 'loot', content: { chest: 'consumable', items: ['Bomb'] } })aretrue;isBombSource({ type: 'empty' })isfalse.
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).solvableistrue. -
"a cracked wall opens once a Bomb chest is reached, never without one": the same floor with the only way to the boss through abombableside. Solvable with a Bomb chest before the wall, not solvable without it.
-
-
Step 2: Run
chemx test tests/unit/FightDrops.spec.tsandchemx test tests/unit/TempleSolver.spec.ts. Expected: FAIL. -
Step 3: Implement the interfaces above. Update the header notes of
roomGrants.tsandlinkNeighbours(fight rooms can hand out keys through drops; a cracked wall is a door opened by a Bomb found inside the temple). The audit anddungeonCountscount maps and compasses withgrantedDungeonItems. -
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.tsandchemx 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.
Files:
- Create:
src/app/Admin/XpCompendium/views/XpTemples/components/XpTempleCreator/components/RoomDropsConfig.vue - Modify:
.../XpTempleCreator/components/index.ts,.../XpTempleCreator/XpRoomEditorPage.vue(next toRoomMonsterConfig, ~line 61) - Test:
.../XpTempleCreator/components/__tests__/RoomDropsConfig.spec.ts
Interfaces:
-
Consumes:
RoomDrops(Task 1);DUNGEON_ITEMSandCONSUMABLE_ITEMSfrom../hooks(the listsRoomLootConfig.vueshows). -
Produces:
RoomDropsConfig, which takes props{ drops: RoomDrops | null }and emitsupdate:dropswithRoomDrops | null. It's mounted for room types inFIGHT_ROOM_TYPESwithv-model:dropsbound toroomData.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
30onto a key drop emits{ chest: 'dungeon', dungeon: 'small-key', gold: 30 }. - Ticking
Bombemitsitems: ['Bomb'].
- Picking "Small Key" emits
-
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.
Files:
- Modify:
-
src/lib/engine/core/dungeons/templeSystem/chests.ts,src/lib/engine/core/dungeons/TempleSystem.ts -
src/lib/services/temples/RoomInteractions.ts(themonster/miniboss/bosscase, ~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): voidandisDropTaken(runs, templeId, level, position): booleaninchests.ts, filed inTempleState.openedChestsunderdropKey.TempleSystemhas public wrappers with the same names. -
RoomIntentgains'takeDrop';RoomSheetgainsdrop?: 'waiting'. -
useTemple:dropContents: ComputedRef<ChestLoot[]>andclaimDropItems(selected: string[]): void. -
useChestReveal:showDropReveal(): void; the reveal's Take routes toclaimDropItemswhile 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 nodrop. - Status
cleared: a "Take it" action with intenttakeDropanddrop: 'waiting'. - Status
notYoursand statusaway: "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:isRoomSpentstaysfalse.
In
FightDrops.spec.ts, withTempleSystem:- A blood-moon restart (
openedChestscleared) makesisDropTakenfalse again. - A daily quest restoring the room does not.
- Status
-
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 whenquestWords.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 asclaimChestItemsdoes, through one extracted localhandOutItems(selected), then callsmarkDropTaken.- 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.claimChestItemsgains that too.
-
The sheet: offers the drop when
-
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.
Files:
- Modify:
src/lib/services/temples/wakeOnEntry.ts,src/app/Console/TempleGrounds/TempleGrounds.ts(~line 375),src/app/Console/TempleGrounds/TempleGrounds.vue(mountXpRevealPromptif absent, as the buildings do) - Create:
src/lib/services/copy/catalog/temple.ts(registered incatalog/index.ts) - Test:
tests/unit/wakeOnEntry.spec.ts
Interfaces:
-
Produces:
-
export const wakeKey = (templeId: string, floor: string): stringreturns${templeId}@${floor}. -
export const entranceFloorOf = (layout: PlayedLayout): string: the floor named byentrance[0], or the only floor. -
wasWokenFor(effects, templeId, floor, entranceFloor): boolean: true forwakeKey, and for the legacy baretempleIdwhenfloor === entranceFloor. -
rememberWake(effects, templeId, floor, now). -
EntryWakeInput.floor: string. The plan only readsmonsterTemplateCells(layout).filter(cell => cell.floor === floor). -
wakeTempleOnEntry(templeId, heroId, deps?, now = new Date(), floor?):floordefaults toentranceFloorOf.EntryWakeDonegainsfloor: stringandstirred: number(quests woken plus quests joined). - Copy key
temple.floorWakewith params{ count, floor }.
-
-
Step 1: Write failing tests in
wakeOnEntry.spec.tson 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: 0and 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 awatchon the run's current level that callswakeTempleOnEntry(props.temple, hero, undefined, new Date(), level)afterwhenProgressReady. - When
stirred > 0:refreshQuestRooms, thenuseRevealPrompt().askwith thetemple.floorWakelines and one "Onward" choice.
- In
-
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.
Files:
- Create:
-
tools/temples/classic/sources/:Doors.py,Regions.pyandEnemyList.pyfromaerinon/ALttPDoorRandomizer@6f390947159b6d2162b52a60aebfd969ccf9b922(EnemyList.pyfromsource/dungeon/);LoZ_1.dotfromTheVGLC/TheVGLC@0e86a8f31f20ecad4eaa5741ff061af88767f7fb; both MIT licence texts; andSOURCES.mdwith 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>.jsonfor 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 everydungeons/*.jsonand asserts:- cells fit inside
rows Γ colsand 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
entranceand oneboss; - exactly one Big Key (
chest: 'big-key'orkeyDrop: 'big'), onebig-chest, onemapand onecompass; - every
stairs,holeandteleportnames a real cell; - the cells of one
roomare connected throughopensides; -
report.smallKeysequals the cells withchest: 'small-key',keyDrop: 'small'orpotKey; - every cell is reachable from the entrance through non-
wallsides, stairs, holes and teleports (Review Focus 3).
- cells fit inside
- 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 >> 4and columnid & 0xf. Each screen is one cell at(2Β·row + top/bottom, 2Β·col + left/right). -
Door slots:
Left/Topis the first half andRight/Botthe second. AMiddoor takes the first half, on both ends. -
Rooms: areas (
Regions.py) joined byIntrdoors share a screen set. Screens of one area are joined byopensides; screens of different areas are split bywall. An enemy's screen is its tile x/y split at0x20. 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 setsshutteron the room; hookshot, boots, torch and flipper gaps βitem; anything else βopen. -
Floors: connected sets of plain doors share a floor;
Up/Dnstairs move Β±1; drop-down exits move down. Name the floorsB2, B1, 1F, 2Fβ¦with the entrance's floor as1F, 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
teleportfrom its cell to the main entrance cell, and the other way. A hole dropping two or more floors is ateleport. -
Overrides:
overrides/<id>.jsonmay pin a screen for an area or chest, or add or remove a side. The report lists every override it applied.
-
Cells: a supertile id gives row
- 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.
Files:
- Modify:
tools/temples/classic/convert.py(thewind-templepath) - Create:
tools/temples/classic/overrides/wind-temple.json,tools/temples/classic/dungeons/wind-temple.json - Test:
tests/unit/ClassicDungeonJson.spec.ts
Interfaces:
-
Consumes: the
ClassicDungeonschema (Task 5). -
Produces:
dungeons/wind-temple.json, one floor1Fof 6Γ6. Its 17 Eagle cells are the non-wall cells of today'swind-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-keysides. - 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:
kwitheβkeyDrop: 'small';kalone β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.
- Match
-
Step 4: Run. Expected: PASS.
-
Step 5: Commit
feat(tools): the Eagle's rooms and doors as JSON, matched from Zelda 1 Level 1.
Files:
- Create:
-
tools/temples/questPlan.ts:Schedule,PlanQuest,SCHEDULESandtemplateFor, moved out ofregenerate.ts. -
tools/temples/renderModule.ts:literal,camel, andrenderModule(id, temple, header: string[]), moved out ofregenerate.ts. tools/temples/classic/dress.tstests/unit/ClassicTemples.spec.tstests/e2e/specs/classic-hera.spec.js
-
- Modify:
-
tools/temples/regenerate.ts(imports the moved pieces;PLANSdropsstorm-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.tsif Step 1's refresh test fails
-
Interfaces:
- Consumes:
ClassicDungeon(Task 5);RoomDrops,isBombSource(Task 1);generateTemple'sGeneratedTempleshape (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 namingtools/temples/classic/dress.tsand the dungeon'ssource -
classicReport(id): { mustClear: number; repeats: string[] }, the beasts a must-clear room had to repeat - Run with esbuild exactly as
regenerate.tsis:β¦ --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-clearmonsterwithlockOnEnter; the key goes indrops, and abigkey drop meansdrops: { 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
bosswithlockOnEnter.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.
- One fight per LTTP
-
Chests:
-
big-keyβloot { chest: 'dungeon', dungeon: 'master-key' }. -
big-chestβloot { chest: 'dungeon', dungeon: plan.relic }. -
map,compassandsmall-keyβ loot dungeon chests of the same item. -
potKeyβ akeyroom. -
otherβloot { chest: 'gold', gold: 25 }.
-
-
Other cells:
-
puzzleβpuzzle { puzzleType: plan.puzzle }. -
stairsandholeβstairs-up/stairs-down/pitwithtargetLevel/targetCoords;teleportβ ateleportpair. - Empty cells, nearest the entrance first: one
shop(withspecialItem: plan.shop), onesave, onecamp { healthPoints: 15 }per floor, and the restempty.
-
-
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
solveTemplefails because ofbombablesides and the temple has no reachable Bomb source, the nearest reachable empty cell before each such wall getsloot { chest: 'consumable', items: ['Bomb'] }. -
Chores:
-
mustCleartemplates 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.tsPLANS[id].questsmust all stay in the plan, so starter quests still fill rooms.
-
-
Step 1: Write failing tests in
ClassicTemples.spec.ts, overCLASSIC_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
roomholds 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(...).solvableholds 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_IDSand a classic block; - the size checks read
templeSizefrom 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 emptyopenedChests. - the file is exactly
-
Step 2: Run
chemx test tests/unit/ClassicTemples.spec.ts. Expected: FAIL. -
Step 3: Move
questPlan.tsandrenderModule.tsout ofregenerate.ts. Then runchemx test tests/unit/ShippedTemplesAudit.spec.ts: the eight regenerated temples must still render byte-for-byte. -
Step 4: Implement
dress.tsand 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. Generatestorm-temple.tsand dropstorm-templefromPLANS. -
Step 5: Run
ClassicTemples.spec.ts,ShippedTemplesAudit.spec.tsandClassicDungeonJson.spec.ts. Expected: PASS. -
Step 6: Write
classic-hera.spec.js:-
cy.seedWorld(), then log a hero in withwindow.__xp.loginUser. - Open the Storm Temple and walk
solveTemple'sorderwith the arrow controls, winning each sealed fight through the battle seam thequest-rooms.spec.jse2e 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.jsagainst 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.
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
PLANSentry; -
mustClearsized 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.
- the theme and relic from today's
- Step 2: Generate the temple. Then run
chemx test tests/unit/ClassicTemples.spec.tsandchemx test tests/unit/ShippedTemplesAudit.spec.ts. Expected: PASS. - Step 3: Paste
classicReport(id).repeatsinto 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>.
Files:
- Modify:
tools/temples/classic/dress.ts(awind-templeplan),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 joinsCLASSIC_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.
- the 17 Eagle cells hold the JSON's contents: 6 key drops or key chests, map, compass,
- 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.tsandShippedTemplesAudit.spec.ts. Expected: PASS. - Step 5: Commit
feat(temples): the Wind Temple's rooms follow Zelda's Eagle one to one.
Files: none in the repo. It's one new board task.
- Step 1: Collect
classicReport(id).repeatsfor 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=valueform only. Say in the description that beasts 001β093 are never touched. - Step 3: Close #1466 with
--as=<@handle>.
| 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.