D&D: wire the Open5e magic-item registry into live gameplay

Magic items now reach players through three surfaces: zone loot drops,
Luigi's "Curios" shelf, and combat effects. Effects are formulaic
(Rarity scalar x Kind), mirroring the bestiary tuning pass, with an
empty magicItemEffectOverlay as the hand-authored refinement path.

- magic_items_gameplay.go: rarity index, magic_item_equipped persistence
  (new table, DnDSlot-keyed), codified effect formula, applyMagicItemEffects
  combat hook, potion/scroll -> ConsumableDef bridge, !adventure equip-magic
- dropZoneLoot: 15% magic-item substitution roll by tier rarity
- Luigi's Curios category: daily UTC-seeded 8-item rotation
- combat_bridge / combat_session_build: applyMagicItemEffects after passives
- consumableDefByName falls through so loot/shop potions auto-resolve
- renderDnDSheet: new Magic Items section

Equippable items live entirely in the DnDSlot scheme, separate from the
legacy tier-gear. Attunement items equip inert until attuned (3-slot cap).
This commit is contained in:
prosolis
2026-05-14 18:38:57 -07:00
parent 297ce3d786
commit 0d666beea3
12 changed files with 996 additions and 10 deletions

View File

@@ -64,11 +64,12 @@ func (p *AdventurePlugin) handleDnDSheetCmd(ctx MessageContext) error {
treasures, _ := loadAdvTreasureBonuses(ctx.Sender)
meta, _ := loadPlayerMeta(ctx.Sender)
house, _ := loadHouseState(ctx.Sender)
magicEquip, _ := loadEquippedMagicItems(ctx.Sender)
return p.SendDM(ctx.Sender, renderDnDSheet(c, advChar, meta, house, equip, treasures))
return p.SendDM(ctx.Sender, renderDnDSheet(c, advChar, meta, house, equip, treasures, magicEquip))
}
func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta, house HouseState, equip map[EquipmentSlot]*AdvEquipment, treasures []AdvTreasureBonus) string {
func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta, house HouseState, equip map[EquipmentSlot]*AdvEquipment, treasures []AdvTreasureBonus, magicEquip map[DnDSlot]EquippedMagicItem) string {
ri, _ := raceInfo(c.Race)
ci, _ := classInfo(c.Class)
mods := c.Modifiers()
@@ -137,6 +138,29 @@ func renderDnDSheet(c *DnDCharacter, adv *AdventureCharacter, meta *PlayerMeta,
b.WriteString(" _(none equipped)_\n")
}
// Magic items — registry items worn in the D&D slots (separate from the
// legacy tier-gear above). Attunement items show whether they're active.
if len(magicEquip) > 0 {
b.WriteString("\n**Magic Items**\n")
for _, ds := range dndSlotOrder {
e, ok := magicEquip[ds]
if !ok || e.Item.ID == "" {
continue
}
status := ""
if e.Item.Attunement {
if e.Attuned {
status = " — attuned"
} else {
status = " — _inert (unattuned)_"
}
}
b.WriteString(fmt.Sprintf(" %s %-9s %s _(%s)_%s\n %s\n",
rarityIcon(e.Item.Rarity), string(ds), e.Item.Name, e.Item.Rarity,
status, magicItemEffectSummary(e.Item)))
}
}
// Attunements (re-using adventure_treasures per v1.1 §7.4)
if len(treasures) > 0 {
b.WriteString("\n**Attunements** (treasures)\n")