task 3 report - xopherdeep/do-it-for-the-xp GitHub Wiki

Task 3 report: taking a drop in play (board #1455)

Branch worktree-agent-a6c384aecab78e0a9, one commit: 2a2b9dac feat(temples): a beaten beast drops what its room holds, and it waits there for a hero with no fight there (on top of origin/master 62e9e5fb).

Status: DONE_WITH_CONCERNS (concerns at the end; none blocks).

What I built

Engine (remembering the take)

  • templeSystem/chests.ts: markDropTaken(runs, templeId, level, position) and isDropTaken(...). They file dropKey(level, position) ("1F:2,3:drop") in TempleState.openedChests, apart from the room's own key, so the search and the drop never spend each other. The key already round-trips through saves: heroRuns parses openedChests keys with parseRoomKey, which returns null for a :drop key, so it is kept and does not touch any room. A blood moon (templeReset, openedChests: []) gives it back; a quest restoring a room only touches clearedRooms.
  • TempleSystem.ts: public markDropTaken / isDropTaken wrappers without runs.

Doors bug (pre-existing)

  • templeSystem/doors.ts. Cause: a Bomb and the Boss Key both filed the plain doorKey in openedDoors, and doorKind read that key as "the boss lock is open". A blown crack into a boss room therefore read open.
  • Fix: a cracked wall into a boss room reads master until the Boss Key has opened it. unlockDoor writes a bossKeyMark (${doorKey}:boss) for that case only, and doorKind requires it for a cracked side. Every other boss door and every other blown crack record exactly what they did before: openedDoors keeps the same keys, so saved runs and the existing exact-set assertions hold. heroRuns ignores the mark when it re-locks doors from a save (its direction is east:boss, not a side). A blood moon clears it with the rest of openedDoors.
  • A run saved before the fix, with a crack into a boss room already blown, now needs the Boss Key at that side again (lead ruling: Bombs never open locked doors).

The sheet

  • vocabulary.ts: RoomIntent gains 'takeDrop'; RoomSheet.drop?: 'waiting'.
  • roomInteractions/treasure.ts: isDropWaiting(room, ctx, isFightLive) = roomDrops(room) !== null && !isDropTaken && !isFightLive.
  • RoomInteractions.ts (monster/miniboss/boss case): the fight is live for a quest room when questWords.fight, for a classic room while !isRoomCleared. When the drop waits the sheet offers Take it (intent takeDrop) before the search, sets drop: 'waiting', and puts the raw line "Something glints where the beast fell." in body (after the quest's chore line, joined with a space; alone in a classic room). Quest statuses that offer it: cleared, pending, notYours, away, emptyLair (every status with no fight).

Taking it (hook and screen)

  • src/lib/services/temples/lootBag.ts (new, pure): bagItemOf(value) (a supply or the Bomb becomes the bag row; a Bomb is { name: 'Bomb', kind: 'consumable', faIcon: 'bomb', qty: 1 } and inBagOnly), bagHolds, bagHoldsAll (simulates the bag and storage row by row, so two supplies that each need the one free slot do not both pass).
  • src/hooks/temples/templeLoot.ts (new): the extracted handOutItems(deps, selected, content) (the old claimChestItems loop, with the Bomb branch added), dropRows(drops) (chestLoot of the drop, dungeon passed through dungeonItemId so "small-key" becomes "key", every row ticked), and takeDrop(...): nothing left -> "Nothing left here."; picks filtered to the drop's own rows; nothing picked -> "You took nothing."; bagHoldsAll false -> nothing handed out, "Your bag is full. Store something at home first."; otherwise handOutItems, saveProgress, markDropTaken, "Collected: ...".
  • useTemple.ts: claimChestItems now calls handOutItems (so chest Bombs go into the bag too, and stay in the chest when the bag is full); new dropContents (computed, [] once taken) and claimDropItems. File went from 680 to 642 lines.
  • templeReveals.ts: useChestReveal gains showDropReveal() (opens the reveal open, not shut, with the drop's rows) and routes Take to claimDropItems while a drop is shown (isDropShown, reset by showChestContentsDialog / showEmptyChest); the Pegasus logic is skipped for a drop.
  • useRoomOutcomes.ts: new dep showDropReveal and case 'takeDrop'.
  • TempleGrounds.ts: wired with no net lines (498 stays 498, under the 500 ratchet) by putting the new names on the existing destructure / argument lines.

TDD evidence

RED (before any implementation), chemx test tests/unit/RoomInteractions.spec.ts:

  ✖ Test Failures (1 failed out of 1)
    ✖ Test suite failed to run
      tests/unit/RoomInteractions.spec.ts:1268:18 - error TS2339: Property 'drop' does not exist on type 'RoomSheet'.

RED, chemx test tests/unit/TempleSystemDoors.spec.ts (the bug reproduced):

  ✖ Test Failures (3 failed out of 24)
    ✖ ... a cracked wall into a boss room › is still the Boss door once a Bomb has blown it: the blast opens the wall, not the lock
      Expected: "master"   Received: "open"
    ✖ ... › takes no small key, and the Boss Key is not spent opening it    (Expected: true, Received: false)
    ✖ ... › stays the Boss door through a reload, and stays open once the Boss Key opened it   (Expected: "master", Received: "open")

RED, chemx test tests/unit/FightDrops.spec.ts (after the doors fix, before the engine/hook code):

  ✖ Test suite failed to run
      tests/unit/FightDrops.spec.ts:204:13 - error TS2339: Property 'markDropTaken' does not exist on type 'TempleSystem'.

and, once the engine existed, the hook block failed with vm.dropContents undefined / vm.claimDropItems is not a function (13 failures).

GREEN (final code, run one at a time):

chemx test tests/unit/RoomInteractions.spec.ts   ✔ All tests passed (137 passed)
chemx test tests/unit/FightDrops.spec.ts         ✔ All tests passed (39 passed)
chemx test tests/unit/TempleSystemDoors.spec.ts  ✔ All tests passed (24 passed)

New tests:

  • RoomInteractions.spec.ts (a fight room's drop): lurking offers no Take it and no drop; cleared, pending, notYours and away offer it (intent: 'takeDrop', drop: 'waiting', the glint line in body); a classic room not yet cleared does not, and a cleared monster/miniboss/boss does; after markDropTaken it goes and the search stays, isRoomSpent stays false, and the two are independent both ways; no drop on a drop-less room or under a loot room.
  • FightDrops.spec.ts: TempleSystem (drop key, apart from the room key, per floor and hero; reload keeps it; another hero's run does not have it; a real blood moon gives it back; a daily quest restoring the room does not) and the hook (rows and ticks, key/Boss Key/map/compass/gold hand-out, second take gives nothing, empty take, rows the drop does not hold ignored, a Bomb into the bag in the shop's shape and merging into an existing stack, chest Bomb into the bag / stays when full, whole-or-nothing when full including the last-two-slots simulation, supply into storage but Bomb bag-only, keys and gold need no slot).
  • TempleSystemDoors.spec.ts: a cracked wall into a boss room stays master after the blast (explain line, no walking, small key refused, Boss Key opens it without being spent, survives a reload, the way back out is plain), and other cracks / plain boss doors still record exactly what they did.

Files changed

src: src/lib/engine/core/dungeons/templeSystem/chests.ts, .../templeSystem/doors.ts, .../TempleSystem.ts, src/lib/services/temples/RoomInteractions.ts, .../roomInteractions/vocabulary.ts, .../roomInteractions/treasure.ts, .../lootBag.ts (new), src/hooks/temples/useTemple.ts, .../templeLoot.ts (new), src/app/Console/TempleGrounds/templeGrounds/templeReveals.ts, .../useRoomOutcomes.ts, src/app/Console/TempleGrounds/TempleGrounds.ts. tests: tests/unit/RoomInteractions.spec.ts, tests/unit/FightDrops.spec.ts, tests/unit/TempleSystemDoors.spec.ts.

Self-review

  • Line budget: new files 162 and 58 lines; TempleGrounds.ts 498 (unchanged); useTemple.ts shrank to 642; TempleSystem.ts grew 667 -> 685, but it was already over 500 (wrappers are required by the brief), so the ratchet count (a file count) does not move.
  • Inline conditions are named; no inline object parameter types (BagRow, BagLoot, LootHandOut, DropTake are named interfaces). Comments in the file's long plain-English style.
  • claimChestItems is behaviour-preserving apart from the new Bomb branch: the loop moved verbatim into handOutItems; the supply branch makes the same addInventoryItem call with the same row.
  • I did not run typecheck / lint / build (barred). jest type-checks every file a spec imports (this caught a BagRow index-signature error, fixed), so the engine, sheet, useTemple, templeLoot and lootBag are type-checked; templeReveals.ts, useRoomOutcomes.ts and TempleGrounds.ts are not imported by the three specs and are only reviewed by eye, so the lead's typecheck is the first compiler pass over them.
  • Per the constraint I ran only the three named specs. The other specs that exercise code I changed are for CI: TempleChestTake.spec (claimChestItems), BossKeyChest.spec, DragonEggRoom.spec, TempleKeyCount.spec, AuthoredBossDoor.spec, TempleSystemMaster.spec, ItemUse.spec, TempleSystemFloors.spec, TempleWallSheet.spec (compiles templeReveals.ts).

Concerns

  1. Unticking a row forfeits it. The brief's claimDropItems(selected) hands out the ticked rows and then marks the whole drop taken (one flag per room), so a hero who unticks the key loses it, and the temple could then be unfinishable although the solver counts that key. I mitigated it by ticking every row of a drop by default (dropRows sets checked: true; the reveal honours it, Take reads "Take all"), but the hero can still untick. If you want it airtight, either ignore selected beyond the "took nothing" guard, or have the reveal not offer unticking for a drop. I did not change the brief's signature.
  2. A Bomb needs the bag, not storage (as the temple shop does: ItemUse.bombInPack reads the pack, so a Bomb in home storage cannot be used). The brief says "no room in the bag or storage" for the whole-drop check; I applied that to supplies (bag or storage) and bag-only to the Bomb. A chest or drop whose Bomb cannot go in the bag says the bag is full even when storage has room.
  3. Quest status pending (claim waiting for the Game Master) offers Take it, as does emptyLair, because the brief's rule is "no fight is live". Say so if the drop should wait for approval.
  4. Old saves: a run with a crack into a boss room already blown reads as the Boss door again (needs the Boss Key), by the lead ruling.
  5. The reveal for a drop is the chest sprite, opened immediately (no "A chest!" shut state), with the chest titles ("You found...", "Treasure!"). A Bomb row shows the generic gift icon (lootFace has no bomb entry); I left ChestLoot.ts alone as out of scope (a bomb: { icon: 'bomb' ... } face is a one-liner if wanted).
  6. .superpowers/ is git-excluded in this repo, so this report is not part of the commit.

Fix round 1 (task review: one Important issue, two lead rulings)

Commit 58fafbd1 fix(temples): a beast's drop is handed out whole, so an unticked row can no longer lose a key the temple counts on (on top of 2a2b9dac).

What changed

  1. All-or-nothing hand-out (src/hooks/temples/templeLoot.ts, takeDrop). The nothing-ticked guard stays (picked: the ticked rows that are the drop's own; none -> "You took nothing."), but the bag check and handOutItems now run over every row in held, not picked. A small key, the Boss Key or gold the hero did not tick is handed out with the rest instead of being lost when the one flag marks the drop taken. A row the drop does not hold is still never handed out. The header and takeDrop doc comments say so.
  2. The drop reveal offers no unticking (small change, so done). TempleChestReveal.vue gains an optional whole prop: every row starts ticked (props.whole || item.checked), toggle returns early once ready, and each row gets aria-disabled="true". A tap before the rows are out still skips the animation as before; ordinary chests and the Pegasus chest are untouched. Wiring with no extra lines in TempleGrounds.ts (still 498): useChestReveal's isDropShown is now a ref exposed in chest, destructured on the existing line, returned on the existing isChestClosed, line, and bound as :whole="isDropShown" on the one <temple-chest-reveal> line in TempleGrounds.vue.
  3. Nothing left behind, or no mark (takeDrop). After handOutItems, saveProgress runs (state was changed directly) and then, if leftBehind is not empty, it answers BAG_FULL_LINE and does not call markDropTaken. One named guard (isSomethingLeft).
  4. Audit ratchet (src/lib/services/temples/lootBag.ts): const isRefused = !bagHolds(given, loot); if (isRefused) return false;.

Not touched (as instructed): the shared bag-full string with shop.ts, the solver/engine view of a cracked wall into a boss room, the one-per-line style in TempleGrounds.ts, the Bomb row's source.

Tests (RED first, then GREEN)

New or changed in tests/unit/FightDrops.spec.ts (hook block):

  • "is all-or-nothing: ticking only the gold of a drop that holds a small key still hands out the key" (key + gold, only gold:15 ticked: playerKeys 1, wallet up, drop taken, message Collected: Small Key, N GP, rows empty).
  • "hands out the Boss Key and a Bomb of a drop whose gold alone was ticked".
  • "does not mark the drop taken when handing it out left something behind, and says the bag is full" (the store's addInventoryItem is spied to refuse while the bag looks free, so bagHoldsAll passes and handOutItems leaves the Bomb behind: not marked, bag-full message, rows still shown).
  • The old "a row it does not hold is ignored" test is now "never handed out, whatever was ticked": a lone stray row is "You took nothing."; a stray beside a held row hands out only the held one (key 1, no Boss Key). In tests/unit/TempleChestReveal.spec.ts (new describe "a beast's drop (whole)"): rows come ticked whatever checked says and Take hands over every row; a tap on a row changes nothing and the row is aria-disabled; an ordinary chest still unticks and has no aria-disabled.

RED (before the fix):

chemx test tests/unit/FightDrops.spec.ts
  ✖ Test Failures (3 failed out of 42)
    ✖ ... is all-or-nothing: ticking only the gold of a drop that holds a small key still hands out the key   Expected: 1  Received: 0
    ✖ ... hands out the Boss Key and a Bomb of a drop whose gold alone was ticked                              Expected: true  Received: false
    ✖ ... does not mark the drop taken when handing it out left something behind, and says the bag is full     Expected: false  Received: true
chemx test tests/unit/TempleChestReveal.spec.ts
  ✖ Test Failures (2 failed out of 31)
    ✖ ... a beast's drop (whole) › comes with every row ticked, whatever the rows say, and takes them all     Expected length: 2  Received length: 0
    ✖ ... › offers no unticking: a tap on a row changes nothing and the rows say they cannot be toggled       Expected length: 2  Received length: 1

GREEN (after the fix, one spec at a time):

chemx test tests/unit/FightDrops.spec.ts         ✔ All tests passed (42 passed)
chemx test tests/unit/TempleChestReveal.spec.ts  ✔ All tests passed (31 passed)
chemx test tests/unit/TempleChestTake.spec.ts    ✔ All tests passed (21 passed)
chemx test tests/unit/RoomInteractions.spec.ts   ✔ All tests passed (137 passed)
chemx test tests/unit/TempleWallSheet.spec.ts    ✔ All tests passed (9 passed)

TempleChestReveal.spec is the one extra spec beyond the ones you listed: I touched TempleChestReveal.vue and added its tests there, so it is the direct cover.

Files changed in the fix

src/hooks/temples/templeLoot.ts, src/lib/services/temples/lootBag.ts, src/app/Console/TempleGrounds/components/TempleChestReveal.vue, src/app/Console/TempleGrounds/TempleGrounds.vue, src/app/Console/TempleGrounds/TempleGrounds.ts, src/app/Console/TempleGrounds/templeGrounds/templeReveals.ts, tests/unit/FightDrops.spec.ts, tests/unit/TempleChestReveal.spec.ts.

Concern 1 of the first report is resolved

A hero can no longer forfeit a drop's key by unticking it: the reveal does not offer the toggle, and the hand-out ignores the ticks anyway. TempleGrounds.ts / TempleGrounds.vue / templeReveals.ts still have no spec that compiles them together, so typecheck is the first compiler pass over those (the whole binding in the .vue template is not type-checked by jest).

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