mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
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:
@@ -376,11 +376,25 @@ func rngIntN(rng *rand.Rand, n int) int {
|
||||
// it into adventure_inventory if one fired, and returns a narration block
|
||||
// (empty when no drop). Reuses §5 LootDropCommon/Uncommon/Rare/Legendary
|
||||
// flavor pools — no new flavor file (per feedback_reuse_existing_flavor).
|
||||
// magicItemDropChance — once a drop fires, the chance it is substituted with
|
||||
// an Open5e SRD registry magic item of the rolled tier's rarity instead of
|
||||
// the biome slate item. Keeps magic items a treat, not the norm.
|
||||
const magicItemDropChance = 0.15
|
||||
|
||||
func (p *AdventurePlugin) dropZoneLoot(userID id.UserID, zoneID ZoneID, monster DnDMonsterTemplate, isBoss bool) string {
|
||||
entry, tier, ok := rollZoneLoot(zoneID, monster.CR, isBoss, nil)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Magic-item substitution: swap the biome slate item for a registry
|
||||
// magic item of the same rarity tier (see magic_items_gameplay.go).
|
||||
if rngFloat(nil) < magicItemDropChance {
|
||||
if mi, miOK := pickMagicItemForRarity(tier.rarity(), nil); miOK {
|
||||
return p.dropMagicItemLoot(userID, mi, tier)
|
||||
}
|
||||
}
|
||||
|
||||
tierVal := tier
|
||||
zoneTier := zoneTierFromID(zoneID)
|
||||
item := AdvItem{
|
||||
@@ -410,6 +424,33 @@ func (p *AdventurePlugin) dropZoneLoot(userID id.UserID, zoneID ZoneID, monster
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dropMagicItemLoot deposits a registry magic item as a combat-victory drop
|
||||
// and returns its narration block. Potions/scrolls land as "consumable" so
|
||||
// the combat pipeline auto-uses them; everything else is a "magic_item"
|
||||
// sellable/equippable via the Curios shelf and `!adventure equip-magic`.
|
||||
func (p *AdventurePlugin) dropMagicItemLoot(userID id.UserID, mi MagicItem, tier LootTier) string {
|
||||
item := magicItemSell(mi)
|
||||
item.SkillSource = "magic_item:" + mi.ID
|
||||
if err := addAdvInventoryItem(userID, item); err != nil {
|
||||
return fmt.Sprintf("_(Loot drop persistence error: %v.)_", err)
|
||||
}
|
||||
var b strings.Builder
|
||||
if line := lootFlavorLine(tier); line != "" {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
tag := string(mi.Rarity)
|
||||
if mi.Attunement {
|
||||
tag += ", attunement"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("✨ **%s** (%s %s) — %d coin baseline.",
|
||||
mi.Name, mi.Kind, tag, mi.Value))
|
||||
if mi.Desc != "" {
|
||||
b.WriteString(fmt.Sprintf(" _%s_", mi.Desc))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// lootFlavorLine maps a tier to one of the existing TwinBee loot pools.
|
||||
// Epic falls through to the Rare pool (next-best fit); Legendary uses its
|
||||
// own pool. No new flavor file written here.
|
||||
|
||||
Reference in New Issue
Block a user