Extends equip-from-the-web (ask 5, magic-only) to all five standard gear slots. Owners get an Equipment panel on their own who page with: - Take off for worn masterwork/arena pieces (round-trippable to pack) - Upgrade to the next shop tier (spends euros, confirm-gated) - Repair a damaged slot (spends euros, confirm-gated) The public Gear panel is hidden for the owner since this supersedes it. Wire: equip_orders gains a tier column; new actions upgrade/repair; new verdicts rejected_downgrade / rejected_insufficient_funds / rejected_max_tier. PlayerDetail carries Slots (EquipSlotView x5) + Balance for the confirm dialogs. handleEquipOrder resolves take-off/upgrade/repair from pd.Slots server-side and rejects a client-forged tier (409), same as ask 5 trusts only Pete's own record. Verified: full suite green, headless render of the panel + confirm dialog in both day and night phases. gogobee ships the poll-apply half separately; Pete deploys first so its ingest accepts the new verdicts before gogobee emits them.
23 KiB
Adventure ask 7 — full equipment management from the web (HANDOFF SPEC)
Status: BUILT + TESTED + SCREENSHOT-VERIFIED, DEPLOY PENDING (as of 2026-07-17).
Both sides compile, vet clean, and pass their suites. The owner Equipment panel was
rendered headless (Chrome) in BOTH day and night phases, and the euro confirm dialog
was exercised (upgrade €25,000 and repair €40) — cost + balance math and thousands
separators read right, no purple-on-night contrast issue, public Gear panel correctly
hidden for the owner. Next: commit Pete, commit gogobee, deploy Pete first, then
gogobee (see Build order step 8), then run the live prosolis probe. This doc is the
complete contract + file:line map. Companion to adventure_expansion_spec.md
(asks 1–6) and the memory project_adventure_expansion.md.
What got built (deviations from the spec below, all intentional)
- Pete:
EquipOrder.Tier+tiercolumn (schema + migration); new actionsupgrade/repair; new verdictsrejected_downgrade/rejected_insufficient_funds/rejected_max_tier.EquipSlotView+Slots/BalanceonPlayerDetail.handleEquipOrderresolves take-off/upgrade/repair frompd.Slotsserver-side and rejects a client-forged tier (409). who.html gained an owner "Equipment" panel with an in-DOM confirm (cost + balance) for the money actions; public Gear panel hidden for the owner. output.css rebuilt. Tests in equip_test.go / who exercised via the real template. - gogobee:
peteclientmirror types. New filepete_equip_manage.goholds the headless mutators:applyMasterworkEquip/applyMasterworkUnequip(free funcs, sentinelerrEquipDowngrade),purchaseEquipmentTier/repairSlot(methods on*AdventurePlugin, euro-idempotent),buildEquipSlotViews,isEquipmentSlot.applyEquipOrderroutes on item Type / slot vocabulary.itemViewsnow gives masterwork/arena backpack rows an equip id.buildDetailSnapshotis now a METHOD (p.buildDetailSnapshot) so it can read the euro balance (nil-guarded for tests). Tests in pete_equip_manage_test.go. - DEVIATION 1 — downgrade block placement: the masterwork-equip downgrade check
lives INSIDE
applyMasterworkEquip(returnserrEquipDowngrade), not in the router. Behavior/verdict identical; keeps the rule next to the mutation and unit- testable. Router maps the sentinel torejected_downgrade. - DEVIATION 2 — no refund on save fault (both euro mutators): the spec suggested
CreditIdemrefund on a later DB error. That is UNSAFE with guid-idempotent retry: a refund on a fresh id followed by a guid-guarded retry that no longer re-debits hands the player both gear and money. Instead we returnretry=trueand let the next poll re-run — the debit is guid-idempotent (skipped) and the slot write is idempotent. This matches the casino escrow precedent exactly. Do NOT "fix" this by adding a refund. - DEVIATION 3 — upgrades only over PLAIN slots:
buildEquipSlotViewsoffersNextTieronly when the slot is plain shop-tier (not masterwork/arena) and sub-max, andpurchaseEquipmentTierrejects an upgrade over special gear asrejected_downgrade("take it off first"). This honors "block downgrades" (buying a plain tier over special strips its bonus) AND keeps the upgrade path free of the non-idempotent eviction step, so the retry-safety above holds with no eviction to reconcile.
Repos: Pete at /home/reala-misaki/git/pete (web mirror, deploys to parodia).
gogobee at /home/reala-misaki/git/gogobee (game engine, deploys to millenia
reala@192.168.1.212). One-way data flow gogobee→Pete; the only route back is the
poll-queue (Pete records intent, gogobee polls + applies + files a verdict).
Why this exists
Ask 5 built "equip from the web" but scoped it to magic items only. The user's worn gear is almost all the OTHER equipment systems, so the feature touched almost nothing they own. Diagnosis of user "prosolis" / character "Rurina" (live prod):
- Worn (the 5
adventure_equipmentslots): weapon Vorpal Sword T5 (shop, not mw), armor The Deepforged Carapace T5 masterwork, helmet Crown of the Fallen T5 (shop), boots Ranger's Boots T4 (shop), tool Mithril Pickaxe T4 (shop). - Backpack (250 items): 1
MasterworkGearThe Wandering Sole (boots, T3), 3 slotted magic items (Wand of the War Mage off_hand, 2 Weapons main_hand), rest consumables/ materials.equippedmagic count = 0 (magic slots empty). So today prosolis gets 3 buried Equip buttons (magic) and nothing else. They want to manage ALL five slots.
The three equipment subsystems (do not conflate — this is where it breaks)
- Standard tiered gear — the 5
EquipmentSlots (weapon/armor/helmet/boots/tool), power = integerTier0..5, raised by BUYING a tier in the shop (euros). No inventory item; the slot's tier IS the gear. This is 4 of Rurina's 5 worn pieces. - Masterwork / Arena gear — special items that live in the backpack
(
adventure_inventory,item_type=MasterworkGear/ArenaGear), equipped INTO anEquipmentSlot(a swap), round-trippable to the pack. - Magic items — backpack
item_type='magic_item', equipped into DnD slots (off_hand/main_hand/ring_1…), a DISJOINT slot namespace. Already handled by ask 5.
User decisions (locked)
- Scope: full management incl. shop-tier gear (not just inventory items).
- Sequencing: build BOTH phases, deploy together (one drop).
- Euro spend: yes, upgrade/repair debit euros from the web, but behind a confirm step showing cost + balance.
- Downgrades: BLOCK them (equip and upgrade).
HANDOFF STATE (2026-07-17 — resume here next session)
Both repos build, vet clean, full suites green. Uncommitted on purpose — screenshot- verify the Equipment panel FIRST, then commit each side, then deploy Pete→gogobee, then the live prosolis probe.
Order for next session:
- Screenshot-verify the owner Equipment panel (never rendered in a browser yet; the
TestEquipPanelRenderstest drives the real template but is not a visual check). Recipe (same as prior asks): a throwaway test callingseedEquip/getWho(both ininternal/web/equip_test.go/who_test.go) that writes the rendered body to an HTML file, served withpython3 -m http.serverfrom a dir with astaticsymlink intointernal/web/static.seedEquipalready seedsSlots(a masterwork weapon with Take off + Repair, plain boots with an Upgrade offer) +Balance100000, so the panel and the confirm dialog both render. Check day AND night phase; confirm the euro confirm box (cost + balance) pops on Upgrade/Repair click, and the€amounts read right. Watch the pete_theme_contrast purple-on-night hazard. - Commit Pete — every changed file below is ask 7 (incl. this doc):
internal/storage/{db,detail,equip,schema,equip_test}.go,internal/web/{equip,equip_test}.go,internal/web/templates/who.html,internal/web/static/css/output.css,adventure_ask7_equipment_mgmt.md. - Commit gogobee — stage ONLY the ask-7 files (the
gogobee_*.mdplan files are unrelated mid-flight postgame work — leave them):git add internal/peteclient/client.go internal/plugin/pete_detail_test.go internal/plugin/pete_equip.go internal/plugin/pete_roster.go internal/plugin/pete_equip_manage.go internal/plugin/pete_equip_manage_test.go - Deploy Pete first (build-on-server-with-cgo per deploy_topology), then gogobee on
millenia. A gogobee verdict string Pete's
validEquipVerdictrejected would 400+park the order, so Pete's ingest must accept the new verdicts before gogobee emits them. - Live prosolis probe (see the Verification section at the bottom). Expected: Take off on armor (Deepforged Carapace T5 masterwork), Upgrade offers on boots + tool (T4→T5, €25000), Repair where condition<100, Equip on The Wandering Sole BLOCKED as a downgrade vs worn T4 boots, + the 3 magic items.
Baseline unchanged: Pete tip 1159e64, gogobee tip b29dcf4. Nothing committed yet.
THE WIRE CONTRACT (both repos must agree)
equip_orders (Pete internal/storage/equip.go + gogobee peteclient.EquipOrder)
- New column
tier INTEGER NOT NULL DEFAULT 0onequip_orders(Peteschema.goCREATE at :176-191 +addColumnIfMissing(d,"equip_orders","tier",...)indb.go, following the existing pattern at db.go:81+). AddTier intto theEquipOrderstruct (Pete storage + gogobeepeteclient/client.go:689-698), plumb through Insert/scan/Pending/ByOwner and the JSON. - Actions (
Actionfield): existingequip,unequip; NEWupgrade,repair. - Field use per action:
equip(magic OR masterwork/arena):ItemID=adventure_inventoryrow id,Slot= the item's slot (DnD slot for magic, EquipmentSlot for masterwork).unequip/ take-off:Slotonly (DnD slot → magic path; EquipmentSlot → masterwork).upgrade:Slot= EquipmentSlot,Tier= target tier.ItemIDunused.repair:Slot= EquipmentSlot.Tier/ItemIDunused.
- Verdicts (add to Pete
validEquipVerdict+ gogobee return strings): existingapplied,rejected_not_owned,rejected_not_worn,rejected_not_equippable; NEWrejected_downgrade,rejected_insufficient_funds,rejected_max_tier. Give each a friendly message in Pete's who.html JSverdictmap (who.html ~:430).
Detail push (Pete internal/storage/detail.go PlayerDetail + gogobee peteclient)
Add to PlayerDetail:
Slots []EquipSlotView— the 5 standard slots, owner-only, for the management panel.Balance float64(json:"balance,omitempty") — the owner's euro balance, for the confirm dialogs.
New type (both repos):
type EquipSlotView struct {
Slot string `json:"slot"` // weapon|armor|helmet|boots|tool
Name string `json:"name"`
Tier int `json:"tier"`
Condition int `json:"condition"`
Masterwork bool `json:"masterwork,omitempty"`
ArenaTier int `json:"arena_tier,omitempty"`
CanTakeOff bool `json:"can_take_off,omitempty"` // masterwork/arena → round-trippable
NextTier int `json:"next_tier,omitempty"` // 0 = at max tier (5)
NextName string `json:"next_name,omitempty"`
NextPrice float64 `json:"next_price,omitempty"`
RepairCost int `json:"repair_cost,omitempty"` // 0 = full condition
}
Worn masterwork/arena pieces are represented HERE (via CanTakeOff), NOT duplicated
into Equipped. Equipped stays magic-only (the DnD slots). Backpack items
(Inventory) keep the ItemView shape; masterwork/arena backpack rows now also get an
equip ID (see gogobee itemViews change) so they render Equip buttons.
PHASE A — equip / take off inventory gear (masterwork + arena + magic). No money.
gogobee changes
itemViews(pete_roster.go:222-263): currently setsItemView.ID = it.IDonly for slotted magic items (:242-255); masterwork/arena backpack rows fall to theelse if it.Slot != ""branch (:256-259) with no id. ALSO setv.ID = it.IDwhenit.Type == "MasterworkGear" || it.Type == "ArenaGear"(they carry a slot). This is the whole reason a masterwork backpack item currently has no Equip button.attachInventoryCompares(pete_roster.go:269-280): it decorates any row withID != 0by callingmagicItemCompare. Now that masterwork rows have ids, GUARD it to magic-only (skip rows wheremagicItemFromAdvItemfails). Masterwork gets no compare card for now (fine).equippedViewsstays magic-only (pete_roster.go:408-431). Worn masterwork/arena are surfaced viaSlots/EquipSlotViewinstead (see Phase-common detail build).- Extract headless mutators mirroring
applyMagicEquip/applyMagicUnequip(magic_items_gameplay.go:592-654/:665-688) and the DM confirm logic (adventure_masterwork.go:487-587):applyMasterworkEquip(uid id.UserID, it AdvItem) (mwEquipOutcome, error): requireit.Slot != ""and Type MasterworkGear/ArenaGear elseerrItemNotEquippable(reuse the sentinel atmagic_items_gameplay.go:573). LoadloadAdvEquipment; if the current occupant is special (Masterwork || ArenaTier>0) evict it back to inventory as a MasterworkGear/ArenaGearAdvItem(see the confirm handler :530-545 for the exact reconstruction, incl.arenaGearByName(name).SetKeyat :563). Anti-dup ordering = magic's:removeAdvInventoryItem(it.ID)FIRST, thensaveAdvEquipment, restore the inventory row on save failure. Set the new row fields exactly like the confirm handler :547-571 (Tier, Condition=100, Name, ActionsUsed=0, and Masterwork+ SkillSource OR ArenaTier+ArenaSet).applyMasterworkUnequip(uid id.UserID, slot EquipmentSlot) (mwUnequipOutcome, error): load equip; if the slot is NOT special (!Masterwork && ArenaTier==0) →errSlotEmpty(:576) →rejected_not_worn(there is nothing round-trippable to take off; plain shop-tier reverts via Phase B, not here). Otherwise move the piece to inventory (MasterworkGear/ArenaGear AdvItem) and RESET the slot row to its tier-0 default:tier=0, condition=100, name = equipmentTiers[slot][0].Name, actions_used=0, arena_tier=0, arena_set='', masterwork=0, skill_source=''(matches the creation seed atadventure_character.go:528-537). Keep the row (do NOT delete — the 5 rows are an invariant; PK user_id+slot).
applyEquipOrder(pete_equip.go:124-173) routing:equip: load the AdvItem byorder.ItemID(as today, :126-152). Branch on Type:MasterworkGear/ArenaGear→applyMasterworkEquip(with downgrade block, below); else →applyMagicEquip(unchanged). Miss →rejected_not_owned.unequip: branch onorder.Slot: if it's an EquipmentSlot value (weapon/armor/helmet/boots/tool) →applyMasterworkUnequip(EquipmentSlot); else →applyMagicUnequip(DnDSlot). (Slot vocabularies are DISJOINT —EquipmentSlotvsDnDSlot— confirmed, so the string alone disambiguates.)
- Downgrade block (masterwork equip): before applying, compare
advEffectiveTier(incoming)vsadvEffectiveTier(currentOccupant)(adventure_character.go:421-432: arena ×1.5, masterwork ×1.25, else ×1). Ifincoming <= currentreturnrejected_downgrade. Magic equip is NOT downgrade-blocked (its target DnD slots are usually empty and the compare card already informs).
Pete changes (Phase A)
- Buttons already render off
ID/worn inwho.go itemRows(:123-145) + theitemrowtemplate (who.html :8-47). Take-off for masterwork slots is rendered fromSlots(see Pete Phase-common UI). Addrejected_downgradetovalidEquipVerdict(storage/equip.go:63) + the JS verdict map. handleEquipOrder(web/equip.go:47-134) already resolves an equip item frompd.Inventoryby id and an unequip frompd.Equippedby slot. Take-off of a masterwork slot comes fromSlots, so add resolution of a take-off/upgrade/repair againstpd.Slots(see Phase B handler notes — same code path).
PHASE B — upgrade / repair the 5 standard slots (web shop). Spends euros (confirm-gated).
gogobee changes
- Extract
purchaseEquipmentTier(uid id.UserID, slot EquipmentSlot, tier int, guid string) (outcome, error)from the body ofadvBuyEquipment(adventure_shop.go:742-829), MINUS flavor text:def := equipmentTiers[slot][tier](adventure_character.go:179-220; 6 tiers 0..5;EquipmentDef{Name,Tier,Description,Price}at :172-177). Guardtierin range;tier >= len→rejected_max_tier.- Downgrade block = existing shop rule: block if
float64(def.Tier) <= advEffectiveTier(current)for masterwork,def.Tier <= current.ArenaTierfor arena,current.Tier >= def.Tierfor plain (seeadventure_shop.go:744/ :750-757) →rejected_downgrade. - Idempotent euro:
if !p.euro.HasExternalTx(guid)gate the affordability check, thenp.euro.DebitIdem(uid, def.Price, "adventure_equip_upgrade", guid)(euro.go:452-461; balance/ok/err). Insufficient →rejected_insufficient_funds. Refund on later DB error viaCreditIdem(:465-475). DO NOT useDebit/Credit(non-idempotent) — the euro header ateuro.go:434-446says web-initiated MUST use the Idem variants. Precedent: casino escrowpete_games.go:101-130. - Move old special gear to inventory (like the shop does), then
saveAdvEquipmentwith the new tier row (Tier, Condition=100, Name=def.Name, ActionsUsed=0, Masterwork=false, ArenaTier=0). Community-pot 5% cut is OPTIONAL for web — decide; simplest to skip it or mirrorcommunityPotAdd(adventure_shop.go:564-567). - NOTE: shop charges FULL
def.Pricefor the chosen tier (not incremental). The web UI should offer upgrading to the NEXT tier only (NextTier/NextPrice in EquipSlotView) to keep it simple; the order carries the explicit targetTier.
- Extract headless repair from
executeRepair(adventure_blacksmith.go:262-333) — it already takes onlyuserID+ a confirm struct and is Matrix-free except the trailingSendDM.repair(uid, slot, guid): recomputeblacksmithRepairCost(eq)(:17-40, base rates:15),HasExternalTx-gate +DebitIdem(uid, cost, "adventure_repair", guid), seteq.Condition=100,saveAdvEquipment, refund on error. Condition already full →rejected_no_change(or justappliedno-op; pick one and add to the verdict set if used). - Poller routing in
applyEquipOrder:upgrade→purchaseEquipmentTier(owner, order.Slot, order.Tier, order.GUID);repair→repair(owner, order.Slot, order.GUID). The GUID is the idempotency key for BOTH the euro move (DebitIdem externalID) AND the existingequip_applied_ordersledger (pete_equip.go:213-240) — belt and suspenders. - Build
Slots+Balancein the detail push (buildDetailSnapshot/ PlayerDetail assemblypete_roster.go:190-211). For eachallSlotsslot readloadAdvEquipment(already used for public gear at rosterDetail :136): fill Name/Tier/ Condition/Masterwork/ArenaTier;CanTakeOff = Masterwork || ArenaTier>0;NextTier/NextName/NextPricefromequipmentTiers[slot][Tier+1]ifTier < 5and it isn't a downgrade;RepairCost = blacksmithRepairCost(eq)ifCondition < 100.Balance = p.euro.GetBalance(uid)(euro.go:408-417).
Pete changes (Phase B)
storage/equip.go: addTierto EquipOrder + Insert/scan/queries; add the three new verdicts tovalidEquipVerdict; addupgrade/repairtovalidEquipAction.web/equip.go handleEquipOrder: acceptupgrade/repairactions. Resolve the slot frompd.Slots(verify it exists and, for upgrade, thatreq.Tier == slot.NextTierandNextTier != 0; for repair thatRepairCost > 0). Reject client-forged tiers — trust only the pushedEquipSlotView, exactly as ask 5 resolves item facts server-side.web/who.html+who.go: build an owner "Equipment" panel from.Slots— per slot a card showing Name (T{Tier}, {Condition}%), and buttons: Take off ifCanTakeOff, Upgrade to {NextName} · €{NextPrice} ifNextTier>0, Repair · €{RepairCost} ifRepairCost>0. Magic worn + backpack panels stay as they are. Hide the public "Gear" panel (who.html :120-135) for the owner ({{if not .HasSelf}}) since this panel supersedes it. New CSS classes → rebuild + commitoutput.css(npx tailwindcss -i internal/web/static/css/input.css -o …/output.css --minify).- Confirm step: for
upgrade/repair(euro-spending) the JS must pop a confirm showing cost +page.Balancebefore POSTing the order (per user decision).equip/unequip/take-off place directly (no money). Reuse the equip JS at who.html ~:416-490.
Cross-cutting / gotchas
- Deploy order: Pete ingest + verdict handlers accept the new actions/verdicts BEFORE
gogobee emits them. New order actions are additive on Pete's side. New DETAIL fields are
omitempty→ safe either order. But a new gogobee VERDICT string that Pete'svalidEquipVerdictrejects would 400 and park the order — so ship Pete first. (Same rule as ask 1's event_type.) - Pete builds WITH cgo ON the server (sqlite). See
deploy_topologymemory. gogobee builds on millenia. Deploy = push to gitea → servergit pull --ff-only+CGO_ENABLED=1 go build -o pete.new .→ swap → restart screenpete. - output.css is a committed build artifact — new Tailwind classes silently no-op in
prod if not rebuilt+committed (bit us on
sm:grid-cols-5). - Euro debt limit applies (
BLACKJACK_DEBT_LIMITdefault −1000). A web upgrade that would breach it is refused byDebitIdem→rejected_insufficient_funds. - Idempotency is doubled: the order GUID keys BOTH
DebitIdem's externalID AND theequip_applied_ordersledger. A retried poll re-files the stored verdict and moves no money. Verify a mid-apply crash can't double-charge (DebitIdem is the guard; record the applied-order ledger AFTER a successful apply, as fulfilEquipOrder already does :104).
Testing (both repos)
- gogobee: unit-test each headless mutator; assert downgrade block, max-tier, insufficient- funds, idempotent replay (same guid twice → one debit), masterwork equip evicts special occupant / overwrites plain, take-off resets to tier-0.
- Pete: seed a PlayerDetail with
Slots+ a masterwork worn piece + masterwork backpack item and assert the who page renders Take off / Upgrade / Repair buttons and the confirm data. Use theseedWho/getWhothrowaway render pattern (seeproject_adventure_expansionmemory + prior sessions' scratch test). AsserthandleEquipOrderrejects a client-forged tier and a non-owner. - Verify verdict strings round-trip;
TestClearCookie…-style table tests fit.
Verification against live data (prod probe scripts were in this session's scratchpad,
which is EPHEMERAL — re-create as needed). Read prod detail via
ssh reala@www.parodia.dev 'cd /opt/pete && python3 -' piping a small script that opens
data/pete.db and json-loads player_self_detail.detail_json (localpart 'prosolis') or
adventure_roster.detail_json (name 'rurina'). Expected post-ship for prosolis: Take off
on armor (Deepforged Carapace), Upgrade offered on weapon→? (already T5 max → none),
boots Upgrade T4→T5 (€25000) etc., Repair where condition<100, Equip on The Wandering
Sole (BLOCKED as downgrade vs worn T4 boots) + 3 magic items.
Deployed baseline at handoff
- Pete tip
1159e64live on parodia. gogobee tipb29dcf4live on millenia (contains all ask 1–6 commits). Nothing for ask 7 written yet. gogobee has unrelated uncommitted postgame-zone work in its tree — keep ask-7 edits in separate commits, stage by name.
Build order (tasks)
- Wire contract types both repos (EquipOrder.Tier, EquipSlotView, verdicts, actions).
- gogobee headless mutators (masterwork equip/unequip, purchaseEquipmentTier, repair).
- gogobee detail push (Slots + Balance; itemViews masterwork id; compare guard).
- gogobee poller routing + downgrade block.
- Pete storage (tier column, actions, verdicts) + handlers.
- Pete who.html/who.go equipment panel + confirm JS + hide public Gear for owner.
- Tests both sides; rebuild+commit output.css; gofmt.
- Deploy Pete first, then gogobee; verify live with prosolis.