2026 10 13 moving house temple - xopherdeep/do-it-for-the-xp GitHub Wiki

The Old House, built like a house: 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: 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

Global Constraints

  • Gates go through chemx only: chemx test <spec>, chemx typecheck, chemx lint, chemx verify --build. Never run pnpm typecheck|lint|test|jest raw.
  • The checkout is shared with other sessions: never git add broadly or git 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> with blob=$(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. Check chemx team lock check-staged <paths> first.
  • A room with a single questId behaves 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 is movingTempleReset.ts.
  • Comments follow the repo: a JSDoc line saying what a thing is and why, plain English, no ticket numbers.

Review Focus

  1. 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.)
  2. 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.)
  3. 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.)
  4. 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.)
  5. 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.)

Task 1: Rooms that hold several quests (resolver)

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 from questRooms.ts); isQuestRoom true for a non-empty questIds.

  • Step 1: Write the failing tests in tests/unit/questRooms.spec.ts (reuse makeQuest, 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 (roomQuestIds is 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") and name?: string ("What the room is called in the house: 'Kitchen'") to RoomContent. In questRooms.ts: roomQuestIds returns content.questIds when it is a non-empty array, else [questId], else [] (strings). headlineQuest: the live (non-draft) quests among the ids, the highest Number(difficulty), ties to list order. isQuestRoom uses roomQuestIds(room).length > 0 || Boolean(room.content?.quest). Replace resolvePick(questId, ...) with resolvePicks(ids, heroId, board, liveQuests): for each id build the candidate the old resolvePick built (board row → quest with item; live and visible → quest without item; live and not visible → notYours); among the quest candidates take the best by PREFERENCE[readRow(item)] with a stable sort; else the first notYours; else null. resolveRoomQuest uses roomQuestIds(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.ts and both specs: feat(quest rooms): a room can hold several quests, each hero meets the open one.


Task 2: The Game Master's readers of list rooms

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, roomSizeShare and describeRoomQuest read list rooms.

  • Step 1: Write the failing tests.

    • TempleQuestRows: a layout with one monster cell { questIds: ["mugs", "dishes"] } (dishes difficulty 13, mugs 3): questRoomCells finds it; its row has title of dishes, moreQuests: 1, mismatch: null; size.total is 13. A questId room 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: describeRoomQuest of that room is { kind: "picked", quest: dishes }; after applyQuestPick(room, mugs) the room has questId: "mugs" and no questIds; after releaseQuest(room) neither is set.
  • Step 2: Run the three specs with chemx test. Expected: FAIL.

  • Step 3: Implement. templeQuestRows: the local isQuestRoom uses roomQuestIds; gmResolution returns { kind: "quest", quest: headlineQuest(room, live), source: "questId" } when the room has ids and a headline, else the template path as now. In rowOf, for a room whose content has questIds: moreQuests is the count of its other live quests, and mismatch is null. questRoomPicker: roomSizeShare uses headlineQuest before the template. roomQuest.ts: applyQuestPick and releaseQuest delete content.questIds; describeRoomQuest uses roomQuestIds/headlineQuest (picked with the headline, missing when 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.


Task 3: Which cells the house has (houseRooms.ts)

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 === 2 and a bedroom, guest or office cell exists; otherwise every 2F row is built on 1F with parents: ["hall"] and no landing, upstairs hall, heal or stairs up.

  • Basement only when basement and storage is ticked.

  • Bedrooms: grown-ups share bedroom-main (none when no grown-up plays). Kids and little ones get max(1, bedrooms - 1) rooms when a grown-up plays, else bedrooms, 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); else packingPile. The guard's id is the drawer's only parent.

  • The wing (small-key room): the first of basementStairs, garage (when not the guard), yard that is built; it gets zone: "wing" and lockedFromParent: true. With a wing, couch is built; storage on 1F then hangs off hall when the garage is the wing.

  • Step 1: Write the failing tests in tests/unit/movingHouseRooms.spec.ts with buildMovingPack and the spec's family (dad grown-up, twelve kid, six little, 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 questIds is ["move-dishes"]; pantry is ["move-pantry-fridge", "move-mugs", "move-daily-dishes"]; drawer parents is ["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 is zone "wing" and lockedFromParent; storage is on B1; couch exists.
    • floors: 2 with no bedroom, guest or office ticked: every cell is on 1F and there is no stairsUp.
    • basement: true with 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 whose categoryId !== "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.
  • Step 2: Run chemx test tests/unit/movingHouseRooms.spec.ts. Expected: FAIL (module missing).

  • Step 3: Implement houseCellsFor from the table and rules, plus the two MovingAnswers fields (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.


Task 4: Where the cells go (housePlanner.ts)

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):

  1. Floors in the order 1F, 2F, B1; cells of a floor in array order.
  2. A cell with at goes there; a cell with twinOf goes on the twin's row and col.
  3. Otherwise its parent is the first of parents already placed on its floor. Try prefer, then the other DIRECTIONS in the order rngFor(\${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.
  4. Nothing fits beside the parent: walk placed cells of the cell's zone on that floor breadth first from the parent (neighbours in DIRECTIONS order), and take the first fitting free neighbour (DIRECTIONS order) of each; via is that cell.
  5. Still nothing: throw new Error(\The house has no room for ${cell.name}`)`.
  6. Trim every floor to the bounding box of all floors' cells together; floors lists only floors with cells, in the order B1, 1F, 2F.
  • Step 1: Write the failing tests in tests/unit/movingHousePlanner.spec.ts with hand-made HouseCell lists (a cell(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 a house cell touches nothing else, and a house cell 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 via set 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 planHouse as above (a Map<floor, Map<"r,c", PlacedCell>> for occupancy).

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

  • Step 5: Commit housePlanner.ts and the spec: feat(moving temple): a floor planner that grows the house from the front hall.


Task 5: The temple record (movingTemple.ts)

Files:

  • Modify: src/lib/services/core/packs/movingTemple.ts (rewrite the layout half; keep MOVING_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":

  1. 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.
  2. One line per floor that has quest rooms: Basement: ..., Downstairs: ... (or Rooms: ... on a one-floor house), Upstairs: ...; then Driveway: 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) has solvable: true and reasons: []; auditTemple(record).passed; every floor grid is at most 9×9; exactly the guard and the van have lockOnEnter, and each has one quest id; every questIds id 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 another moveDate.
    • the summary for the default family on two floors has the lines Downstairs: , Upstairs: and Driveway: The moving van, Last look.
  • 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 in movingTemple/houseRecord.ts if 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.


Task 6: The setup dialog and the rebuild reset

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) calls clearProgress once for each hero with "gen-move-house"; when one call rejects, the others still run and the promise resolves. Mock @/lib/databases/TempleProgressDb as movingTemple.spec does.

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

  • Step 3: Implement forgetOldHouseWalks (Promise.allSettled; a rejection is debug.warned, never thrown). In the dialog:

    • refs floors = ref<1 | 2>(1) and basement = ref(false); answers adds floors: floors.value, basement: basement.value && (counts.storage || 0) > 0.
    • a new <section class="moving-section"> under "Rooms in the home": <h3>Floors</h3>, a role="radiogroup" in the moving-bands style with two role="radio" buttons, "1 floor" and "2 floors" (data-cy="moving-floors-1" / "moving-floors-2"); below it, v-if="(counts.storage || 0) > 0", a moving-check checkbox "Storage is a basement" (data-cy="moving-basement").
    • preview: ...and a ${floorsWord} house temple with ${previewTemple.rooms.length} rooms, with floorsWord "one-floor" / "two-floor" from previewTemple.floors.
    • build(): when a temple is built, await forgetOldHouseWalks(userStore.usersAz.map((hero) => String(hero.id))) before restoreBackup.
    • .moving-summary { white-space: pre-line; }.
  • Step 4: Run chemx test tests/unit/movingTempleReset.spec.ts, then chemx 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.


Task 7: The room name in the temple header, and the e2e walk

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 by TempleGrounds.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 contain two-floor house temple with.
    • in the walk-in test, after entering: cy.get("[data-cy=temple-room-name]").should("have.text", "Front porch"); click ion-fab[vertical=top][horizontal=center] ion-fab-button; expect "Front hall"; keep the screenshots (04-front-door, a new 05-front-hall).
    • remove the bedroom deep link (its row and column came from the old comb layout); the old 05/06 screenshots go.
  • 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:8100 answers): 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 at tests/e2e/screenshots/moving-temple.spec.js/04-front-door.png and 05-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.


Task 8: Docs and the gate

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.

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