adventure: scope ask 6 (item upgrade comparison), verified both sides

This commit is contained in:
prosolis
2026-07-17 09:42:18 -07:00
parent 4f4bd9fbc1
commit 3230939c51

View File

@@ -522,6 +522,139 @@ page reflects the row.
5. **Ask 2 (LLM dispatches)** — last, because the prose guard is the only piece
here that fails *publicly* if it is wrong.
## 6. Item comparison — "is this backpack item an upgrade?" (scoped 2026-07-17)
A sixth ask, scoped after all five shipped. On the owner's own page, hovering a
backpack magic item shows a **compare card**: the item currently worn in that
same slot and the per-stat deltas, so the player can answer "is this better than
what I've got on?" without scrolling between two panels and eyeballing two
opaque effect strings.
**This section was written AFTER verifying both repos** (unlike §§1-5, which were
written a side at a time and were wrong wherever the two sides disagreed). The
findings below are checked against the gogobee engine, not assumed — but treat
the *design decisions* (verdict semantics, ring handling) as proposals, not
settled, and re-confirm the file refs at build time.
### The load-bearing constraint (same one as §4)
**Pete must not compute the comparison.** The ask-4 lesson: a display-only power
approximation *lies the moment it disagrees with the engine*, and character math
lives in gogobee. That is doubly true here — the comparison depends on three
things Pete does not hold:
- **Tempering.** An item's effective power folds in its per-instance temper
(`EquippedMagicItem.Effective()``temperedItem`, `magic_items_gameplay.go:252`).
The worn item and the backpack item each temper differently; the diff must be
over *tempered* effects.
- **Bond availability.** An attunement item worn with no free bond is **inert**
equipping it changes nothing. So the honest verdict for such an item is "would
sit inert until you free a bond," not "downgrade." Same distinction §4/ask 4
drew, resurfacing on the compare card. Bond state is engine-side.
- **Which slot it lands in** (rings — see below).
So gogobee computes the comparison and pushes it; Pete renders it. **Simple diff
arithmetic, engine-side inputs.**
### What the comparison is built from — this is the good news
The Worn-panel items are **magic items**, and their power is a *structured
numeric struct*, not just the opaque `Effect` string:
`magicItemEffect{DamageBonus float64, DamageReductMult float64, FlatDmgStart int,
InitiativeBias float64, MaxHP int}` (`magic_items_gameplay.go:179`), derived by
`magicItemEffectFor(mi)` (`:212`) keyed on Kind+Rarity with a per-item overlay.
So a real field-by-field diff exists. **Diff the structs, never parse the
`Effect` summary string** — the summary drops any field that is zero, so parsing
it back loses deltas.
Direction of "better" per field (the verdict logic needs this):
`DamageBonus` ↑, `FlatDmgStart` ↑, `InitiativeBias` ↑, `MaxHP` ↑ are gains;
`DamageReductMult` is a multiplier on damage taken in `(0,1]`, so **lower is
better** (0.90 = 10% damage taken).
### NOT the masterwork gear
There are two equip systems. This ask is the **magic-item** Worn/backpack panels
(`MagicItem` / `DnDSlot`, effects via `magicItemEffectFor`). The other is
masterwork `EquipmentSlot`/`AdvEquipment` gear feeding `DerivePlayerStats`
`CombatStats` (`combat_stats.go`). Do **not** cross the streams — different
slots, different power model, scoped out in ask 5. A compare only ever pairs a
magic item against a magic item.
### Payload
Additive `Compare` sub-object on the backpack `ItemView` (`peteclient.ItemView`,
mirrored in Pete `storage.ItemView`). Owner-private — it rides the PlayerDetail
`detail_json` blob (**no migration**, no new endpoint, no public exposure), same
channel as §4. `omitempty` → safe deploy order either way. Item names are
game-authored (not player names), so **no prose-guard / injection surface** like
§2.
```json
"compare": {
"verdict": "upgrade | downgrade | sidegrade | same | new | inert",
"vs_name": "Ring of Protection", // worn item being replaced; "" when verdict=new
"vs_slot": "ring_2", // the slot it would land in (see rings)
"deltas": [
{"label": "damage", "better": true, "text": "+3% damage"},
{"label": "hp", "better": false, "text": "-4 HP"}
]
}
```
`deltas` is engine-rendered player-facing text (like `magicItemEffectSummary`),
one entry per changed field, each flagged `better`. Pete renders chips/arrows off
`better`; it does no math.
### Verdict semantics (proposal — confirm)
Different stats are **not fungible** — the engine cannot say +3% damage beats
4 HP. So use **strict dominance**, and let the player judge the mixed case:
- **upgrade** — every delta a gain (≥), at least one strict.
- **downgrade** — every delta a loss (≤), at least one strict.
- **sidegrade** — mixed. *This is the case the string-only view could never
show, and the reason this ask exists.* Show all deltas; claim no winner.
- **same** — no field differs.
- **new** — the target slot is empty; frame as "equips into an empty <slot>,"
all-gain but labelled a fill, not a replacement.
- **inert** — attunement item, no free bond: overrides the stat verdict, because
wearing it does nothing until a bond frees.
### Rings are double-slot (the subtlety to decide)
`DnDSlotRing1` / `DnDSlotRing2` (`dnd_equipment.go:24-25`) — two ring slots. A
backpack ring has to compare against *something*. Proposal: compare against the
**weaker of the two worn rings** (the one a sensible player would replace); if
either ring slot is empty, verdict `new` (fills the empty slot). Flag: this is a
judgement call, not derivable — confirm before building.
### Integration points
- **gogobee:** `itemViews` (`pete_roster.go:219`) builds the backpack ItemViews
but does **not** currently know the equipped set (it is shared with the vault
call). The compare needs the equipped magic items in scope — pass them into a
backpack-only post-pass, or a new `itemViewsWithCompare(inv, equipped)`. Attach
`Compare` for backpack magic items only (vault rows carry an id today but no
button; compare follows the same "backpack only" rule). Reuse
`magicItemEffectFor` on `temperedItem(...)` for both sides; read bonds from
`loadEquippedMagicItems`/the attune cap.
- **Pete:** `itemRow` (`internal/web/who.go:111`) already carries per-panel state;
add the compare card to the backpack row template in `who.html`. Hover-tooltip
on desktop. **Mobile has no hover** — render an always-visible verdict chip
(⬆ upgrade / ⬇ downgrade / ⇄ sidegrade / ✦ new / ⚠ inert) and put the full
deltas behind a tap. New Tailwind classes → rebuild+commit `output.css` (the
committed-artifact trap from every prior ask). Screenshot-verify day + night.
### Cost / honest scope
The engine work is small (one diff function over an existing struct, plus the
ring/bond decisions). The UI is the bulk: a tooltip that also degrades to a
tap-target on touch, styled for both phases. No new table, no migration, no new
endpoint, no public surface. Deploy order free (additive private field).
---
## Follow-ups, not covered by any of the five
Both Pete-only.