N1/A1-A3: rewire the drop systems Phase R orphaned

Treasure, masterwork, and consumable drops each had zero call sites: they
only ever fired from the legacy daily activity loop, which adventure.go now
intercepts with a deprecation DM. Hook all three to the zone-combat seam.

A1 - treasure: rollAdvTreasureDropDetailed takes a weight, applied to the
base rate. Boss x4, elite x2, standard x1, plus one x1 roll on zone clear.
Near-miss DMs now fire only for weighted moments; at x1 on autopilot they'd
land on ~3% of every kill.

A2 - masterwork: the catalog is keyed to mining/fishing/foraging, so the
dungeon lookup returned nil and the hook would have been a silent no-op.
masterworkDefForZone rolls across all three slot lines at the zone's tier.
Flavor now follows loc.Activity rather than the item's catalog line, with a
new dungeon pool - a crypt boss must not narrate a pickaxe striking ore.

A3 - consumables + ingredients: the audit found more than the four named
ingredients were stranded. generateAdvLoot is reachable only from
resolveDungeonAction, which has no callers, so all four legacy loot tables
were dead and every one of the 12 recipes was uncraftable. rollZoneIngredient
draws from those tables directly at the zone's tier, reviving them wholesale
rather than re-keying 24 ingredients into the per-zone slates.
This commit is contained in:
prosolis
2026-07-09 18:29:31 -07:00
parent a4f162d2ee
commit e8f4863ae0
11 changed files with 545 additions and 107 deletions

View File

@@ -0,0 +1,68 @@
package plugin
import (
"strings"
"testing"
)
// N1/A2 — the masterwork catalog is keyed to gathering activities, so the
// zone-combat seam had nothing to look up. masterworkDefForZone bridges it.
func TestMasterworkDefForZone_CoversEveryTier(t *testing.T) {
for tier := 1; tier <= 5; tier++ {
if def := masterworkDefForZone(tier); def == nil {
t.Errorf("tier %d has no masterwork def", tier)
} else if def.Tier != tier {
t.Errorf("tier %d returned a tier-%d def", tier, def.Tier)
}
}
}
// A dungeon drop must be able to land in any of the three slots — otherwise
// zone play would only ever upgrade one piece of gear.
func TestMasterworkDefForZone_RollsAllSlots(t *testing.T) {
seen := map[EquipmentSlot]bool{}
for i := 0; i < 300; i++ {
if def := masterworkDefForZone(3); def != nil {
seen[def.Slot] = true
}
}
for _, slot := range []EquipmentSlot{SlotWeapon, SlotArmor, SlotBoots} {
if !seen[slot] {
t.Errorf("slot %v never rolled in 300 draws", slot)
}
}
}
// The pre-existing lookup must keep working for the gathering activities.
func TestMasterworkDefFor_GatheringUnchanged(t *testing.T) {
for _, act := range masterworkSlotActivities {
for tier := 1; tier <= 5; tier++ {
def := masterworkDefFor(act, tier)
if def == nil {
t.Fatalf("%s tier %d lost its def", act, tier)
}
if def.Activity != act {
t.Errorf("%s tier %d returned activity %s", act, tier, def.Activity)
}
}
}
}
// Flavor follows the place, not the catalog line. A crypt boss must not
// narrate a pickaxe striking ore.
func TestMasterworkDropFlavorText_DungeonHasItsOwnVoice(t *testing.T) {
gatheringWords := []string{"pickaxe", "fishing", "foraging", "vein", "ore", "branch"}
for tier := 1; tier <= 5; tier++ {
text := masterworkDropFlavorText(AdvActivityDungeon, tier)
if text == "" {
t.Fatalf("dungeon tier %d has no flavor", tier)
}
lower := strings.ToLower(text)
for _, w := range gatheringWords {
if strings.Contains(lower, w) {
t.Errorf("dungeon tier %d flavor leaks gathering word %q", tier, w)
}
}
}
}