Adv 2.0 D&D Phase R R3-R5: Combat-link, zone loot, fishing, economy

R3 — Combat Event Integration:
- dnd_expedition_combat.go: Combat Interrupt rolls (§4.2) with
  threat-clock and Ranger-wilderness modifiers; Patrol Encounters
  scaled by threat level; recordZoneKill writer with monsterKillTags.
- Interrupt gate at head of handleHarvestCmd; patrol gate before
  resolveRoom in zoneCmdAdvance; kill writer wired into combat-win
  paths.

R4a — Zone Loot Tables:
- dnd_zone_loot.go: §5 loot drop tables for all 10 zones × 5 tiers
  with §8.1 sell-value bands. dropTierFromCR brackets + boss floor.
- Hooks on combat-win in resolveCombatRoom, resolveBossRoom,
  runHarvestInterrupt, tryPatrolEncounter.

R4b — Fishing Integration:
- dnd_zone_fish.go: fishingZones allow-list, fishingSkillBonus,
  rangerRareCatchUpgrade (§6.2), feywildFishDistortion narration.
- §6.1 fish entries added to resource registry for Forest, Sunken
  Temple, Underdark, Feywild zones.
- !fish wired through handleHarvestCmd; zoneItemFlavor matrix
  populated for all 10 zones × all loot items.

R5 — Economy Integration:
- dnd_economy.go: !sell (post-expedition gate, single CHA Persuasion
  DC 17 → +15% bump), !craft (§8.2 4 exemplar recipes), !lore
  (INT/Arcana DC 15 recipe discovery).
- dnd_known_recipe table for persistent recipe discovery.

Flavor reuse: HarvestInterrupt, PatrolEncounter, CombatVictory,
PlayerDeath, LootDrop*, FeywildTimeDistortion* — all existing pools.
No new flavor file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 18:31:27 -07:00
parent 83cdf07374
commit c170adaf05
13 changed files with 2507 additions and 0 deletions

View File

@@ -0,0 +1,603 @@
package plugin
import (
"fmt"
"math/rand/v2"
"strings"
"gogobee/internal/flavor"
"maunium.net/go/mautrix/id"
)
// Resource & Combat Integration §5 — Zone-Contextual Loot Tables.
//
// Drops fire on combat victory (room/elite/boss/harvest-interrupt/patrol).
// Drop tier is selected from enemy CR per the §5 bracket table; the item
// itself is rolled from a per-zone slate so a goblin warrens uncommon
// reads differently than an underdark uncommon. Sell values follow the
// §8.1 rarity brackets so R5's `!sell` can pay out cleanly later.
//
// New file lives next to dnd_zone_loot.go; no existing files touched
// outside the combat-resolution call sites that invoke `dropZoneLoot`.
// LootTier is the dropped-item rarity bracket selected by enemy CR.
type LootTier string
const (
LootTierCommon LootTier = "common"
LootTierUncommon LootTier = "uncommon"
LootTierRare LootTier = "rare"
LootTierEpic LootTier = "epic"
LootTierLegendary LootTier = "legendary"
)
// ZoneLootDrop is one item slot in a zone's loot table.
type ZoneLootDrop struct {
Name string
ItemType string // "consumable", "scroll", "trinket", "gear", "trophy", "currency"
BaseValue int // §8.1 midpoint
}
// rarityFor maps the loot tier to a DnDRarity (kept separate so the
// §8.1 sell brackets line up cleanly with the equipment rarity scheme).
func (t LootTier) rarity() DnDRarity {
switch t {
case LootTierCommon:
return RarityCommon
case LootTierUncommon:
return RarityUncommon
case LootTierRare:
return RarityRare
case LootTierEpic:
return RarityEpic
case LootTierLegendary:
return RarityLegendary
}
return RarityCommon
}
// dropTierFromCR implements the §5 enemy-CR → drop-tier table.
// - guaranteed: tier that always drops if a drop fires.
// - bonus: a higher-tier upgrade roll (empty = no upgrade roll).
// - bonusPct: chance the drop is upgraded to `bonus`.
// - dropChance: probability the slot drops at all (1.0 = guaranteed slot).
func dropTierFromCR(cr float32, isBoss bool) (guaranteed LootTier, bonus LootTier, bonusPct float64, dropChance float64) {
if isBoss {
// §5: bosses minimum Rare, with a small Epic upgrade.
return LootTierRare, LootTierEpic, 0.25, 1.0
}
switch {
case cr >= 13:
return LootTierEpic, LootTierLegendary, 0.05, 1.0
case cr >= 9:
return LootTierRare, LootTierEpic, 0.10, 1.0
case cr >= 5:
return LootTierUncommon, LootTierRare, 0.15, 1.0
case cr >= 2:
return LootTierCommon, LootTierUncommon, 0.30, 1.0
default: // CR 01
return LootTierCommon, "", 0.0, 0.70
}
}
// zoneLootTables — full §5 registry: 10 zones × 5 tiers, biome-flavored.
// Common slots lean on shared exploration items (rations, scrap, salve)
// re-skinned per zone; rares pull from each zone's signature gear; the
// legendary row is single-entry per zone and reads as a trophy.
var zoneLootTables = map[ZoneID]map[LootTier][]ZoneLootDrop{
ZoneGoblinWarrens: {
LootTierCommon: {
{Name: "Goblin Field Ration", ItemType: "consumable", BaseValue: 8},
{Name: "Stolen Coin Pouch", ItemType: "currency", BaseValue: 12},
{Name: "Worg-Tooth Trinket", ItemType: "trinket", BaseValue: 10},
},
LootTierUncommon: {
{Name: "Hobgoblin Captain's Insignia", ItemType: "trophy", BaseValue: 45},
{Name: "Crude Alchemy Vial", ItemType: "consumable", BaseValue: 35},
{Name: "Goblin-Forged Shortblade", ItemType: "gear", BaseValue: 50},
},
LootTierRare: {
{Name: "War Standard Banner", ItemType: "trophy", BaseValue: 200},
{Name: "Shaman's Carved Stave", ItemType: "gear", BaseValue: 220},
},
LootTierEpic: {
{Name: "Hobgoblin General's Cuirass", ItemType: "gear", BaseValue: 800},
},
LootTierLegendary: {
{Name: "King Grobnar's Iron Crown", ItemType: "trophy", BaseValue: 4500},
},
},
ZoneCryptValdris: {
LootTierCommon: {
{Name: "Tarnished Burial Coin", ItemType: "currency", BaseValue: 10},
{Name: "Cracked Bone Charm", ItemType: "trinket", BaseValue: 8},
{Name: "Funeral-Wrap Linen", ItemType: "trinket", BaseValue: 9},
},
LootTierUncommon: {
{Name: "Pre-Empire Signet Ring", ItemType: "trinket", BaseValue: 50},
{Name: "Necrotic-Etched Dagger", ItemType: "gear", BaseValue: 55},
{Name: "Sealed Funerary Vial", ItemType: "consumable", BaseValue: 40},
},
LootTierRare: {
{Name: "Valdris Scriptorium Tablet", ItemType: "trophy", BaseValue: 240},
{Name: "Silver-Inlaid Reliquary", ItemType: "trinket", BaseValue: 220},
},
LootTierEpic: {
{Name: "Crypt-Lord's Burial Mantle", ItemType: "gear", BaseValue: 850},
},
LootTierLegendary: {
{Name: "Valdris's Sealing Sigil", ItemType: "trophy", BaseValue: 5000},
},
},
ZoneForestShadows: {
LootTierCommon: {
{Name: "Foraged Trail Loaf", ItemType: "consumable", BaseValue: 9},
{Name: "Carved Wooden Talisman", ItemType: "trinket", BaseValue: 11},
{Name: "Bundle of Shadow Reed", ItemType: "trinket", BaseValue: 8},
},
LootTierUncommon: {
{Name: "Hunter's Bone Bow Charm", ItemType: "trinket", BaseValue: 50},
{Name: "Dryad-Touched Salve", ItemType: "consumable", BaseValue: 45},
{Name: "Owlbear-Talon Knife", ItemType: "gear", BaseValue: 55},
},
LootTierRare: {
{Name: "Fey-Stained Scroll Case", ItemType: "scroll", BaseValue: 230},
{Name: "Elder Druid's Walking Stave", ItemType: "gear", BaseValue: 240},
},
LootTierEpic: {
{Name: "Greenwarden's Ironbark Vest", ItemType: "gear", BaseValue: 900},
},
LootTierLegendary: {
{Name: "Heart-Wood of the First Grove", ItemType: "trophy", BaseValue: 5500},
},
},
ZoneSunkenTemple: {
LootTierCommon: {
{Name: "Salt-Crusted Coin", ItemType: "currency", BaseValue: 9},
{Name: "Pearl Sliver", ItemType: "trinket", BaseValue: 12},
{Name: "Pressure-Sealed Vial", ItemType: "consumable", BaseValue: 11},
},
LootTierUncommon: {
{Name: "Kuo-toa Spear-Head", ItemType: "gear", BaseValue: 50},
{Name: "Coral-Set Pendant", ItemType: "trinket", BaseValue: 55},
{Name: "Tide-Blessed Censer", ItemType: "trinket", BaseValue: 45},
},
LootTierRare: {
{Name: "Drowned Acolyte's Tome", ItemType: "scroll", BaseValue: 240},
{Name: "Aboleth-Glass Lens", ItemType: "trinket", BaseValue: 250},
},
LootTierEpic: {
{Name: "Tide-Caller's Trident Head", ItemType: "gear", BaseValue: 950},
},
LootTierLegendary: {
{Name: "Dar'eth's Drowned Crown", ItemType: "trophy", BaseValue: 6000},
},
},
ZoneManorBlackspire: {
LootTierCommon: {
{Name: "Tarnished Silver Spoon", ItemType: "trinket", BaseValue: 10},
{Name: "Forty-Year-Old Tincture", ItemType: "consumable", BaseValue: 12},
{Name: "Mourner's Cameo", ItemType: "trinket", BaseValue: 11},
},
LootTierUncommon: {
{Name: "Vampire-Hunter's Stake Set", ItemType: "gear", BaseValue: 55},
{Name: "Cursed Family Locket", ItemType: "trinket", BaseValue: 50},
{Name: "Estate-Sealed Wine Bottle", ItemType: "consumable", BaseValue: 45},
},
LootTierRare: {
{Name: "Blackspire Family Diary", ItemType: "scroll", BaseValue: 240},
{Name: "Ghost-Iron Candelabrum", ItemType: "trinket", BaseValue: 230},
},
LootTierEpic: {
{Name: "Manor-Lord's Funeral Coat", ItemType: "gear", BaseValue: 950},
},
LootTierLegendary: {
{Name: "Blackspire Patriarch's Death Mask", ItemType: "trophy", BaseValue: 6000},
},
},
ZoneUnderforge: {
LootTierCommon: {
{Name: "Forge-Slag Trinket", ItemType: "trinket", BaseValue: 11},
{Name: "Iron-Filing Pouch", ItemType: "trinket", BaseValue: 9},
{Name: "Dwarven Hardtack", ItemType: "consumable", BaseValue: 10},
},
LootTierUncommon: {
{Name: "Azer-Forged Hammer Head", ItemType: "gear", BaseValue: 55},
{Name: "Heat-Etched Sigil Plate", ItemType: "trinket", BaseValue: 50},
{Name: "Salamander-Oil Flask", ItemType: "consumable", BaseValue: 50},
},
LootTierRare: {
{Name: "Master-Smith's Ledger Page", ItemType: "scroll", BaseValue: 250},
{Name: "Mithral-Thread Gauntlet", ItemType: "gear", BaseValue: 280},
},
LootTierEpic: {
{Name: "Forge-Marshal's Anvil-Charm", ItemType: "trinket", BaseValue: 1000},
},
LootTierLegendary: {
{Name: "The First Hammer's Striking Head", ItemType: "trophy", BaseValue: 7000},
},
},
ZoneUnderdark: {
LootTierCommon: {
{Name: "Cave-Spider Silk Skein", ItemType: "trinket", BaseValue: 11},
{Name: "Dim-Glow Mushroom Bundle", ItemType: "consumable", BaseValue: 10},
{Name: "Drow-Worked Coin", ItemType: "currency", BaseValue: 12},
},
LootTierUncommon: {
{Name: "Drow Hand-Crossbow Bolt Case", ItemType: "gear", BaseValue: 55},
{Name: "Faerzress-Etched Charm", ItemType: "trinket", BaseValue: 50},
{Name: "Diluted Drow Sleep Vial", ItemType: "consumable", BaseValue: 50},
},
LootTierRare: {
{Name: "Mind Flayer Codex Page", ItemType: "scroll", BaseValue: 270},
{Name: "Drow-Adamantine Bracer", ItemType: "gear", BaseValue: 290},
},
LootTierEpic: {
{Name: "Matron's House-Sigil Brooch", ItemType: "trinket", BaseValue: 1100},
},
LootTierLegendary: {
{Name: "Eyeless King's Skull-Crown", ItemType: "trophy", BaseValue: 7500},
},
},
ZoneFeywildCrossing: {
LootTierCommon: {
{Name: "Pressed Fey Petal", ItemType: "trinket", BaseValue: 12},
{Name: "Honey-Wax Candle", ItemType: "consumable", BaseValue: 10},
{Name: "Dream-Spun Thread", ItemType: "trinket", BaseValue: 11},
},
LootTierUncommon: {
{Name: "Pixie-Wing Glassine", ItemType: "trinket", BaseValue: 55},
{Name: "Wisp-Lantern Charm", ItemType: "trinket", BaseValue: 60},
{Name: "Hag-Brewed Bitter Cordial", ItemType: "consumable", BaseValue: 50},
},
LootTierRare: {
{Name: "Time-Locked Music Box", ItemType: "trinket", BaseValue: 280},
{Name: "Fey Court Invitation Scroll", ItemType: "scroll", BaseValue: 260},
},
LootTierEpic: {
{Name: "Summer-Knight's Thorn Buckler", ItemType: "gear", BaseValue: 1100},
},
LootTierLegendary: {
{Name: "Thornmother's Heart-Thorn Reliquary", ItemType: "trophy", BaseValue: 8000},
},
},
ZoneDragonsLair: {
LootTierCommon: {
{Name: "Half-Melted Gold Coin", ItemType: "currency", BaseValue: 13},
{Name: "Volcanic Glass Sliver", ItemType: "trinket", BaseValue: 11},
{Name: "Kobold Field Pouch", ItemType: "consumable", BaseValue: 10},
},
LootTierUncommon: {
{Name: "Drake-Scale Bracer", ItemType: "gear", BaseValue: 55},
{Name: "Kobold Trap-Mechanism", ItemType: "trinket", BaseValue: 50},
{Name: "Drake-Blood Cordial", ItemType: "consumable", BaseValue: 50},
},
LootTierRare: {
{Name: "Hoard-Inventory Page", ItemType: "scroll", BaseValue: 280},
{Name: "Dragonfire-Forged Spike", ItemType: "gear", BaseValue: 290},
},
LootTierEpic: {
{Name: "Wyrmguard's Cinder-Cloak Clasp", ItemType: "trinket", BaseValue: 1150},
},
LootTierLegendary: {
{Name: "Infernax's Tooth-Reliquary", ItemType: "trophy", BaseValue: 9000},
},
},
ZoneAbyssPortal: {
LootTierCommon: {
{Name: "Brimstone-Caked Coin", ItemType: "currency", BaseValue: 13},
{Name: "Sulfur-Wax Stub", ItemType: "consumable", BaseValue: 11},
{Name: "Demon-Ichor Smear-Vial", ItemType: "trinket", BaseValue: 12},
},
LootTierUncommon: {
{Name: "Quasit-Bound Ring", ItemType: "trinket", BaseValue: 55},
{Name: "Corrupted-Metal Buckle", ItemType: "gear", BaseValue: 50},
{Name: "Planar-Shard Pendant", ItemType: "trinket", BaseValue: 55},
},
LootTierRare: {
{Name: "Abyssal-Marked Codex Sheet", ItemType: "scroll", BaseValue: 290},
{Name: "Marilith-Steel Edge", ItemType: "gear", BaseValue: 300},
},
LootTierEpic: {
{Name: "Nalfeshnee's Honor-Sigil", ItemType: "trinket", BaseValue: 1200},
},
LootTierLegendary: {
{Name: "Belaxath's Bound Truename Tablet", ItemType: "trophy", BaseValue: 10000},
},
},
}
// rollZoneLoot rolls a single combat-victory drop. Returns ok=false when
// the drop chance fails (low-CR misses) or the zone has no table.
//
// Resolution order:
// 1. Pick guaranteed/bonus tier from CR via dropTierFromCR.
// 2. If dropChance fails → no drop.
// 3. Bonus upgrade roll → bonusPct chance to step up the tier.
// 4. Pick a uniform random entry from the zone's slate at that tier;
// fall back down a tier if the slate is empty.
func rollZoneLoot(zoneID ZoneID, cr float32, isBoss bool, rng *rand.Rand) (ZoneLootDrop, LootTier, bool) {
guaranteed, bonus, bonusPct, dropChance := dropTierFromCR(cr, isBoss)
if rngFloat(rng) >= dropChance {
return ZoneLootDrop{}, "", false
}
tier := guaranteed
if bonus != "" && rngFloat(rng) < bonusPct {
tier = bonus
}
zone, ok := zoneLootTables[zoneID]
if !ok {
return ZoneLootDrop{}, "", false
}
if entry, ok := pickLootEntry(zone, tier, rng); ok {
return entry, tier, true
}
// Fallback: walk down tiers if the chosen one is empty.
for _, fallback := range []LootTier{LootTierEpic, LootTierRare, LootTierUncommon, LootTierCommon} {
if fallback == tier {
continue
}
if entry, ok := pickLootEntry(zone, fallback, rng); ok {
return entry, fallback, true
}
}
return ZoneLootDrop{}, "", false
}
func pickLootEntry(zone map[LootTier][]ZoneLootDrop, tier LootTier, rng *rand.Rand) (ZoneLootDrop, bool) {
slate := zone[tier]
if len(slate) == 0 {
return ZoneLootDrop{}, false
}
idx := rngIntN(rng, len(slate))
return slate[idx], true
}
// rngFloat / rngIntN: nil-safe wrappers so tests can pass a seeded rng
// while production paths use the package-global generator.
func rngFloat(rng *rand.Rand) float64 {
if rng == nil {
return rand.Float64()
}
return rng.Float64()
}
func rngIntN(rng *rand.Rand, n int) int {
if n <= 0 {
return 0
}
if rng == nil {
return rand.IntN(n)
}
return rng.IntN(n)
}
// dropZoneLoot is the combat-resolution call site. Rolls a drop, deposits
// 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).
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 ""
}
tierVal := tier
zoneTier := zoneTierFromID(zoneID)
item := AdvItem{
Name: entry.Name,
Type: entry.ItemType,
Tier: zoneTier,
Value: int64(entry.BaseValue),
SkillSource: fmt.Sprintf("zone_loot:%s:%s", zoneID, tierVal),
}
if err := addAdvInventoryItem(userID, item); err != nil {
return fmt.Sprintf("_(Loot drop persistence error: %v.)_", err)
}
var b strings.Builder
if line := lootFlavorLine(tierVal); line != "" {
b.WriteString(line)
b.WriteString("\n")
}
icon := lootIcon(tierVal)
desc := zoneItemDescription(zoneID, entry.Name)
if desc != "" {
b.WriteString(fmt.Sprintf("%s **%s** (%s) — %d coin baseline. _%s_",
icon, entry.Name, tierVal, entry.BaseValue, desc))
} else {
b.WriteString(fmt.Sprintf("%s **%s** (%s) — %d coin baseline.",
icon, entry.Name, tierVal, entry.BaseValue))
}
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.
func lootFlavorLine(tier LootTier) string {
switch tier {
case LootTierCommon:
return flavor.Pick(flavor.LootDropCommon)
case LootTierUncommon:
return flavor.Pick(flavor.LootDropUncommon)
case LootTierRare, LootTierEpic:
return flavor.Pick(flavor.LootDropRare)
case LootTierLegendary:
return flavor.Pick(flavor.LootDropLegendary)
}
return ""
}
func lootIcon(tier LootTier) string {
switch tier {
case LootTierCommon:
return "🎁"
case LootTierUncommon:
return "🎁✨"
case LootTierRare:
return "💎"
case LootTierEpic:
return "💎✨"
case LootTierLegendary:
return "👑"
}
return "🎁"
}
// zoneItemDescription is the §5 zone-contextual item flavor table.
// Populated for Potion of Healing across all 10 zones (the design
// doc's exemplar) plus a per-item flavor matrix for the entries each
// zone's loot table actually drops (R4b polish). Falls back to "" so
// callers render the bare name when no flavor is on file.
func zoneItemDescription(zoneID ZoneID, itemName string) string {
if itemName == "Potion of Healing" {
return potionOfHealingZoneFlavor[zoneID]
}
if zoneMap, ok := zoneItemFlavor[zoneID]; ok {
if line, ok := zoneMap[itemName]; ok {
return line
}
}
return ""
}
// zoneItemFlavor — per-zone, per-item flavor for the zone loot tables
// above. Keys must match `ZoneLootDrop.Name` exactly. Items not listed
// here render with no italicized flavor line, just the name + tier +
// coin baseline. Tone target: matches the §5 Potion-of-Healing
// exemplar — short, in-character, biome-distinctive.
var zoneItemFlavor = map[ZoneID]map[string]string{
ZoneGoblinWarrens: {
"Goblin Field Ration": "Wrapped in burlap. Smells like onions, fear, and someone's last meal.",
"Stolen Coin Pouch": "Mismatched currency, three different empires represented. The goblins didn't discriminate.",
"Worg-Tooth Trinket": "Strung on sinew. Still has bone fragments embedded in the gum-line.",
"Hobgoblin Captain's Insignia": "Bronze, dented, and unmistakably stolen from somewhere it mattered.",
"Crude Alchemy Vial": "The label is in goblin shorthand. The contents fizz when you tilt it.",
"Goblin-Forged Shortblade": "Edge holds. Balance is wrong. Built by hand, not by a smith.",
"War Standard Banner": "Heraldry of three warbands stitched over each other. The bottom layer is older than the empire.",
"Shaman's Carved Stave": "Knotwood, rune-burned. Hums faintly when you stop paying attention to it.",
"Hobgoblin General's Cuirass": "Plate-and-leather, embossed with a sigil that's been scraped halfway off and re-stamped.",
"King Grobnar's Iron Crown": "Three sizes too large for any goblin who ever lived. Whatever wore this first wasn't a goblin.",
},
ZoneCryptValdris: {
"Tarnished Burial Coin": "Stamped with a face nobody alive remembers. Heavier than it should be.",
"Cracked Bone Charm": "Wrist-strung. Whoever wore it didn't make it out.",
"Funeral-Wrap Linen": "Brittle, brown, and still smelling faintly of myrrh.",
"Pre-Empire Signet Ring": "The crest pre-dates any standing kingdom. The metal is still warm somehow.",
"Necrotic-Etched Dagger": "Black-veined steel. Cold to hold, colder to put down.",
"Sealed Funerary Vial": "Wax-stoppered. Whatever's inside has had centuries to think about itself.",
"Valdris Scriptorium Tablet": "Stone, carved in a script that splits opinion among the few scholars who can read it.",
"Silver-Inlaid Reliquary": "Empty now. Someone took whatever was inside and didn't replace it.",
"Crypt-Lord's Burial Mantle": "Heavy embroidery. The threads aren't thread.",
"Valdris's Sealing Sigil": "It hums. The Crypt is older than the empire. This is older than the Crypt.",
},
ZoneForestShadows: {
"Foraged Trail Loaf": "Dense, dark, and faintly bitter. Travelers' bread, baked under a canopy that doesn't quite let light through.",
"Carved Wooden Talisman": "Birch, knife-shaped, etched with a ward against something the carver wouldn't name.",
"Bundle of Shadow Reed": "Cut along the streambank. Smells like cold water and rot.",
"Hunter's Bone Bow Charm": "Small. Femur-bone of something none of the local hunters will identify.",
"Dryad-Touched Salve": "Pine-resin base. Heals fast. Tastes like sap if you're foolish enough to try.",
"Owlbear-Talon Knife": "Talon set into a wood handle. The talon is still sharp and the wood is older than the kill.",
"Fey-Stained Scroll Case": "The leather has faint iridescence under direct light. Don't ask where it was tanned.",
"Elder Druid's Walking Stave": "Living wood, still budding. Doesn't take kindly to being indoors.",
"Greenwarden's Ironbark Vest": "Bark-plated, woven with sapling fibre. The forest grew this for someone.",
"Heart-Wood of the First Grove": "A palm-sized chunk of something that pulses if you hold it long enough. Don't.",
},
ZoneSunkenTemple: {
"Salt-Crusted Coin": "The denomination is unreadable but the metal is real silver. Bring it to Thom Krooke; he knows the era.",
"Pearl Sliver": "Fragment, not a whole pearl. Something cracked it open trying to eat what was inside.",
"Pressure-Sealed Vial": "Cold to touch. Pop the seal and it whistles air for a full second before it stops.",
"Kuo-toa Spear-Head": "Bone-and-shell composite. Wickedly barbed. Still smells like the deep.",
"Coral-Set Pendant": "The coral is bleached except where it touched the wearer's skin.",
"Tide-Blessed Censer": "Brass, pitted. Still holds a faint trace of incense and old prayer.",
"Drowned Acolyte's Tome": "Pages stuck together. The ones that separate cleanly are the ones you should read carefully.",
"Aboleth-Glass Lens": "Looks ordinary. Look through it and you see things ten feet to the left of where they actually are.",
"Tide-Caller's Trident Head": "The barbs are folded inward. Whoever forged this didn't want it pulled out clean.",
"Dar'eth's Drowned Crown": "Coral-encrusted gold. Fits no human skull comfortably.",
},
ZoneManorBlackspire: {
"Tarnished Silver Spoon": "Estate-marked. The set it belonged to is presumably still in a drawer somewhere upstairs.",
"Forty-Year-Old Tincture": "Label dated 1486. Still potent. The Blackspires kept good apothecaries.",
"Mourner's Cameo": "A widow's profile, ivory-on-jet. Likeness unknown; mood unmistakable.",
"Vampire-Hunter's Stake Set": "Three stakes, one mallet, all hawthorn. The mallet has been used.",
"Cursed Family Locket": "Don't open it. (You'll open it. They always do.)",
"Estate-Sealed Wine Bottle": "Wax intact. The label's faded but the vintage is legible — and good.",
"Blackspire Family Diary": "Penmanship is neat for the first hundred pages and then becomes other things.",
"Ghost-Iron Candelabrum": "Cold even when lit. The flames lean toward you regardless of where you stand.",
"Manor-Lord's Funeral Coat": "Tailored. Mourning-black. Smells faintly of formaldehyde and rosewater.",
"Blackspire Patriarch's Death Mask": "Plaster, eyes closed. The expression is wrong for someone who died in their sleep.",
},
ZoneUnderforge: {
"Forge-Slag Trinket": "A cooled drip of something that used to be molten. Dwarves keep these. Nobody knows why.",
"Iron-Filing Pouch": "Heavy for its size. Magnetic. Useful, somehow, to someone, eventually.",
"Dwarven Hardtack": "You could break a tooth on this. The dwarves who made it would consider that a feature.",
"Azer-Forged Hammer Head": "Still warm. Will be warm tomorrow. Will be warm a hundred years from now.",
"Heat-Etched Sigil Plate": "The runes were burned in by hand. The hand that did it was not a hand that minded heat.",
"Salamander-Oil Flask": "Sealed in glass. The oil ripples even when nothing else does.",
"Master-Smith's Ledger Page": "Old dwarven shorthand. Inventory of a forge that stopped recording entries the year the elementals woke up.",
"Mithral-Thread Gauntlet": "Light enough to be wrong. Sharper than it has any right to be along the knuckles.",
"Forge-Marshal's Anvil-Charm": "A miniature anvil on a chain. Heavy enough to be impractical. Worn anyway.",
"The First Hammer's Striking Head": "The dwarven creation-myths name this. They don't agree on which dwarf swung it.",
},
ZoneUnderdark: {
"Cave-Spider Silk Skein": "Wound on a bone-spool. Tensile strength embarrassing to surface looms.",
"Dim-Glow Mushroom Bundle": "Tied with hair. Edible. Slightly hallucinogenic. Drow eat them as a snack.",
"Drow-Worked Coin": "Etched on both sides. The faces are not faces.",
"Drow Hand-Crossbow Bolt Case": "Six bolts. Two are tipped with something dark. Two are tipped with something darker. Two are bare.",
"Faerzress-Etched Charm": "Magic-disrupting. Useful as a hex-breaker; useless inside an active wardline.",
"Diluted Drow Sleep Vial": "House-watered for trade with the surface. Still drops a person flat in twenty seconds.",
"Mind Flayer Codex Page": "The script reads itself into your head if you stare too long. Don't.",
"Drow-Adamantine Bracer": "Forged in fae-fire. Black with a violet undersheen. Doesn't tarnish.",
"Matron's House-Sigil Brooch": "Eight-legged crest. The matron who wore it is presumably no longer wearing it.",
"Eyeless King's Skull-Crown": "Bone-set, with sockets where stones should go. Worn smooth by something patient.",
},
ZoneFeywildCrossing: {
"Pressed Fey Petal": "Color shifts when you blink. Smells like a season you don't have a name for.",
"Honey-Wax Candle": "Burns blue. The wax pools upward.",
"Dream-Spun Thread": "Spider silk except it sings if you pluck it.",
"Pixie-Wing Glassine": "Pressed wing-membrane between two slips of glass. Iridescence depends on who is watching.",
"Wisp-Lantern Charm": "Glows softly when held; goes dark when set down. Doesn't ask permission to do either.",
"Hag-Brewed Bitter Cordial": "Black, syrupy, and tastes exactly like one specific regret.",
"Time-Locked Music Box": "Plays a tune that's three notes longer than the box should physically be able to hold.",
"Fey Court Invitation Scroll": "Sealed in honeycomb wax. The invitation is for a party that hasn't happened yet, or already did.",
"Summer-Knight's Thorn Buckler": "Thorn-bossed, briar-rimmed. The thorns flex when struck and then re-form.",
"Thornmother's Heart-Thorn Reliquary": "Sealed bronze, briar-wrapped. The thorn inside still pulses on a slow rhythm.",
},
ZoneDragonsLair: {
"Half-Melted Gold Coin": "Slumped flat by heat. Still gold. Still legal tender if Thom Krooke can read the year.",
"Volcanic Glass Sliver": "Sharper than steel and a quarter the weight. Edges chip on impact.",
"Kobold Field Pouch": "Trail rations, a fire-starter, two prayer-beads to a god named Kurtulmak.",
"Drake-Scale Bracer": "Overlapping scales, still attached to a strip of original drake-hide. Heat-resistant.",
"Kobold Trap-Mechanism": "Pre-built, single-use, and surprisingly clever. Read the inscribed instructions before activating.",
"Drake-Blood Cordial": "Distilled fire. Burns going down. Fire-resistance for an hour and a hangover for a day.",
"Hoard-Inventory Page": "An accountant's record of items the dragon owns. The dragon does not know it lost this page.",
"Dragonfire-Forged Spike": "Blackened steel, never cools. Driven into stone with the hilt-end first.",
"Wyrmguard's Cinder-Cloak Clasp": "Brass-and-obsidian. The clasp is dragonshape. The wearer was, briefly, a problem.",
"Infernax's Tooth-Reliquary": "A single tooth, mounted in gold-and-bone. The dragon is younger by exactly this tooth.",
},
ZoneAbyssPortal: {
"Brimstone-Caked Coin": "Sulfur-yellow at the edges. Burns the nose if you sniff it. (You won't sniff it twice.)",
"Sulfur-Wax Stub": "Half-burned demon-tallow. Doesn't smell like tallow.",
"Demon-Ichor Smear-Vial": "Stoppered tight. The contents move on their own when you turn away.",
"Quasit-Bound Ring": "Tiny, plain, and humming. Don't put it on. There's something in there.",
"Corrupted-Metal Buckle": "Was steel once. Is something else now. The shape held; the metal didn't.",
"Planar-Shard Pendant": "Looks like a fragment of a much larger reality. Held wrong, it cuts where there's nothing to cut.",
"Abyssal-Marked Codex Sheet": "Vellum, demon-scribed. The script reads in a voice that isn't yours.",
"Marilith-Steel Edge": "Six-bladed weapon-fragment, broken from a longer edge. The break is centuries old. The edge is sharp.",
"Nalfeshnee's Honor-Sigil": "A demon's idea of dignity. Brass, blood-stained, embossed with a name you can't pronounce.",
"Belaxath's Bound Truename Tablet": "Stone, demon-runed. The truename is bound by inscription. Read it and the binding holds. Speak it and it doesn't.",
},
}
// potionOfHealingZoneFlavor — §5 Zone Loot Flavor by Biome, verbatim.
var potionOfHealingZoneFlavor = map[ZoneID]string{
ZoneGoblinWarrens: "A stolen vial, still stoppered with a rag. It smells like goblin and better days.",
ZoneCryptValdris: "A glass vial embedded in a burial offering. Whoever left it didn't need it.",
ZoneForestShadows: "Bark-sealed, filled with something deep red. It tastes like the forest before it went wrong.",
ZoneSunkenTemple: "Salt-rimed and pressure-sealed. A temple offering, preserved by the deep cold.",
ZoneManorBlackspire: "Found in the medicine cabinet, dated forty years ago. Remarkably, still effective.",
ZoneUnderforge: "Hot to the touch even now. The dwarves made things to last.",
ZoneUnderdark: "Drow-made. The formula is different. The effect is the same. The ingredients are not discussed.",
ZoneFeywildCrossing: "It shifts color when you look at it sideways. The fey make medicine the way they make everything: beautifully and with caveats.",
ZoneDragonsLair: "A kobold medic's field kit, looted from a belt pouch. The kobolds take care of their own.",
ZoneAbyssPortal: "Glows faintly red. Still works. TwinBee recommends not asking what's in it.",
}