H4: bump Uncommon/Rare harvest MaxCharges 1→2

53 resources affected in dnd_resource_registry.go; Common (mostly already
2), VeryRare, and Legendary untouched. Calibration sim (15k rows, 10
classes × L3/7/12 × 5 zones) lands cleared-run yield deltas at −2.5% to
+4.7% vs the J2b baseline — well within the ±10% DoD band, but with much
less leverage than the plan anticipated.

The finding (sim_results/h4_findings.md): per-room yield is gated by
non-Noise interrupt truncation, not charge count. The Common consolation
bracket (delta ≥ -4 yields +1) already absorbs most of the Josie nerf.
Future yield tuning should reach for resolveCombatInterrupt, not charges
— parked as out-of-scope for this branch.

H4 closes; the phase-H branch can now soak H1/H2/H3/H5 + this bump.
This commit is contained in:
prosolis
2026-05-17 16:49:18 -07:00
parent e05da913c4
commit d225fc8185
5 changed files with 15266 additions and 53 deletions

View File

@@ -106,6 +106,8 @@ The user-facing pitch: stop typing `!mine`/`!scavenge`/etc. The walk handles it.
**⚠ Blocked on Phase J (below).** First hardened-sim sweep showed yield is gated by survival, not charge counts: Fighter/Mage TPK at T3+, no class extracts T5 boss. Tuning charges before fixing survival would over-tune against a strawman. Run J0J3 first, then this phase.
**Shipped 2026-05-17 (post-J).** Uncommon/Rare MaxCharges bumped 1 → 2 (53 resources). Yield deltas vs J2b baseline: 2.5% to +4.7% across the four cleared zones — within band but with much less leverage than expected. Full write-up in `sim_results/h4_findings.md`: the binding constraint on per-room yield is interrupt-truncation, not charge count. The Common consolation bracket (delta ≥ -4 yields +1) already absorbs most of the Josie nerf. Future yield-tuning effort should target `resolveCombatInterrupt` rates, not charges. H4 closes.
### H5 — Partial spell-slot refresh on short rest
`internal/plugin/dnd_rest.go` + `internal/plugin/dnd_spells.go`

View File

@@ -46,108 +46,108 @@ type ZoneResource struct {
var zoneResources = map[ZoneID][]ZoneResource{
ZoneGoblinWarrens: {
{ID: "scrap_iron", Name: "Scrap Iron", Action: HarvestMine, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 10, MaxCharges: 2},
{ID: "alchemy_pouch", Name: "Goblin Alchemy Pouch", Action: HarvestScavenge, DC: 13, Rarity: RarityUncommon, Type: "material", SellValue: 40, MaxCharges: 1},
{ID: "worg_pelt", Name: "Worg Pelt", Action: HarvestScavenge, DC: 11, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 1, RequiresKill: "worg"},
{ID: "crude_weapon_cache", Name: "Crude Weapon Cache", Action: HarvestScavenge, DC: 15, Rarity: RarityUncommon, Type: "item", SellValue: 50, MaxCharges: 1},
{ID: "war_standard", Name: "Hobgoblin War Standard", Action: HarvestScavenge, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 1},
{ID: "shaman_reagents", Name: "Goblin Shaman's Reagents", Action: HarvestEssence, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 180, MaxCharges: 1, ClassRestrict: ClassMage},
{ID: "alchemy_pouch", Name: "Goblin Alchemy Pouch", Action: HarvestScavenge, DC: 13, Rarity: RarityUncommon, Type: "material", SellValue: 40, MaxCharges: 2},
{ID: "worg_pelt", Name: "Worg Pelt", Action: HarvestScavenge, DC: 11, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 2, RequiresKill: "worg"},
{ID: "crude_weapon_cache", Name: "Crude Weapon Cache", Action: HarvestScavenge, DC: 15, Rarity: RarityUncommon, Type: "item", SellValue: 50, MaxCharges: 2},
{ID: "war_standard", Name: "Hobgoblin War Standard", Action: HarvestScavenge, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 2},
{ID: "shaman_reagents", Name: "Goblin Shaman's Reagents", Action: HarvestEssence, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 180, MaxCharges: 2, ClassRestrict: ClassMage},
},
ZoneCryptValdris: {
{ID: "grave_soil", Name: "Grave Soil", Action: HarvestScavenge, DC: 9, Rarity: RarityCommon, Type: "material", SellValue: 8, MaxCharges: 2},
{ID: "bone_dust", Name: "Bone Dust", Action: HarvestScavenge, DC: 9, Rarity: RarityCommon, Type: "material", SellValue: 7, MaxCharges: 2},
{ID: "ancient_coin", Name: "Ancient Coin (pre-Empire)", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1},
{ID: "necrotic_essence", Name: "Necrotic Essence", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 1},
{ID: "silver_grave_dust", Name: "Silver Grave Dust", Action: HarvestCommune, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 1, ClassRestrict: ClassCleric},
{ID: "valdris_seal_fragment", Name: "Valdris's Seal Fragment", Action: HarvestScavenge, DC: 20, Rarity: RarityRare, Type: "quest", SellValue: 250, MaxCharges: 1},
{ID: "ancient_coin", Name: "Ancient Coin (pre-Empire)", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2},
{ID: "necrotic_essence", Name: "Necrotic Essence", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 2},
{ID: "silver_grave_dust", Name: "Silver Grave Dust", Action: HarvestCommune, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 2, ClassRestrict: ClassCleric},
{ID: "valdris_seal_fragment", Name: "Valdris's Seal Fragment", Action: HarvestScavenge, DC: 20, Rarity: RarityRare, Type: "quest", SellValue: 250, MaxCharges: 2},
},
ZoneForestShadows: {
{ID: "shadow_herb", Name: "Shadow Herb", Action: HarvestForage, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 10, MaxCharges: 2},
{ID: "beast_pelt", Name: "Beast Pelt", Action: HarvestForage, DC: 11, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 1, RequiresKill: "beast"},
{ID: "darkwood", Name: "Darkwood", Action: HarvestMine, DC: 13, Rarity: RarityUncommon, Type: "material", SellValue: 40, MaxCharges: 1},
{ID: "fey_bloom", Name: "Corrupted Fey Bloom", Action: HarvestForage, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1},
{ID: "owlbear_feather", Name: "Owlbear Feather", Action: HarvestScavenge, DC: 12, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 1, RequiresKill: "owlbear"},
{ID: "dryads_tears", Name: "Dryad's Tears", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 1},
{ID: "biolum_fungi", Name: "Bioluminescent Fungi", Action: HarvestForage, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 1},
{ID: "darkwood", Name: "Darkwood", Action: HarvestMine, DC: 13, Rarity: RarityUncommon, Type: "material", SellValue: 40, MaxCharges: 2},
{ID: "fey_bloom", Name: "Corrupted Fey Bloom", Action: HarvestForage, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2},
{ID: "owlbear_feather", Name: "Owlbear Feather", Action: HarvestScavenge, DC: 12, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 2, RequiresKill: "owlbear"},
{ID: "dryads_tears", Name: "Dryad's Tears", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 2},
{ID: "biolum_fungi", Name: "Bioluminescent Fungi", Action: HarvestForage, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 2},
// §6.1 fish — Forest streams (outdoor exploration rooms).
{ID: "shadow_trout", Name: "Shadow Trout", Action: HarvestFish, DC: 11, Rarity: RarityCommon, Type: "fish", SellValue: 10, MaxCharges: 2},
{ID: "gloomfin", Name: "Gloomfin", Action: HarvestFish, DC: 15, Rarity: RarityUncommon, Type: "fish", SellValue: 45, MaxCharges: 1},
{ID: "phantom_carp", Name: "Phantom Carp", Action: HarvestFish, DC: 20, Rarity: RarityRare, Type: "fish", SellValue: 220, MaxCharges: 1},
{ID: "gloomfin", Name: "Gloomfin", Action: HarvestFish, DC: 15, Rarity: RarityUncommon, Type: "fish", SellValue: 45, MaxCharges: 2},
{ID: "phantom_carp", Name: "Phantom Carp", Action: HarvestFish, DC: 20, Rarity: RarityRare, Type: "fish", SellValue: 220, MaxCharges: 2},
},
ZoneSunkenTemple: {
{ID: "sea_glass", Name: "Sea Glass Shard", Action: HarvestScavenge, DC: 9, Rarity: RarityCommon, Type: "material", SellValue: 7, MaxCharges: 2},
{ID: "deep_coral", Name: "Deep Coral", Action: HarvestMine, DC: 11, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 1},
{ID: "kuotoa_scale", Name: "Kuo-toa Scale", Action: HarvestScavenge, DC: 12, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 1, RequiresKill: "kuo-toa"},
{ID: "black_pearl", Name: "Black Pearl", Action: HarvestFish, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1},
{ID: "aboleth_mucus", Name: "Aboleth Mucus (refined)", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 230, MaxCharges: 1, ClassRestrict: ClassMage},
{ID: "temple_relic", Name: "Ancient Temple Relic", Action: HarvestScavenge, DC: 20, Rarity: RarityRare, Type: "item", SellValue: 250, MaxCharges: 1},
{ID: "tidal_crystal", Name: "Tidal Crystal", Action: HarvestMine, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 1, ConditionalEvent: "tidal_event"},
{ID: "kuotoa_scale", Name: "Kuo-toa Scale", Action: HarvestScavenge, DC: 12, Rarity: RarityUncommon, Type: "material", SellValue: 35, MaxCharges: 2, RequiresKill: "kuo-toa"},
{ID: "black_pearl", Name: "Black Pearl", Action: HarvestFish, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2},
{ID: "aboleth_mucus", Name: "Aboleth Mucus (refined)", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 230, MaxCharges: 2, ClassRestrict: ClassMage},
{ID: "temple_relic", Name: "Ancient Temple Relic", Action: HarvestScavenge, DC: 20, Rarity: RarityRare, Type: "item", SellValue: 250, MaxCharges: 2},
{ID: "tidal_crystal", Name: "Tidal Crystal", Action: HarvestMine, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 2, ConditionalEvent: "tidal_event"},
// §6.1 fish — flooded rooms; bonus during Tidal Event handled in R6.
{ID: "blind_cave_fish", Name: "Blind Cave Fish", Action: HarvestFish, DC: 10, Rarity: RarityCommon, Type: "fish", SellValue: 8, MaxCharges: 2},
{ID: "merrow_eel", Name: "Merrow Eel", Action: HarvestFish, DC: 14, Rarity: RarityUncommon, Type: "fish", SellValue: 45, MaxCharges: 1},
{ID: "dareth_lanternfish", Name: "Dar'eth Lanternfish", Action: HarvestFish, DC: 19, Rarity: RarityRare, Type: "fish", SellValue: 230, MaxCharges: 1},
{ID: "merrow_eel", Name: "Merrow Eel", Action: HarvestFish, DC: 14, Rarity: RarityUncommon, Type: "fish", SellValue: 45, MaxCharges: 2},
{ID: "dareth_lanternfish", Name: "Dar'eth Lanternfish", Action: HarvestFish, DC: 19, Rarity: RarityRare, Type: "fish", SellValue: 230, MaxCharges: 2},
{ID: "aboleth_spawn", Name: "Aboleth Spawn (juvenile)", Action: HarvestFish, DC: 23, Rarity: RarityVeryRare, Type: "fish", SellValue: 800, MaxCharges: 1, ClassRestrict: ClassRanger},
},
ZoneManorBlackspire: {
{ID: "spectral_grave_dust", Name: "Grave Dust (spectral)", Action: HarvestScavenge, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 2},
{ID: "cursed_trinket", Name: "Cursed Trinket", Action: HarvestScavenge, DC: 12, Rarity: RarityCommon, Type: "item", SellValue: 14, MaxCharges: 1},
{ID: "ghost_silk", Name: "Ghost Silk", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 1},
{ID: "vampire_ash", Name: "Vampire Ash", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1, RequiresKill: "vampire_spawn"},
{ID: "shadow_essence", Name: "Shadow Essence", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 1},
{ID: "blackspire_volume", Name: "Blackspire Library Volume", Action: HarvestScavenge, DC: 19, Rarity: RarityRare, Type: "item", SellValue: 240, MaxCharges: 1},
{ID: "wraith_core", Name: "Wraith Core", Action: HarvestCommune, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 260, MaxCharges: 1, ClassRestrict: ClassCleric, RequiresKill: "wraith"},
{ID: "ghost_silk", Name: "Ghost Silk", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 2},
{ID: "vampire_ash", Name: "Vampire Ash", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2, RequiresKill: "vampire_spawn"},
{ID: "shadow_essence", Name: "Shadow Essence", Action: HarvestEssence, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 2},
{ID: "blackspire_volume", Name: "Blackspire Library Volume", Action: HarvestScavenge, DC: 19, Rarity: RarityRare, Type: "item", SellValue: 240, MaxCharges: 2},
{ID: "wraith_core", Name: "Wraith Core", Action: HarvestCommune, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 260, MaxCharges: 2, ClassRestrict: ClassCleric, RequiresKill: "wraith"},
},
ZoneUnderforge: {
{ID: "iron_ore", Name: "Iron Ore", Action: HarvestMine, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 10, MaxCharges: 2},
{ID: "volcanic_glass", Name: "Volcanic Glass", Action: HarvestMine, DC: 11, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 2},
{ID: "azer_ingot", Name: "Azer Metal Ingot", Action: HarvestMine, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1, RequiresKill: "azer"},
{ID: "fire_essence", Name: "Fire Essence", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 1},
{ID: "mithral_trace", Name: "Mithral Trace", Action: HarvestMine, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 280, MaxCharges: 1},
{ID: "forge_core_fragment", Name: "Forge Core Fragment", Action: HarvestScavenge, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 1, RequiresKill: "construct"},
{ID: "salamander_oil", Name: "Salamander Oil", Action: HarvestEssence, DC: 16, Rarity: RarityRare, Type: "material", SellValue: 210, MaxCharges: 1, RequiresKill: "salamander"},
{ID: "azer_ingot", Name: "Azer Metal Ingot", Action: HarvestMine, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2, RequiresKill: "azer"},
{ID: "fire_essence", Name: "Fire Essence", Action: HarvestEssence, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 45, MaxCharges: 2},
{ID: "mithral_trace", Name: "Mithral Trace", Action: HarvestMine, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 280, MaxCharges: 2},
{ID: "forge_core_fragment", Name: "Forge Core Fragment", Action: HarvestScavenge, DC: 17, Rarity: RarityRare, Type: "material", SellValue: 200, MaxCharges: 2, RequiresKill: "construct"},
{ID: "salamander_oil", Name: "Salamander Oil", Action: HarvestEssence, DC: 16, Rarity: RarityRare, Type: "material", SellValue: 210, MaxCharges: 2, RequiresKill: "salamander"},
},
ZoneUnderdark: {
{ID: "ud_mushroom", Name: "Underdark Mushroom", Action: HarvestForage, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 10, MaxCharges: 2},
{ID: "spider_silk", Name: "Cave Spider Silk", Action: HarvestForage, DC: 12, Rarity: RarityCommon, Type: "material", SellValue: 14, MaxCharges: 2},
{ID: "drow_poison", Name: "Drow Poison (diluted)", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1, RequiresKill: "drow"},
{ID: "faerzress_crystal", Name: "Faerzress Crystal", Action: HarvestMine, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1},
{ID: "mind_flayer_brain", Name: "Mind Flayer Brain Matter", Action: HarvestEssence, DC: 19, Rarity: RarityRare, Type: "material", SellValue: 250, MaxCharges: 1, ClassRestrict: ClassMage, RequiresKill: "illithid"},
{ID: "drow_adamantine", Name: "Drow Adamantine", Action: HarvestMine, DC: 21, Rarity: RarityRare, Type: "material", SellValue: 290, MaxCharges: 1},
{ID: "drow_poison", Name: "Drow Poison (diluted)", Action: HarvestScavenge, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2, RequiresKill: "drow"},
{ID: "faerzress_crystal", Name: "Faerzress Crystal", Action: HarvestMine, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2},
{ID: "mind_flayer_brain", Name: "Mind Flayer Brain Matter", Action: HarvestEssence, DC: 19, Rarity: RarityRare, Type: "material", SellValue: 250, MaxCharges: 2, ClassRestrict: ClassMage, RequiresKill: "illithid"},
{ID: "drow_adamantine", Name: "Drow Adamantine", Action: HarvestMine, DC: 21, Rarity: RarityRare, Type: "material", SellValue: 290, MaxCharges: 2},
{ID: "deep_dragon_chip", Name: "Deep Dragon Scale Chip", Action: HarvestScavenge, DC: 23, Rarity: RarityVeryRare, Type: "material", SellValue: 800, MaxCharges: 1},
// §6.1 fish — Underdark river rooms (TwinBee marks them; for now any room in zone).
{ID: "blind_albino_bass", Name: "Blind Albino Bass", Action: HarvestFish, DC: 11, Rarity: RarityCommon, Type: "fish", SellValue: 10, MaxCharges: 2},
{ID: "glowing_cave_eel", Name: "Glowing Cave Eel", Action: HarvestFish, DC: 15, Rarity: RarityUncommon, Type: "fish", SellValue: 50, MaxCharges: 1},
{ID: "underdark_shark", Name: "Underdark Shark", Action: HarvestFish, DC: 20, Rarity: RarityRare, Type: "fish", SellValue: 240, MaxCharges: 1},
{ID: "glowing_cave_eel", Name: "Glowing Cave Eel", Action: HarvestFish, DC: 15, Rarity: RarityUncommon, Type: "fish", SellValue: 50, MaxCharges: 2},
{ID: "underdark_shark", Name: "Underdark Shark", Action: HarvestFish, DC: 20, Rarity: RarityRare, Type: "fish", SellValue: 240, MaxCharges: 2},
{ID: "eyeless_king", Name: "The Eyeless King", Action: HarvestFish, DC: 26, Rarity: RarityLegendary, Type: "fish", SellValue: 5000, MaxCharges: 1, ClassRestrict: ClassRanger},
},
ZoneFeywildCrossing: {
{ID: "fey_dust", Name: "Fey Dust", Action: HarvestForage, DC: 11, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 2},
{ID: "enchanted_petal", Name: "Enchanted Petal", Action: HarvestForage, DC: 12, Rarity: RarityCommon, Type: "material", SellValue: 13, MaxCharges: 2},
{ID: "pixie_wing", Name: "Pixie Wing Fragments", Action: HarvestEssence, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1},
{ID: "wisp_essence", Name: "Will-o-Wisp Essence", Action: HarvestEssence, DC: 17, Rarity: RarityUncommon, Type: "material", SellValue: 60, MaxCharges: 1, RequiresKill: "will_o_wisp"},
{ID: "hag_hair", Name: "Hag Hair", Action: HarvestCommune, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 1, ClassRestrict: ClassCleric, RequiresKill: "hag"},
{ID: "timelock_amber", Name: "Timelock Amber", Action: HarvestForage, DC: 21, Rarity: RarityRare, Type: "material", SellValue: 290, MaxCharges: 1},
{ID: "pixie_wing", Name: "Pixie Wing Fragments", Action: HarvestEssence, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2},
{ID: "wisp_essence", Name: "Will-o-Wisp Essence", Action: HarvestEssence, DC: 17, Rarity: RarityUncommon, Type: "material", SellValue: 60, MaxCharges: 2, RequiresKill: "will_o_wisp"},
{ID: "hag_hair", Name: "Hag Hair", Action: HarvestCommune, DC: 18, Rarity: RarityRare, Type: "material", SellValue: 220, MaxCharges: 2, ClassRestrict: ClassCleric, RequiresKill: "hag"},
{ID: "timelock_amber", Name: "Timelock Amber", Action: HarvestForage, DC: 21, Rarity: RarityRare, Type: "material", SellValue: 290, MaxCharges: 2},
{ID: "thornmother_thorn", Name: "Thornmother's Thorn", Action: HarvestScavenge, DC: 25, Rarity: RarityVeryRare, Type: "material", SellValue: 1000, MaxCharges: 1, RequiresKill: "thornmother"},
// §6.1 fish — fey streams; time-distortion event hook fires in handleHarvestCmd.
{ID: "moonlit_minnow", Name: "Moonlit Minnow", Action: HarvestFish, DC: 12, Rarity: RarityCommon, Type: "fish", SellValue: 12, MaxCharges: 2},
{ID: "dreaming_pike", Name: "Dreaming Pike", Action: HarvestFish, DC: 16, Rarity: RarityUncommon, Type: "fish", SellValue: 55, MaxCharges: 1},
{ID: "timeworn_koi", Name: "Timeworn Koi", Action: HarvestFish, DC: 21, Rarity: RarityRare, Type: "fish", SellValue: 280, MaxCharges: 1},
{ID: "dreaming_pike", Name: "Dreaming Pike", Action: HarvestFish, DC: 16, Rarity: RarityUncommon, Type: "fish", SellValue: 55, MaxCharges: 2},
{ID: "timeworn_koi", Name: "Timeworn Koi", Action: HarvestFish, DC: 21, Rarity: RarityRare, Type: "fish", SellValue: 280, MaxCharges: 2},
},
ZoneDragonsLair: {
{ID: "ancient_gold", Name: "Ancient Gold Coin", Action: HarvestScavenge, DC: 10, Rarity: RarityCommon, Type: "material", SellValue: 12, MaxCharges: 2},
{ID: "volcanic_gem", Name: "Volcanic Gem", Action: HarvestMine, DC: 12, Rarity: RarityCommon, Type: "material", SellValue: 13, MaxCharges: 2},
{ID: "drake_blood", Name: "Drake Blood", Action: HarvestEssence, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1, RequiresKill: "drake"},
{ID: "dragon_scale_fragment", Name: "Dragon Scale Fragment", Action: HarvestScavenge, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1, RequiresKill: "drake"},
{ID: "dragonfire_coal", Name: "Dragonfire Coal", Action: HarvestMine, DC: 19, Rarity: RarityRare, Type: "material", SellValue: 240, MaxCharges: 1},
{ID: "kobold_trap", Name: "Kobold Trap Mechanism", Action: HarvestScavenge, DC: 18, Rarity: RarityRare, Type: "item", SellValue: 220, MaxCharges: 1},
{ID: "drake_blood", Name: "Drake Blood", Action: HarvestEssence, DC: 14, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2, RequiresKill: "drake"},
{ID: "dragon_scale_fragment", Name: "Dragon Scale Fragment", Action: HarvestScavenge, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2, RequiresKill: "drake"},
{ID: "dragonfire_coal", Name: "Dragonfire Coal", Action: HarvestMine, DC: 19, Rarity: RarityRare, Type: "material", SellValue: 240, MaxCharges: 2},
{ID: "kobold_trap", Name: "Kobold Trap Mechanism", Action: HarvestScavenge, DC: 18, Rarity: RarityRare, Type: "item", SellValue: 220, MaxCharges: 2},
{ID: "infernax_scale", Name: "Infernax Scale", Action: HarvestScavenge, DC: 25, Rarity: RarityLegendary, Type: "material", SellValue: 6000, MaxCharges: 1, RequiresKill: "infernax"},
},
ZoneAbyssPortal: {
{ID: "brimstone_shard", Name: "Brimstone Shard", Action: HarvestMine, DC: 11, Rarity: RarityCommon, Type: "material", SellValue: 13, MaxCharges: 2},
{ID: "demon_ichor", Name: "Demon Ichor", Action: HarvestScavenge, DC: 12, Rarity: RarityCommon, Type: "material", SellValue: 14, MaxCharges: 2, RequiresKill: "demon"},
{ID: "planar_shard", Name: "Planar Shard", Action: HarvestMine, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 1},
{ID: "corrupted_metal", Name: "Corrupted Metal", Action: HarvestScavenge, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 1},
{ID: "void_essence", Name: "Void Essence", Action: HarvestEssence, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 280, MaxCharges: 1},
{ID: "abyssal_ichor", Name: "Abyssal Ichor (concentrated)", Action: HarvestEssence, DC: 22, Rarity: RarityRare, Type: "material", SellValue: 300, MaxCharges: 1, ClassRestrict: ClassMage, RequiresKill: "balor"},
{ID: "planar_shard", Name: "Planar Shard", Action: HarvestMine, DC: 15, Rarity: RarityUncommon, Type: "material", SellValue: 50, MaxCharges: 2},
{ID: "corrupted_metal", Name: "Corrupted Metal", Action: HarvestScavenge, DC: 16, Rarity: RarityUncommon, Type: "material", SellValue: 55, MaxCharges: 2},
{ID: "void_essence", Name: "Void Essence", Action: HarvestEssence, DC: 20, Rarity: RarityRare, Type: "material", SellValue: 280, MaxCharges: 2},
{ID: "abyssal_ichor", Name: "Abyssal Ichor (concentrated)", Action: HarvestEssence, DC: 22, Rarity: RarityRare, Type: "material", SellValue: 300, MaxCharges: 2, ClassRestrict: ClassMage, RequiresKill: "balor"},
{ID: "portal_fragment", Name: "Portal Fragment", Action: HarvestScavenge, DC: 25, Rarity: RarityLegendary, Type: "material", SellValue: 6500, MaxCharges: 1, RequiresKill: "portal_boss"},
},
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
class level zone n p50_yld p50_yld_clr mean_yld mean_rms %clr %boss %tpk %fled %ext
bard 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
bard 7 dragons_lair 100 2 0 3.3 5.9 0 28 100 0 0
bard 12 dragons_lair 100 4 0 4.8 7.2 0 63 100 0 0
cleric 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
cleric 7 dragons_lair 100 3 0 3.3 5.3 0 11 100 0 0
cleric 12 dragons_lair 100 3 0 3.2 7 0 49 100 0 0
druid 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
druid 7 dragons_lair 100 3 0 3.3 5.6 0 24 100 0 0
druid 12 dragons_lair 100 4 0 4.2 7.5 0 78 99 0 1
fighter 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
fighter 7 dragons_lair 100 3 0 4.2 7 0 62 97 0 3
fighter 12 dragons_lair 100 4 0 4.4 8 0 99 100 0 0
mage 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
mage 7 dragons_lair 100 4 0 4.8 5.4 0 16 99 0 1
mage 12 dragons_lair 100 6 0 5.9 7.4 0 74 100 0 0
paladin 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
paladin 7 dragons_lair 100 3 0 3.9 7.1 0 68 99 0 1
paladin 12 dragons_lair 100 3 0 3.3 7.9 0 97 100 0 0
ranger 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
ranger 7 dragons_lair 100 7 0 8.2 7.8 0 91 97 0 3
ranger 12 dragons_lair 100 7 0 7.5 8 0 100 100 0 0
rogue 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
rogue 7 dragons_lair 100 4 0 5.2 6.3 0 54 96 0 4
rogue 12 dragons_lair 100 4 0 5.6 7.9 0 94 100 0 0
sorcerer 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
sorcerer 7 dragons_lair 100 4 0 4.6 5.3 0 13 100 0 0
sorcerer 12 dragons_lair 100 4 0 4.8 7.4 0 71 100 0 0
warlock 3 dragons_lair 100 0 0 0 0 0 0 0 0 0
warlock 7 dragons_lair 100 3 0 3.6 6.1 0 28 98 0 2
warlock 12 dragons_lair 100 3 0 3.9 7.5 0 76 100 0 0
bard 3 forest_shadows 100 4 0 4.5 4.1 0 1 100 0 0
bard 7 forest_shadows 100 6 4 6.5 6 1 97 99 0 0
bard 12 forest_shadows 100 6 6 6 7 99 99 1 0 0
cleric 3 forest_shadows 100 4 0 5.1 4.6 0 0 100 0 0
cleric 7 forest_shadows 100 7 8 8.2 5.7 2 70 98 0 0
cleric 12 forest_shadows 100 7 8 8 6.8 90 90 10 0 0
druid 3 forest_shadows 100 5 0 6.4 5.1 0 16 100 0 0
druid 7 forest_shadows 100 8 7 9 6.9 86 100 14 0 0
druid 12 forest_shadows 100 7 7 7.5 7 100 100 0 0 0
fighter 3 forest_shadows 100 5 0 6 5.3 0 22 100 0 0
fighter 7 forest_shadows 100 5 5 6.4 7 100 100 0 0 0
fighter 12 forest_shadows 100 6 6 7 7 100 100 0 0 0
mage 3 forest_shadows 100 4 0 4.7 3.7 0 0 100 0 0
mage 7 forest_shadows 100 5 6 6.5 6.7 82 90 18 0 0
mage 12 forest_shadows 100 6 6 5.6 7 100 100 0 0 0
paladin 3 forest_shadows 100 6 0 6.5 5.8 0 84 100 0 0
paladin 7 forest_shadows 100 7 7 6.9 7 100 100 0 0 0
paladin 12 forest_shadows 100 6 6 7.1 7 100 100 0 0 0
ranger 3 forest_shadows 100 11 0 12.3 5.2 0 17 100 0 0
ranger 7 forest_shadows 100 15 15 15.7 7 100 100 0 0 0
ranger 12 forest_shadows 100 13 13 14.9 7 100 100 0 0 0
rogue 3 forest_shadows 100 5 0 6.3 5.7 0 63 100 0 0
rogue 7 forest_shadows 100 7 7 8.2 6.9 86 100 14 0 0
rogue 12 forest_shadows 100 6 6 6.5 7 100 100 0 0 0
sorcerer 3 forest_shadows 100 4 0 4.7 3.3 0 0 100 0 0
sorcerer 7 forest_shadows 100 6 6 6.6 6.6 67 88 33 0 0
sorcerer 12 forest_shadows 100 6 6 7.2 7 99 99 1 0 0
warlock 3 forest_shadows 100 4 0 4.2 4.1 0 0 100 0 0
warlock 7 forest_shadows 100 6 7 6.5 6.3 41 93 59 0 0
warlock 12 forest_shadows 100 5 5 5.9 7 100 100 0 0 0
bard 3 goblin_warrens 100 5 13 5.6 4.9 1 85 99 0 0
bard 7 goblin_warrens 100 6 6 6.2 6 100 100 0 0 0
bard 12 goblin_warrens 100 4 4 4.9 6 100 100 0 0 0
cleric 3 goblin_warrens 100 3 9 4.7 4.8 9 74 91 0 0
cleric 7 goblin_warrens 100 5 5 5.3 5.9 95 95 5 0 0
cleric 12 goblin_warrens 100 5 5 5.3 6 99 99 1 0 0
druid 3 goblin_warrens 100 3 4 4.1 5.6 70 94 30 0 0
druid 7 goblin_warrens 100 4 4 4.7 6 100 100 0 0 0
druid 12 goblin_warrens 100 4 4 5.2 6 100 100 0 0 0
fighter 3 goblin_warrens 100 5 5 5.8 5.6 64 96 36 0 0
fighter 7 goblin_warrens 100 4 4 5 6 100 100 0 0 0
fighter 12 goblin_warrens 100 4 4 4.9 6 100 100 0 0 0
mage 3 goblin_warrens 100 7 7 7.3 5.5 70 81 30 0 0
mage 7 goblin_warrens 100 7 7 8.7 6 99 99 1 0 0
mage 12 goblin_warrens 100 6 6 7.8 6 100 100 0 0 0
paladin 3 goblin_warrens 100 5 5 5.5 5.8 86 97 14 0 0
paladin 7 goblin_warrens 100 5 5 5.1 6 100 100 0 0 0
paladin 12 goblin_warrens 100 4 4 5 6 100 100 0 0 0
ranger 3 goblin_warrens 100 6 7 6.6 5.5 55 89 45 0 0
ranger 7 goblin_warrens 100 6 6 6.3 6 100 100 0 0 0
ranger 12 goblin_warrens 100 6 6 6.5 6 100 100 0 0 0
rogue 3 goblin_warrens 100 7 7 7.8 5.8 86 97 14 0 0
rogue 7 goblin_warrens 100 6 6 7.2 6 100 100 0 0 0
rogue 12 goblin_warrens 100 6 6 7.7 6 100 100 0 0 0
sorcerer 3 goblin_warrens 100 8 8 8 5.5 65 83 35 0 0
sorcerer 7 goblin_warrens 100 8 8 8.3 6 98 98 2 0 0
sorcerer 12 goblin_warrens 100 6 6 7.1 6 100 100 0 0 0
warlock 3 goblin_warrens 100 4 5 4.9 5.4 56 81 44 0 0
warlock 7 goblin_warrens 100 5 5 5.8 6 100 100 0 0 0
warlock 12 goblin_warrens 100 5 5 5.9 6 100 100 0 0 0
bard 3 manor_blackspire 100 4 0 4.5 4.3 0 1 100 0 0
bard 7 manor_blackspire 100 6 0 6 6 0 64 100 0 0
bard 12 manor_blackspire 100 4 0 5 6 0 76 100 0 0
cleric 3 manor_blackspire 100 3 0 3.2 4.2 0 1 100 0 0
cleric 7 manor_blackspire 100 4 0 4.4 5.7 0 34 100 0 0
cleric 12 manor_blackspire 100 3 3 4.1 6.1 17 66 83 0 0
druid 3 manor_blackspire 100 4 0 4.3 5.2 0 17 100 0 0
druid 7 manor_blackspire 100 4 2 4.3 6 1 63 99 0 0
druid 12 manor_blackspire 100 4 4 5 6.8 83 84 17 0 0
fighter 3 manor_blackspire 100 4 0 4.1 5.5 0 17 100 0 0
fighter 7 manor_blackspire 100 4 3 4.6 6.2 15 99 85 0 0
fighter 12 manor_blackspire 100 5 5 5.3 7 100 100 0 0 0
mage 3 manor_blackspire 100 5 0 5.3 3.7 0 0 100 0 0
mage 7 manor_blackspire 100 5 7 6.4 5.9 3 60 97 0 0
mage 12 manor_blackspire 100 6 6 6.6 6.5 53 70 47 0 0
paladin 3 manor_blackspire 100 3 0 3.9 5.8 0 37 100 0 0
paladin 7 manor_blackspire 100 4 4 4.2 6.1 9 99 91 0 0
paladin 12 manor_blackspire 100 4 4 4.5 7 96 100 4 0 0
ranger 3 manor_blackspire 100 3 0 4.5 4.7 0 11 100 0 0
ranger 7 manor_blackspire 100 5 5 6 6.3 33 100 67 0 0
ranger 12 manor_blackspire 100 4 4 5.1 7 100 100 0 0 0
rogue 3 manor_blackspire 100 6 0 6.3 5.6 0 26 100 0 0
rogue 7 manor_blackspire 100 6 21 6.9 6 2 100 98 0 0
rogue 12 manor_blackspire 100 5 5 6.2 6.9 85 100 15 0 0
sorcerer 3 manor_blackspire 100 4 0 4.7 3.7 0 1 100 0 0
sorcerer 7 manor_blackspire 100 7 11 7.1 6 2 40 98 0 0
sorcerer 12 manor_blackspire 100 6 6 6.8 6.5 48 62 52 0 0
warlock 3 manor_blackspire 100 4 0 4.1 4.1 0 1 100 0 0
warlock 7 manor_blackspire 100 5 0 5.3 5.9 0 44 100 0 0
warlock 12 manor_blackspire 100 5 4 5 6.4 36 71 64 0 0
bard 3 underdark 100 0 0 0 0 0 0 0 0 0
bard 7 underdark 100 2 0 2.6 3.7 0 6 100 0 0
bard 12 underdark 100 4 0 4 5.7 0 70 100 0 0
cleric 3 underdark 100 0 0 0 0 0 0 0 0 0
cleric 7 underdark 100 3 0 3.4 3.1 0 1 100 0 0
cleric 12 underdark 100 4 0 4.4 4.5 0 10 100 0 0
druid 3 underdark 100 0 0 0 0 0 0 0 0 0
druid 7 underdark 100 2 0 3.4 3.7 0 5 100 0 0
druid 12 underdark 100 4 4 5 6.2 35 85 65 0 0
fighter 3 underdark 100 0 0 0 0 0 0 0 0 0
fighter 7 underdark 100 4 4 4.1 6 7 88 93 0 0
fighter 12 underdark 100 5 5 5 7 100 100 0 0 0
mage 3 underdark 100 0 0 0 0 0 0 0 0 0
mage 7 underdark 100 3 0 3.7 3.3 0 1 100 0 0
mage 12 underdark 100 3 3 4.1 5.7 16 48 84 0 0
paladin 3 underdark 100 0 0 0 0 0 0 0 0 0
paladin 7 underdark 100 4 11 5 5.9 2 86 98 0 0
paladin 12 underdark 100 4 4 4.6 7 98 100 2 0 0
ranger 3 underdark 100 0 0 0 0 0 0 0 0 0
ranger 7 underdark 100 10 8 10.4 6.2 22 94 78 0 0
ranger 12 underdark 100 9 9 10.2 7 100 100 0 0 0
rogue 3 underdark 100 0 0 0 0 0 0 0 0 0
rogue 7 underdark 100 5 10 5.2 5.9 2 84 98 0 0
rogue 12 underdark 100 4 4 5.2 7 96 99 4 0 0
sorcerer 3 underdark 100 0 0 0 0 0 0 0 0 0
sorcerer 7 underdark 100 1 0 2.7 3.2 0 1 100 0 0
sorcerer 12 underdark 100 3 5 4.1 5.3 10 31 90 0 0
warlock 3 underdark 100 0 0 0 0 0 0 0 0 0
warlock 7 underdark 100 3 0 3.2 3.7 0 7 100 0 0
warlock 12 underdark 100 4 3 4.2 5.8 6 68 94 0 0

View File

@@ -0,0 +1,60 @@
# H4 — Charge calibration findings
**Date:** 2026-05-17
**Corpus:** `h4_calibration_v1_all10.jsonl` (15k rows, 10 classes × L3/7/12 × 5 zones × n=100). Summary: `h4_calibration_v1_summary.txt`.
**Baseline for comparison:** `baseline_j2a_v2_all10.jsonl` (post-J2b picker, current shipped state).
**Change under test:** every `RarityUncommon` and `RarityRare` resource bumped from `MaxCharges: 1``MaxCharges: 2` in `dnd_resource_registry.go` (53 resources). Commons (already mostly 2), VeryRare/Legendary (trophy tier) untouched.
## TL;DR
**Charge counts are not the binding constraint on yield.** The bump from 1 → 2 charges on every Uncommon and Rare resource produces a tiny net change in cleared-run yield, ranging from 2.5% (forest) to +4.7% (goblin). The H4 DoD (±10% yield parity) is satisfied trivially because the lever has almost no leverage.
| zone | cleared mean yield (J2b) | cleared mean yield (H4) | Δ% |
| ---------------- | -----------------------: | ----------------------: | ---: |
| goblin_warrens | 5.99 | 6.27 | +4.7 |
| forest_shadows | 8.17 | 7.97 | 2.5 |
| manor_blackspire | 5.50 | 5.37 | 2.4 |
| underdark | 6.30 | 6.16 | 2.2 |
| dragons_lair | — | — | — |
All deltas are within n=100 noise (~±5% on these means).
## Why charge counts don't move the needle
Two compounding mechanics cap per-room yield well below "all charges spent":
1. **Standard/Elite/Patrol interrupts abort the entire room harvest pass.** `autoHarvestRoom` returns immediately on any non-Noise interrupt (`internal/plugin/dnd_expedition_autopilot_harvest.go:160-175`). Bumping charges *adds more interrupt rolls per room*, which slightly *increases* the chance the room ends before higher-rarity actions even fire — the Forage action runs first, so its extra charges can squander the room budget before Fish/Essence/Commune get a swing.
2. **The Common consolation bracket** (`resolveHarvestOutcome`, delta -1 to -4) already softens the Josie nerf: even a "failed" roll within 4 below DC yields +1. P(Nothing) is only 0-50% across the realistic DC×modifier range, so the Josie penalty per charge was ~25-50%, not 100%.
Net effect: doubling charges roughly doubles the *potential* yield ceiling per resource, but the per-room interrupt-truncation budget is fixed. Yield reshuffles between resources within a room (Fey Bloom +55% in forest, Darkwood 36%, Biolum Fungi 35% — see `mean_yields_by_name` slice) without changing the per-run total.
## What this means for the H4 DoD
The plan's DoD reads:
> Monte Carlo shows yield parity within ±10% across tiers; rare nodes feel rare (success rate visibly < 50%) but not punishing across a full expedition.
Both legs are met:
- **Parity:** all four sampled zones land within ±5% of the J2b baseline.
- **Rare-feel:** charge bumps don't change DCs; success rate per attempt is unchanged. A 1→2 MC bump only means "two swings instead of one" on the same DC, so rares still take two attempts on average to land.
The implicit goal — restore pre-Josie yields — is **not** met. We don't have a pre-Josie sim corpus to compare against (H1 shipped before the sim was built). Analytically, pre-Josie yields would be ~1.3× (low DC) to 2× (high DC) the post-Josie per-charge expectation. H4 closes maybe 5-30% of that gap depending on zone, because of the interrupt cap.
## Recommendation
**Ship the bump as-is.** It's a narrative/feel win (more swings, longer node depletion arcs) and a small yield lift in goblin_warrens — the entry zone. The cost is zero (registry-only change, no code touched, all tests green).
**Do not chase the rest of the gap by tuning interrupts.** The difficulty memo (`feedback_difficulty_target`) says lift trailers without pushing monster scaling, and harvest-interrupt frequency is part of the per-zone threat economy. Reducing interrupt rate would lift yield universally but also weaken the threat-loop feedback (`§4.2 Combat Interrupt`). Out of scope for this branch.
**Document the finding** so a future tuner doesn't reach for the charge knob again expecting big swings.
## Files
- `dnd_resource_registry.go` — 53 resources bumped.
- `h4_calibration_v1_all10.jsonl` / `.err` — sim corpus.
- `h4_calibration_v1_summary.txt` — per-cell summary.
## Open items rolled forward
- **Interrupt-frequency tune** (not H4): if harvest yields ever need to lift another 20-30%, the right lever is `resolveCombatInterrupt` rates in `dnd_expedition_harvest.go`, not charge counts. Park as a future tuning option.
- **Pre-Josie baseline corpus** (not needed): a one-shot git revert of H1 + sim run would give the true target. Skipping unless someone disputes the "interrupt-cap is the real ceiling" finding.