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
|
5. **Ask 2 (LLM dispatches)** — last, because the prose guard is the only piece
|
||||||
here that fails *publicly* if it is wrong.
|
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
|
## Follow-ups, not covered by any of the five
|
||||||
|
|
||||||
Both Pete-only.
|
Both Pete-only.
|
||||||
|
|||||||
Reference in New Issue
Block a user