adventure: scope ask 6 (item upgrade comparison), verified both sides
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user