adventure: ask 7 — full equipment management from the web
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.
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
# 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` + `tier` column (schema + migration); new actions
|
||||
`upgrade`/`repair`; new verdicts `rejected_downgrade`/`rejected_insufficient_funds`/
|
||||
`rejected_max_tier`. `EquipSlotView` + `Slots`/`Balance` on `PlayerDetail`.
|
||||
`handleEquipOrder` resolves take-off/upgrade/repair from `pd.Slots` server-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: `peteclient` mirror types. New file `pete_equip_manage.go` holds the
|
||||
headless mutators: `applyMasterworkEquip`/`applyMasterworkUnequip` (free funcs,
|
||||
sentinel `errEquipDowngrade`), `purchaseEquipmentTier`/`repairSlot` (methods on
|
||||
`*AdventurePlugin`, euro-idempotent), `buildEquipSlotViews`, `isEquipmentSlot`.
|
||||
`applyEquipOrder` routes on item Type / slot vocabulary. `itemViews` now gives
|
||||
masterwork/arena backpack rows an equip id. `buildDetailSnapshot` is 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` (returns `errEquipDowngrade`), not in the
|
||||
router. Behavior/verdict identical; keeps the rule next to the mutation and unit-
|
||||
testable. Router maps the sentinel to `rejected_downgrade`.
|
||||
- **DEVIATION 2 — no refund on save fault (both euro mutators):** the spec suggested
|
||||
`CreditIdem` refund 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 return `retry=true` and 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:** `buildEquipSlotViews` offers
|
||||
`NextTier` only when the slot is plain shop-tier (not masterwork/arena) and sub-max,
|
||||
and `purchaseEquipmentTier` rejects an upgrade over special gear as
|
||||
`rejected_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_equipment` slots): 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 `MasterworkGear` **The Wandering Sole** (boots, T3), 3 slotted
|
||||
magic items (Wand of the War Mage off_hand, 2 Weapons main_hand), rest consumables/
|
||||
materials. `equipped` magic 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)
|
||||
1. **Standard tiered gear** — the 5 `EquipmentSlot`s (weapon/armor/helmet/boots/tool),
|
||||
power = integer `Tier` 0..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.
|
||||
2. **Masterwork / Arena gear** — special items that live in the backpack
|
||||
(`adventure_inventory`, `item_type` = `MasterworkGear` / `ArenaGear`), equipped INTO
|
||||
an `EquipmentSlot` (a swap), round-trippable to the pack.
|
||||
3. **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:**
|
||||
1. **Screenshot-verify** the owner Equipment panel (never rendered in a browser yet; the
|
||||
`TestEquipPanelRenders` test drives the real template but is not a visual check).
|
||||
Recipe (same as prior asks): a throwaway test calling `seedEquip`/`getWho` (both in
|
||||
`internal/web/equip_test.go` / `who_test.go`) that writes the rendered body to an HTML
|
||||
file, served with `python3 -m http.server` from a dir with a `static` symlink into
|
||||
`internal/web/static`. `seedEquip` already seeds `Slots` (a masterwork weapon with
|
||||
Take off + Repair, plain boots with an Upgrade offer) + `Balance` 100000, 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.
|
||||
2. **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`.
|
||||
3. **Commit gogobee** — stage ONLY the ask-7 files (the `gogobee_*.md` plan 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`
|
||||
4. **Deploy Pete first** (build-on-server-with-cgo per deploy_topology), then gogobee on
|
||||
millenia. A gogobee verdict string Pete's `validEquipVerdict` rejected would 400+park
|
||||
the order, so Pete's ingest must accept the new verdicts before gogobee emits them.
|
||||
5. **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 0` on `equip_orders`
|
||||
(Pete `schema.go` CREATE at :176-191 + `addColumnIfMissing(d,"equip_orders","tier",...)`
|
||||
in `db.go`, following the existing pattern at db.go:81+). Add `Tier int` to the
|
||||
`EquipOrder` struct (Pete storage + gogobee `peteclient/client.go:689-698`), plumb
|
||||
through Insert/scan/Pending/ByOwner and the JSON.
|
||||
- **Actions** (`Action` field): existing `equip`, `unequip`; NEW `upgrade`, `repair`.
|
||||
- **Field use per action:**
|
||||
- `equip` (magic OR masterwork/arena): `ItemID` = `adventure_inventory` row id,
|
||||
`Slot` = the item's slot (DnD slot for magic, EquipmentSlot for masterwork).
|
||||
- `unequip` / take-off: `Slot` only (DnD slot → magic path; EquipmentSlot → masterwork).
|
||||
- `upgrade`: `Slot` = EquipmentSlot, `Tier` = target tier. `ItemID` unused.
|
||||
- `repair`: `Slot` = EquipmentSlot. `Tier`/`ItemID` unused.
|
||||
- **Verdicts** (add to Pete `validEquipVerdict` + gogobee return strings): existing
|
||||
`applied`, `rejected_not_owned`, `rejected_not_worn`, `rejected_not_equippable`;
|
||||
NEW `rejected_downgrade`, `rejected_insufficient_funds`, `rejected_max_tier`.
|
||||
Give each a friendly message in Pete's who.html JS `verdict` map (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):
|
||||
```go
|
||||
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
|
||||
1. **`itemViews`** (`pete_roster.go:222-263`): currently sets `ItemView.ID = it.ID`
|
||||
only for slotted magic items (:242-255); masterwork/arena backpack rows fall to the
|
||||
`else if it.Slot != ""` branch (:256-259) with no id. ALSO set `v.ID = it.ID` when
|
||||
`it.Type == "MasterworkGear" || it.Type == "ArenaGear"` (they carry a slot). This is
|
||||
the whole reason a masterwork backpack item currently has no Equip button.
|
||||
2. **`attachInventoryCompares`** (`pete_roster.go:269-280`): it decorates any row with
|
||||
`ID != 0` by calling `magicItemCompare`. Now that masterwork rows have ids, GUARD it
|
||||
to magic-only (skip rows where `magicItemFromAdvItem` fails). Masterwork gets no
|
||||
compare card for now (fine).
|
||||
3. **`equippedViews`** stays magic-only (`pete_roster.go:408-431`). Worn masterwork/arena
|
||||
are surfaced via `Slots`/`EquipSlotView` instead (see Phase-common detail build).
|
||||
4. **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)`:
|
||||
require `it.Slot != ""` and Type MasterworkGear/ArenaGear else `errItemNotEquippable`
|
||||
(reuse the sentinel at `magic_items_gameplay.go:573`). Load `loadAdvEquipment`; if the
|
||||
current occupant is special (`Masterwork || ArenaTier>0`) evict it back to inventory
|
||||
as a MasterworkGear/ArenaGear `AdvItem` (see the confirm handler :530-545 for the
|
||||
exact reconstruction, incl. `arenaGearByName(name).SetKey` at :563). **Anti-dup
|
||||
ordering = magic's**: `removeAdvInventoryItem(it.ID)` FIRST, then `saveAdvEquipment`,
|
||||
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 at `adventure_character.go:528-537`). Keep the row (do NOT delete — the 5 rows
|
||||
are an invariant; PK user_id+slot).
|
||||
5. **`applyEquipOrder`** (`pete_equip.go:124-173`) routing:
|
||||
- `equip`: load the AdvItem by `order.ItemID` (as today, :126-152). Branch on Type:
|
||||
`MasterworkGear`/`ArenaGear` → `applyMasterworkEquip` (with downgrade block, below);
|
||||
else → `applyMagicEquip` (unchanged). Miss → `rejected_not_owned`.
|
||||
- `unequip`: branch on `order.Slot`: if it's an EquipmentSlot value
|
||||
(weapon/armor/helmet/boots/tool) → `applyMasterworkUnequip(EquipmentSlot)`;
|
||||
else → `applyMagicUnequip(DnDSlot)`. (Slot vocabularies are DISJOINT —
|
||||
`EquipmentSlot` vs `DnDSlot` — confirmed, so the string alone disambiguates.)
|
||||
6. **Downgrade block** (masterwork equip): before applying, compare
|
||||
`advEffectiveTier(incoming)` vs `advEffectiveTier(currentOccupant)`
|
||||
(`adventure_character.go:421-432`: arena ×1.5, masterwork ×1.25, else ×1). If
|
||||
`incoming <= current` return `rejected_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 in `who.go itemRows` (:123-145) + the `itemrow`
|
||||
template (who.html :8-47). Take-off for masterwork slots is rendered from `Slots`
|
||||
(see Pete Phase-common UI). Add `rejected_downgrade` to `validEquipVerdict`
|
||||
(`storage/equip.go:63`) + the JS verdict map.
|
||||
- `handleEquipOrder` (`web/equip.go:47-134`) already resolves an equip item from
|
||||
`pd.Inventory` by id and an unequip from `pd.Equipped` by slot. Take-off of a masterwork
|
||||
slot comes from `Slots`, so add resolution of a take-off/upgrade/repair against
|
||||
`pd.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
|
||||
1. **Extract `purchaseEquipmentTier(uid id.UserID, slot EquipmentSlot, tier int, guid string) (outcome, error)`**
|
||||
from the body of `advBuyEquipment` (`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). Guard `tier` in range;
|
||||
`tier >= len` → `rejected_max_tier`.
|
||||
- **Downgrade block** = existing shop rule: block if `float64(def.Tier) <= advEffectiveTier(current)`
|
||||
for masterwork, `def.Tier <= current.ArenaTier` for arena, `current.Tier >= def.Tier`
|
||||
for plain (see `adventure_shop.go:744` / :750-757) → `rejected_downgrade`.
|
||||
- **Idempotent euro**: `if !p.euro.HasExternalTx(guid)` gate the affordability check,
|
||||
then `p.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 via `CreditIdem` (`:465-475`). DO NOT use `Debit`/`Credit`
|
||||
(non-idempotent) — the euro header at `euro.go:434-446` says web-initiated MUST use
|
||||
the Idem variants. Precedent: casino escrow `pete_games.go:101-130`.
|
||||
- Move old special gear to inventory (like the shop does), then `saveAdvEquipment`
|
||||
with 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 mirror `communityPotAdd` (`adventure_shop.go:564-567`).
|
||||
- NOTE: shop charges FULL `def.Price` for 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 target `Tier`.
|
||||
2. **Extract headless repair** from `executeRepair` (`adventure_blacksmith.go:262-333`)
|
||||
— it already takes only `userID` + a confirm struct and is Matrix-free except the
|
||||
trailing `SendDM`. `repair(uid, slot, guid)`: recompute `blacksmithRepairCost(eq)`
|
||||
(`:17-40`, base rates `:15`), `HasExternalTx`-gate + `DebitIdem(uid, cost,
|
||||
"adventure_repair", guid)`, set `eq.Condition=100`, `saveAdvEquipment`, refund on error.
|
||||
Condition already full → `rejected_no_change` (or just `applied` no-op; pick one and add
|
||||
to the verdict set if used).
|
||||
3. **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
|
||||
existing `equip_applied_orders` ledger (`pete_equip.go:213-240`) — belt and suspenders.
|
||||
4. **Build `Slots` + `Balance` in the detail push** (`buildDetailSnapshot` /
|
||||
PlayerDetail assembly `pete_roster.go:190-211`). For each `allSlots` slot read
|
||||
`loadAdvEquipment` (already used for public gear at rosterDetail :136): fill Name/Tier/
|
||||
Condition/Masterwork/ArenaTier; `CanTakeOff = Masterwork || ArenaTier>0`;
|
||||
`NextTier/NextName/NextPrice` from `equipmentTiers[slot][Tier+1]` if `Tier < 5` and it
|
||||
isn't a downgrade; `RepairCost = blacksmithRepairCost(eq)` if `Condition < 100`.
|
||||
`Balance = p.euro.GetBalance(uid)` (`euro.go:408-417`).
|
||||
|
||||
### Pete changes (Phase B)
|
||||
- `storage/equip.go`: add `Tier` to EquipOrder + Insert/scan/queries; add the three new
|
||||
verdicts to `validEquipVerdict`; add `upgrade`/`repair` to `validEquipAction`.
|
||||
- `web/equip.go handleEquipOrder`: accept `upgrade`/`repair` actions. Resolve the slot
|
||||
from `pd.Slots` (verify it exists and, for upgrade, that `req.Tier == slot.NextTier`
|
||||
and `NextTier != 0`; for repair that `RepairCost > 0`). Reject client-forged tiers —
|
||||
trust only the pushed `EquipSlotView`, 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** if `CanTakeOff`,
|
||||
**Upgrade to {NextName} · €{NextPrice}** if `NextTier>0`, **Repair · €{RepairCost}** if
|
||||
`RepairCost>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 + commit `output.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.Balance` before 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's
|
||||
`validEquipVerdict` rejects 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_topology` memory. gogobee
|
||||
builds on millenia. Deploy = push to gitea → server `git pull --ff-only` +
|
||||
`CGO_ENABLED=1 go build -o pete.new .` → swap → restart screen `pete`.
|
||||
- **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_LIMIT` default −1000). A web upgrade that
|
||||
would breach it is refused by `DebitIdem` → `rejected_insufficient_funds`.
|
||||
- **Idempotency is doubled**: the order GUID keys BOTH `DebitIdem`'s externalID AND the
|
||||
`equip_applied_orders` ledger. 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 the `seedWho`/`getWho` throwaway render pattern (see
|
||||
`project_adventure_expansion` memory + prior sessions' scratch test). Assert
|
||||
`handleEquipOrder` rejects 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 `1159e64` live on parodia. gogobee tip `b29dcf4` live 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)
|
||||
1. Wire contract types both repos (EquipOrder.Tier, EquipSlotView, verdicts, actions).
|
||||
2. gogobee headless mutators (masterwork equip/unequip, purchaseEquipmentTier, repair).
|
||||
3. gogobee detail push (Slots + Balance; itemViews masterwork id; compare guard).
|
||||
4. gogobee poller routing + downgrade block.
|
||||
5. Pete storage (tier column, actions, verdicts) + handlers.
|
||||
6. Pete who.html/who.go equipment panel + confirm JS + hide public Gear for owner.
|
||||
7. Tests both sides; rebuild+commit output.css; gofmt.
|
||||
8. Deploy Pete first, then gogobee; verify live with prosolis.
|
||||
Reference in New Issue
Block a user