2026 10 13 moving house temple - 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: The Moving House pack builds "The Old House" as a real floor plan (1 or 2 floors, an optional basement) whose open rooms hold every hero's chore for that room of the house.
Architecture: The quest-room resolver learns a list of quests per room (content.questIds), so every screen that reads rooms through it (run sync, battle loader, quest sheet) works unchanged. The Moving Day builder splits into three pure modules: which cells the house has (houseRooms.ts), where they go on a 9x9 frame (housePlanner.ts), and the façade that turns the plan into a temple record (movingTemple.ts). The setup dialog asks for floors and a basement, and clears every hero's walk through this one temple before loading.
Tech Stack: Vue 3 + Ionic 7, TypeScript, jest 27 (unit), Cypress 8 (e2e, --browser chrome).
Spec: docs/superpowers/specs/2026-10-13-moving-house-temple-design.md
- Gates go through chemx only:
chemx test <spec>,chemx typecheck,chemx lint,chemx verify --build. Never runpnpm typecheck|lint|test|jestraw. - The checkout is shared with other sessions: never
git addbroadly orgit stash. Commit only your own paths from a private index (GIT_INDEX_FILE=$(mktemp) git read-tree HEAD,git update-index --add --cacheinfo 100644,<blob>,<path>withblob=$(git hash-object -w <path>),git write-tree,git commit-tree -p HEAD,git update-ref refs/heads/master <new> <old>), then sync the shared index for those paths. Checkchemx team lock check-staged <paths>first. - A room with a single
questIdbehaves exactly as before: every existing spec passes unchanged. - The temple keeps
id: "gen-move-house",name: "The Old House",themeId: "home",element: "earth",generated: true. - The same answers give the same record; the move date never changes it.
- Floors are listed
B1,1F,2F(only those built); each fits 9x9. - Only the key guard and the van seal (
lockOnEnter: true), and each holds exactly one quest. - Pure modules under
src/lib/services/**carry no storage and no Vue; the one storage helper ismovingTempleReset.ts. - Comments follow the repo: a JSDoc line saying what a thing is and why, plain English, no ticket numbers.
-
A won fight in an open list room: the hero comes back from the battle into a room where their next chore lurks. They expect no second fight on the spot; the next beast is there when they walk in again. (Task 1, test in
TempleQuestRooms.spec.ts.) - The Game Master edits a generated room: picking one quest in the editor replaces the list, and the room is a plain single-quest room from then on. (Task 2.)
- A hero who sits the move out ("out") or a hero name that is empty: the reset still clears their old walk, and a bedroom is never named "'s bedroom". (Tasks 4 and 7.)
- The biggest house the dialog allows (8 bedrooms, 5 bathrooms, 4 guest rooms, every tick, 2 floors and a basement, 6 heroes) fits 9x9 and solves. (Task 6 matrix.)
- A family with only little ones and a kitchen: the sealed kitchen holds the one quest that exists (the daily dishes), and the temple still solves. (Tasks 4 and 6.)
Files:
- Modify:
src/lib/engine/core/dungeons/types.ts:26-53(RoomContent) - Modify:
src/lib/services/temples/questRooms.ts:136-241 - Test:
tests/unit/questRooms.spec.ts,tests/unit/TempleQuestRooms.spec.ts
Interfaces:
-
Produces:
RoomContent.questIds?: string[],RoomContent.name?: string;roomQuestIds(room: Partial<Room> | null | undefined): string[];headlineQuest(room: Partial<Room> | null | undefined, quests: readonly Achievement[]): Achievement | undefined(exported fromquestRooms.ts);isQuestRoomtrue for a non-emptyquestIds. -
Step 1: Write the failing tests in
tests/unit/questRooms.spec.ts(reusemakeQuest,makeItem,room,HERO,MIA):
describe("rooms that hold several quests", () => {
const dishes = makeQuest("dishes", { difficulty: 13, assignee: [MIA] });
const mugs = makeQuest("mugs", { difficulty: 3 });
const pantry = makeQuest("pantry", { difficulty: 8 });
const kitchen = room({ questIds: ["dishes", "pantry", "mugs"] });
it("names its quests in order, and reads a single questId as a list of one", () => {
expect(roomQuestIds(kitchen)).toEqual(["dishes", "pantry", "mugs"]);
expect(roomQuestIds(room({ questId: "q1" }))).toEqual(["q1"]);
expect(roomQuestIds(room({}))).toEqual([]);
expect(isQuestRoom(kitchen)).toBe(true);
expect(isQuestRoom(room({ questIds: [] }))).toBe(false);
});
it("gives each hero the quest that is open for them", () => {
const board = [makeItem(mugs, "open")];
expect(resolveRoomQuest(kitchen, HERO, board, [dishes, pantry, mugs])).toMatchObject({ kind: "quest", quest: { id: "mugs" }, source: "questId" });
});
it("prefers open, then pending, then done, then away; ties go to list order", () => {
// pantry done (cleared), mugs done with a claim waiting (pending): pending wins
const board = [makeItem(pantry, "done", false), makeItem(mugs, "done", true)];
expect(resolveRoomQuest(kitchen, HERO, board, [pantry, mugs])).toMatchObject({ kind: "quest", quest: { id: "mugs" } });
const twoOpen = [makeItem(mugs, "open"), makeItem(pantry, "open")];
expect(resolveRoomQuest(kitchen, HERO, twoOpen, [pantry, mugs])).toMatchObject({ quest: { id: "pantry" } });
});
it("sleeps for a hero none of its quests is for", () => {
expect(resolveRoomQuest(room({ questIds: ["dishes"] }), HERO, [], [dishes])).toMatchObject({ kind: "notYours", quest: { id: "dishes" } });
});
it("falls back to its template, else an empty lair, when none of its quests is live", () => {
expect(resolveRoomQuest(room({ questIds: ["gone"] }), HERO, [], [])).toEqual({ kind: "emptyLair" });
});
it("is headlined by its biggest live quest, ties to list order", () => {
expect(headlineQuest(kitchen, [mugs, pantry, dishes])?.id).toBe("dishes");
expect(headlineQuest(room({ questIds: ["mugs", "x"] }), [mugs, makeQuest("x", { difficulty: 3 })])?.id).toBe("mugs");
expect(headlineQuest(room({ questId: "pantry" }), [pantry])?.id).toBe("pantry");
});
});(Board statuses are "open" | "done" | "notDue"; "done" with pending: true reads pending.)
-
Step 2: Run
chemx test tests/unit/questRooms.spec.ts. Expected: FAIL (roomQuestIdsis not exported). -
Step 3: Implement. Add
questIds?: string[]("A quest room: the quests it holds, in order; each hero meets the one open for them") andname?: string("What the room is called in the house: 'Kitchen'") toRoomContent. InquestRooms.ts:roomQuestIdsreturnscontent.questIdswhen it is a non-empty array, else[questId], else[](strings).headlineQuest: the live (non-draft) quests among the ids, the highestNumber(difficulty), ties to list order.isQuestRoomusesroomQuestIds(room).length > 0 || Boolean(room.content?.quest). ReplaceresolvePick(questId, ...)withresolvePicks(ids, heroId, board, liveQuests): for each id build the candidate the oldresolvePickbuilt (board row → quest with item; live and visible → quest without item; live and not visible → notYours); among thequestcandidates take the best byPREFERENCE[readRow(item)]with a stable sort; else the firstnotYours; else null.resolveRoomQuestusesroomQuestIds(room); the classic check becomes "no ids and no template". -
Step 4: Run
chemx test tests/unit/questRooms.spec.ts. Expected: PASS, the old tests included. -
Step 5: Write the Review Focus 1 test in
tests/unit/TempleQuestRooms.spec.ts, built like "again when the temple screen opens a run already going (back from a battle)":
it("leaves an open list room unsealed when its next chore lurks after a win", async () => {
const a = quest("q-a");
const b = quest("q-b");
const id = makeTemple(
{ E: { type: "entrance" }, LIST: { type: "monster", content: { questIds: ["q-a", "q-b"] } } },
[["E", "LIST"]]
);
world([a, b]);
await openFor(id);
expect(temples.moveToPosition(id, [0, 1], "east")).toBe(true);
world([a, b], [entry("q-a", { source: "battle" })]); // won a; b is still open
temples.initTemple(id, [0, 1], undefined, HERO); // back from the battle
await temples.whenQuestRoomsReady(id);
expect(cleared(id, 1)).toBe(false); // b lurks
expect(temples.getCurrentRoom(id)?.locked ?? {}).not.toEqual(SEALED);
});-
Step 6: Run
chemx test tests/unit/TempleQuestRooms.spec.ts. Expected: PASS. -
Step 7: Commit
types.ts,questRooms.tsand both specs:feat(quest rooms): a room can hold several quests, each hero meets the open one.
Files:
- Modify:
src/lib/services/temples/templeQuestRows.ts:56-111, 257-353 - Modify:
src/lib/services/temples/questRoomPicker.ts:336-346 - Modify:
src/app/Admin/XpCompendium/views/XpTemples/components/XpTempleCreator/hooks/grid/roomQuest.ts:20-68 - Test:
tests/unit/TempleQuestRows.spec.ts,tests/unit/QuestRoomPicker.spec.ts,tests/unit/TempleCreatorRoomQuest.spec.ts
Interfaces:
-
Consumes:
roomQuestIds,headlineQuest(Task 1). -
Produces: no new names;
gmResolution,roomSizeShareanddescribeRoomQuestread list rooms. -
Step 1: Write the failing tests.
- TempleQuestRows: a layout with one monster cell
{ questIds: ["mugs", "dishes"] }(dishes difficulty 13, mugs 3):questRoomCellsfinds it; its row hastitleof dishes,moreQuests: 1,mismatch: null;size.totalis 13. AquestIdroom holding a 13 in a monster room still reports its mismatch (unchanged). - QuestRoomPicker:
roomSizeShare({ type: "monster", content: { questIds: ["mugs", "dishes"] } }, [mugs, dishes])is{ difficulty: 13 }. - TempleCreatorRoomQuest:
describeRoomQuestof that room is{ kind: "picked", quest: dishes }; afterapplyQuestPick(room, mugs)the room hasquestId: "mugs"and noquestIds; afterreleaseQuest(room)neither is set.
- TempleQuestRows: a layout with one monster cell
-
Step 2: Run the three specs with
chemx test. Expected: FAIL. -
Step 3: Implement. templeQuestRows: the local
isQuestRoomusesroomQuestIds;gmResolutionreturns{ kind: "quest", quest: headlineQuest(room, live), source: "questId" }when the room has ids and a headline, else the template path as now. InrowOf, for a room whose content hasquestIds:moreQuestsis the count of its other live quests, andmismatchis null. questRoomPicker:roomSizeShareusesheadlineQuestbefore the template. roomQuest.ts:applyQuestPickandreleaseQuestdeletecontent.questIds;describeRoomQuestusesroomQuestIds/headlineQuest(pickedwith the headline,missingwhen the room names ids and none is live). -
Step 4: Run the three specs. Expected: PASS.
-
Step 5: Commit the three modules and specs:
feat(temple editor): list rooms read by their biggest quest in the quest tab, size meter and editor.
Files:
- Modify:
src/lib/services/core/packs/movingPack.ts:65-73(MovingAnswers) - Create:
src/lib/services/core/packs/movingTemple/houseRooms.ts - Test:
tests/unit/movingHouseRooms.spec.ts
Interfaces:
- Consumes:
MovingPack,MovingAnswers,MovingHero(movingPack.ts);Direction(@/lib/engine/dungeons/generator). - Produces:
export type HouseFloor = "B1" | "1F" | "2F";
export type HouseCellKind =
| "entrance" | "quest" | "guard" | "boss" | "master" | "masterKey" | "key" | "map" | "compass"
| "camp" | "heal" | "save" | "stairsUp" | "stairsDown" | "loot";
/** Cells touch only cells of their own zone, except the one link to the cell they hang off */
export type HouseZone = "house" | "drawer" | "driveway" | "wing";
export interface HouseCell {
id: string;
name: string;
kind: HouseCellKind;
floor: HouseFloor;
/** The cells it may hang off, first built one wins; empty for fixed cells and landings */
parents: string[];
/** Directions from the parent tried first, in order */
prefer: Direction[];
zone: HouseZone;
/** A fixed cell of the 9x9 frame, [row, col] */
at?: [number, number];
/** A landing: the stairs cell on 1F it stands over or under */
twinOf?: string;
/** Pack quest ids, in list order; empty for cells that are not fights */
questIds: string[];
/** The small-key room: its parent's door into it is locked */
lockedFromParent?: boolean;
}
export function houseCellsFor(pack: MovingPack, answers: MovingAnswers): HouseCell[] | null;MovingAnswers gains floors?: 1 | 2 (default 1) and basement?: boolean (default false, only read while storage is ticked); the pack builder ignores both.
The cells, in placement order. Quest ids: move-<slug>, move-<slug>-<heroId> for a per-hero quest (every hero who plays, dialog order), move-daily-<slug> for a daily; only ids pack.quests holds are kept, and a quest cell left with no ids is not built (cells hanging off it take their next parent).
| id | name | kind | floor | parents | prefer | zone / at | quests |
|---|---|---|---|---|---|---|---|
| porch | Front porch | entrance | 1F | house, at [8,4] | |||
| van | The moving van | boss | 1F | porch | driveway, at [8,3] | load-van | |
| lastLook | Last look | quest | 1F | van | driveway, at [8,2] | walkthrough, deep-clean | |
| master | Keys to the New House | master | 1F | lastLook | driveway, at [8,1] | ||
| camp | Sleeping bags | camp | 1F | porch | house, at [8,5] | ||
| hall | Front hall | quest | 1F | porch | house, at [7,4] | see below | |
| map | Coat closet | map | 1F | hall | E, W, N | house | |
| living | Living room | quest | 1F | hall | W, N, E | house | living-dining, game-shelf, daily tidy |
| couch | Under the couch cushions | key | 1F | living, hall | W, N, E, S | house (only with a wing) | |
| dining | Dining room | quest | 1F | living, hall | N, W, E | house | supplies, box-labels, frames (+ living-dining with no living room) |
| compass | China cabinet | compass | 1F | dining, living, hall | W, N, E | house | |
| pantry | Pantry | quest | 1F | dining, hall | N, E, W | house | the kitchen's other quests |
| kitchen | Kitchen | guard | 1F | pantry, dining, hall | N, E, W | house | its first: dishes, pantry-fridge, mugs, daily dishes |
| garage | Garage | guard or quest | 1F | pantry, hall | E, W, N | house (or wing) | garage, garage-gear (guard: its first) |
| workbench | Workbench | quest | 1F | garage | N, E, W | house | the garage guard's other quests |
| packingPile | Packing pile | guard | 1F | hall | N, E, W | house | open-first (only with no kitchen and no garage guard) |
| drawer | Junk drawer | masterKey | 1F | the guard | N, E, W, S | drawer | |
| laundry | Laundry room | quest | 1F | pantry, dining, hall | W, N, E | house | laundry |
| yard | Back yard | quest | 1F | pantry, dining, living, hall | N, E, W | house (or wing) | yard |
| pets | Pet corner | quest | 1F | living, hall | S, W, N | house | pets, daily feed-pets |
| bath | Bathroom | quest | 1F | hall | E, N, W | house | bathrooms, daily teeth |
| storage | Storage room | quest | 1F | garage, hall | N, E, W | house (no basement) | storage |
| office / guest / bedrooms | as on 2F | quest | 1F | hall | W, E, N | house (1-floor house) | as on 2F |
| stairsUp | Stairs up | stairsUp | 1F | hall | E, W, N | house (2 floors) | |
| basementStairs | Basement stairs | stairsDown | 1F | hall | W, E, N | wing (basement) | |
| landing | Landing | stairsDown | 2F | house, twinOf stairsUp | |||
| upHall | Upstairs hall | save | 2F | landing | N, E, W, S | house | |
| bedroom-main | "'s bedroom" (one grown-up) / "Main bedroom" | quest | 2F | upHall, landing | W, E, N, S | house | each grown-up's closet-grownup, beds-apart |
| bedroom-1..n | "'s bedroom" / " and 's room" / ", and 's room" | quest | 2F | upHall, landing | W, E, N, S | house | kid: keep-donate-trash, books-kid, dresser-kid, backpack-kid, clear-old-room; little: toy-triage, books-little, toys-little, backpack-little, dust-bunny-hunt (each hero's own ids, hero by hero) |
| guest | Guest room | quest | 2F | upHall | W, E, N | house | guest-room |
| office | Home office | quest | 2F | upHall | E, W, N | house | office, paperwork |
| heal | Medicine cabinet (2+ bathrooms) / Linen closet | heal | 2F | upHall | E, W, N | house | |
| b1Landing | Basement | stairsUp | B1 | house, twinOf basementStairs | |||
| storage | Storage | quest | B1 | b1Landing | N, E, W, S | house (basement) | storage |
| forgotten | Forgotten box | loot | B1 | b1Landing, storage | E, W, N, S | house |
Rules the table leaves to code:
-
Front hall quests: open-first (unless the packing pile holds it), donation-run, light-boxes per kid, pillow-parade per little; then, when their room is not built: paperwork (office), supplies, box-labels, frames (dining), daily tidy (living), daily teeth (bath), daily dishes (kitchen), and every bedroom quest (no bedroom ticked).
-
Two floors only when
floors === 2and a bedroom, guest or office cell exists; otherwise every 2F row is built on 1F withparents: ["hall"]and no landing, upstairs hall, heal or stairs up. -
Basement only when
basementand storage is ticked. -
Bedrooms: grown-ups share
bedroom-main(none when no grown-up plays). Kids and little ones getmax(1, bedrooms - 1)rooms when a grown-up plays, elsebedrooms, dealt round robin in dialog order. A hero with an empty name reads "Hero". -
The guard: the kitchen when built; else the garage when it has a quest (first quest in the guard, the rest in
workbench); elsepackingPile. The guard's id is the drawer's only parent. -
The wing (small-key room): the first of
basementStairs,garage(when not the guard),yardthat is built; it getszone: "wing"andlockedFromParent: true. With a wing,couchis built; storage on 1F then hangs offhallwhen the garage is the wing. -
Step 1: Write the failing tests in
tests/unit/movingHouseRooms.spec.tswithbuildMovingPackand the spec's family (dadgrown-up,twelvekid,sixlittle, default rooms, move 2026-11-20, today 2026-10-01):- the default home, 1 floor: ids include porch, van, lastLook, master, camp, hall, living, dining, pantry, kitchen, drawer, garage, laundry, yard, bath, storage, bedroom-main, bedroom-1, bedroom-2; every cell's floor is "1F"; kitchen
questIdsis["move-dishes"]; pantry is["move-pantry-fridge", "move-mugs", "move-daily-dishes"]; drawerparentsis["kitchen"]. -
floors: 2: the bedrooms, upHall, landing and heal are on 2F;landing.twinOf === "stairsUp"; heal is named "Medicine cabinet" (2 bathrooms). -
floors: 2, basement: true: basementStairs iszone "wing"andlockedFromParent; storage is on B1; couch exists. -
floors: 2with no bedroom, guest or office ticked: every cell is on 1F and there is no stairsUp. -
basement: truewith storage unticked: no basementStairs, no B1 cell. - every old-house quest and daily the pack made is in exactly one cell:
cells.flatMap(c => c.questIds).sort()equals the pack's quest ids whosecategoryId !== "cat-move-unpack", sorted; no id appears twice. - names: one grown-up gives "Dad's bedroom"; two kids in one kid room give "Twelve and Six's room"; an empty hero name gives "Hero's bedroom".
- only little ones, kitchen ticked: the kitchen holds
["move-daily-dishes"]and no pantry is built. - nobody playing, or no load-van:
null.
- the default home, 1 floor: ids include porch, van, lastLook, master, camp, hall, living, dining, pantry, kitchen, drawer, garage, laundry, yard, bath, storage, bedroom-main, bedroom-1, bedroom-2; every cell's floor is "1F"; kitchen
-
Step 2: Run
chemx test tests/unit/movingHouseRooms.spec.ts. Expected: FAIL (module missing). -
Step 3: Implement
houseCellsForfrom the table and rules, plus the twoMovingAnswersfields (JSDoc: "How many floors the home has; the temple puts the bedrooms upstairs on two", "The storage room is a basement under the house"). -
Step 4: Run the spec. Expected: PASS. Then
chemx test tests/unit/movingPack.spec.ts. Expected: PASS (pack unchanged). -
Step 5: Commit
movingPack.ts,houseRooms.ts, the spec:feat(moving temple): the rooms of the house and the chores in each.
Files:
- Create:
src/lib/services/core/packs/movingTemple/housePlanner.ts - Test:
tests/unit/movingHousePlanner.spec.ts
Interfaces:
- Consumes:
HouseCell,HouseFloor(Task 3);DIRECTIONS,STEP,OPPOSITE,rngFor(@/lib/engine/dungeons/generator). - Produces:
export const HOUSE_FRAME = 9;
export interface PlacedCell {
cell: HouseCell; row: number; col: number;
/** The id of the cell it was placed beside (its parent, or the fallback cell); none for fixed cells and landings */
via?: string;
}
export interface HousePlan { floors: HouseFloor[]; rows: number; cols: number; cells: PlacedCell[] }
export function planHouse(cells: readonly HouseCell[], seed: string): HousePlan;The placement (one 9x9 frame shared by every floor, so twin stairs share row and col):
- Floors in the order 1F, 2F, B1; cells of a floor in array order.
- A cell with
atgoes there; a cell withtwinOfgoes on the twin's row and col. - Otherwise its parent is the first of
parentsalready placed on its floor. Tryprefer, then the otherDIRECTIONSin the orderrngFor(\${seed}:${cell.id}`)` shuffles them. A target fits when it is inside the frame, free on that floor, and every placed orthogonal neighbour other than the parent has the cell's own zone. - Nothing fits beside the parent: walk placed cells of the cell's zone on that floor breadth first from the parent (neighbours in
DIRECTIONSorder), and take the first fitting free neighbour (DIRECTIONSorder) of each;viais that cell. - Still nothing:
throw new Error(\The house has no room for ${cell.name}`)`. - Trim every floor to the bounding box of all floors' cells together;
floorslists only floors with cells, in the order B1, 1F, 2F.
-
Step 1: Write the failing tests in
tests/unit/movingHousePlanner.spec.tswith hand-madeHouseCelllists (acell(id, overrides)helper):- fixed cells keep their place relative to each other after the trim (porch directly east of the van, the hall directly north of the porch).
- a cell with
prefer: ["west"]beside a free west goes west. - a
drawer-zone cell hung off ahousecell touches nothing else, and ahousecell placed later never lands next to it (place a drawer, then 20 house cells off the same parent; assert the drawer's three other neighbours are empty). - a twin lands on its twin's row and col on the other floor.
- the same cells and seed give the same plan; a cell whose first preferred direction is free lands there whatever
the seed (the seed only orders the directions after
prefer). - every placed non-fixed cell has
viaset to a cell it touches. - a full 9x9 frame of fixed cells makes one more cell throw "The house has no room for X".
-
Step 2: Run
chemx test tests/unit/movingHousePlanner.spec.ts. Expected: FAIL. -
Step 3: Implement
planHouseas above (aMap<floor, Map<"r,c", PlacedCell>>for occupancy). -
Step 4: Run the spec. Expected: PASS.
-
Step 5: Commit
housePlanner.tsand the spec:feat(moving temple): a floor planner that grows the house from the front hall.
Files:
- Modify:
src/lib/services/core/packs/movingTemple.ts(rewrite the layout half; keepMOVING_TEMPLE_ID,MOVING_TEMPLE_NAME,movingTempleBackup) - Test:
tests/unit/movingTemple.spec.ts(rewrite the layout tests; keep the backup and merge-load tests)
Interfaces:
- Consumes:
houseCellsFor(Task 3),planHouse(Task 4),roomQuestIds(Task 1),questTier,templeSize. - Produces:
export type MovingRoomType = "monster" | "miniboss" | "boss";
export interface MovingTempleRoom {
id: string; label: string; floor: HouseFloor; questIds: string[]; names: string[];
type: MovingRoomType; tier: QuestTier; /** its biggest quest's */ difficulty: number; guardsKey: boolean;
}
export interface MovingTemple { record: TempleInterface; rooms: MovingTempleRoom[]; size: number; floors: HouseFloor[] }
export const buildMovingTemple: (pack: MovingPack, answers: MovingAnswers) => MovingTemple | null;
export const movingTempleSummary: (temple: MovingTemple) => string;Cells to rooms (every room gets content.name; tokens counted per char like the generator, "_" for walls):
| kind | char | room |
|---|---|---|
| entrance | _00_ |
{ type: "entrance", visited: true } |
| quest |
q__q (biggest ≤ 1) else Q__Q
|
type "monster", content { questIds, difficulty: biggest ≤ 1 ? "easy" : "normal" }
|
| guard | x__x |
type "miniboss", content { questIds: [id], difficulty: "hard", lockOnEnter: true }
|
| boss | X__X |
type "boss", content { questIds: [id], difficulty: "boss", lockOnEnter: true }, locked: { master: true }
|
| master | $__$ |
type "master", content { chest: "dungeon", dungeon: "Pegasus", pegasus: true }
|
| masterKey / key / map / compass |
MK__ / K___ / $__$ / $__$
|
type "loot", content { chest: "dungeon", dungeon: "master-key" / "key" / "map" / "compass" }
|
| camp / heal / save |
CAMP / H__P / SAVE
|
type "camp" / "health" (both healthPoints: 15) / "save"
|
| stairsUp / stairsDown |
ST_U / ST_D
|
type "stairs-up" / "stairs-down", content { targetLevel, targetCoords: { x: col, y: row } } of the twin |
| loot | $__$ |
type "loot", content { chest: "consumable", items: ["potion"] }
|
The room the wing was placed beside (via) gets locked[direction from it to the wing] = true. dungeonLayout: { entrance: [index of "1F" in floors, porch row, porch col], maze: { [floor]: grid }, rooms }. seed: moving-day: plus JSON.stringify of the ticked rooms (sorted keys, counts > 0), floors, basement, and each playing hero's [id, band]. Description: The old house, room by room. Every room holds the packing quests for it, and the key to the van is in the junk drawer behind the ${guard name lowercased}.. rooms: the quest, guard and boss cells in plan order; size is templeSize of their biggest difficulties.
movingTempleSummary, lines joined with "\n":
The Old House is open in Hometown: a ${"one-floor" | "two-floor"} house with ${plural(n, "room")} of packing, size ${size}. Take the van keys from the junk drawer behind the ${guard}, then load the van.- One line per floor that has quest rooms:
Basement: ...,Downstairs: ...(orRooms: ...on a one-floor house),Upstairs: ...; thenDriveway: The moving van, Last look. Driveway-zone rooms are only on the Driveway line; names comma-joined in plan order.
-
Step 1: Rewrite the layout tests in
tests/unit/movingTemple.spec.ts. Keep its mocks and family, and the tests for the id, the backup and the merge load. Add:- a matrix: floors 1 and 2 × basement off and on × [default rooms, every room at its max (bedroom 8, bathroom 5, guest 4, all ticks), only kitchen, only bedroom 1, nothing ticked] × families [dad+twelve+six, six alone, two grown-ups + four kids, dad alone]. For each:
solveTemple(record.dungeonLayout)hassolvable: trueandreasons: [];auditTemple(record).passed; every floor grid is at most 9×9; exactly the guard and the van havelockOnEnter, and each has one quest id; everyquestIdsid is a pack quest and none is an unpack one. - the default family on two floors: each hero has a quest in at least three rooms; the van's only neighbours are the porch and Last look; Last look's only neighbours are the van and the master room.
-
buildMovingTemple(pack, answers)deep-equals the build for the same answers with anothermoveDate. - the summary for the default family on two floors has the lines
Downstairs:,Upstairs:andDriveway: The moving van, Last look.
- a matrix: floors 1 and 2 × basement off and on × [default rooms, every room at its max (bedroom 8, bathroom 5, guest 4, all ticks), only kitchen, only bedroom 1, nothing ticked] × families [dad+twelve+six, six alone, two grown-ups + four kids, dad alone]. For each:
-
Step 2: Run
chemx test tests/unit/movingTemple.spec.ts. Expected: FAIL. -
Step 3: Implement the façade:
houseCellsFor→planHouse(cells, seed)→ the record per the table. Keep the file under ~300 lines; the cell-to-room table can live inmovingTemple/houseRecord.tsif it grows. -
Step 4: Run
chemx test tests/unit/movingTemple.spec.ts tests/unit/movingHouseRooms.spec.ts tests/unit/movingHousePlanner.spec.ts. Expected: PASS. A matrix case that fails the solver is a planner or table bug: fix it there, never by loosening the test. -
Step 5: Commit
movingTemple.ts(+houseRecord.ts) and the spec:feat(moving temple): the Old House as a floor plan, every hero's chores in its rooms.
Files:
- Create:
src/lib/services/core/packs/movingTempleReset.ts - Modify:
src/app/SideMenu/XpSettings/components/MovingHouseModal.vue - Test:
tests/unit/movingTempleReset.spec.ts
Interfaces:
-
Consumes:
MOVING_TEMPLE_ID,buildMovingTemple,movingTempleSummary(Task 5);getTempleProgressDb,TempleProgressDb.clearProgress(userId, templeId). -
Produces:
forgetOldHouseWalks(heroIds: readonly string[], db?: Pick<TempleProgressDb, "clearProgress">): Promise<void>. -
Step 1: Write the failing test
tests/unit/movingTempleReset.spec.ts: with a fake db (clearProgress: jest.fn()),forgetOldHouseWalks(["dad", "twelve", "out-kid"], db)callsclearProgressonce for each hero with"gen-move-house"; when one call rejects, the others still run and the promise resolves. Mock@/lib/databases/TempleProgressDbas movingTemple.spec does. -
Step 2: Run
chemx test tests/unit/movingTempleReset.spec.ts. Expected: FAIL. -
Step 3: Implement
forgetOldHouseWalks(Promise.allSettled; a rejection isdebug.warned, never thrown). In the dialog:- refs
floors = ref<1 | 2>(1)andbasement = ref(false);answersaddsfloors: floors.value, basement: basement.value && (counts.storage || 0) > 0. - a new
<section class="moving-section">under "Rooms in the home":<h3>Floors</h3>, arole="radiogroup"in themoving-bandsstyle with tworole="radio"buttons, "1 floor" and "2 floors" (data-cy="moving-floors-1"/"moving-floors-2"); below it,v-if="(counts.storage || 0) > 0", amoving-checkcheckbox "Storage is a basement" (data-cy="moving-basement"). - preview:
...and a ${floorsWord} house temple with ${previewTemple.rooms.length} rooms, withfloorsWord"one-floor" / "two-floor" frompreviewTemple.floors. -
build(): when a temple is built,await forgetOldHouseWalks(userStore.usersAz.map((hero) => String(hero.id)))beforerestoreBackup. -
.moving-summary { white-space: pre-line; }.
- refs
-
Step 4: Run
chemx test tests/unit/movingTempleReset.spec.ts, thenchemx verify --build. Expected: PASS; typecheck clean; no new lint errors. -
Step 5: Commit the helper, the dialog, the spec:
feat(moving house): ask for floors and a basement, and forget the old walk on a rebuild.
Files:
- Modify:
src/app/Console/TempleGrounds/TempleGrounds.vue:7-48,src/app/Console/TempleGrounds/_TempleGrounds.scss - Modify:
tests/e2e/specs/moving-temple.spec.js
Interfaces:
-
Consumes:
RoomContent.name(Task 1),currentRoom(already returned byTempleGrounds.ts). -
Step 1: Update the e2e
moving-temple.spec.js:- in the dialog test, click
[data-cy=moving-floors-2]before building and expect the preview to containtwo-floor house temple with. - in the walk-in test, after entering:
cy.get("[data-cy=temple-room-name]").should("have.text", "Front porch"); clickion-fab[vertical=top][horizontal=center] ion-fab-button; expect"Front hall"; keep the screenshots (04-front-door, a new05-front-hall). - remove the bedroom deep link (its row and column came from the old comb layout); the old
05/06screenshots go.
- in the dialog test, click
-
Step 2: Add the plate in the toolbar, after the start buttons:
<ion-title v-if="currentRoom?.content?.name" class="temple-room-name" data-cy="temple-room-name">{{ currentRoom.content.name }}</ion-title>. SCSS: StatusPlz,$eb-color-cream, one line with an ellipsis, no wider than the space between the buttons. -
Step 3: Run
chemx verify --build. Expected: typecheck clean, no new lint errors, suite green. -
Step 4: Run the e2e against the shared dev server (start nothing if
curl -s localhost:8100answers):pnpm vue-cli-service test:e2e --headless --browser chrome --url http://localhost:8100 --spec tests/e2e/specs/moving-temple.spec.js. Expected: all tests pass. Then look attests/e2e/screenshots/moving-temple.spec.js/04-front-door.pngand05-front-hall.png: the name plate reads cleanly at 390 wide. -
Step 5: Commit the page, its SCSS, the e2e and its screenshots:
feat(temple): a room's name in the header; e2e walks the Old House porch to hall.
Files:
-
Modify:
docs/Quest-Rooms-Spec.md(section 5.2: list rooms, the headline quest, sealed rooms hold one quest) -
Modify:
docs/superpowers/specs/2026-10-13-moving-house-temple-design.md(Status line: implemented, with the commit range) -
Step 1: Add the 5.2 paragraph and update the spec's status line.
-
Step 2: Run
chemx verify --build. Expected: typecheck clean, suite green; the AST gate's critical and high counts no higher than before Task 1. -
Step 3: Commit in the docs submodule (
git -C docs commit -- <paths>), then bump the submodule pointer in the main repo with the private-index protocol:docs: list rooms in the quest-room spec; moving temple shipped.