Files
gogobee/internal/plugin/bestiary_srd.go
prosolis 4934383a9a J3 D8-f #2: T4 difficulty lift (leaders-define-band) + feywild fork1 soft-lock fix
T4 monster tuning so martial leaders land in the 60-75% band (underdark
59/80, feywild 65/84 at L10/L12, n=50); casters trail (class-side gap that
monster tuning provably can't compress -- Pass 1 showed casters pinned at 0%
while martials moved). T5 deferred (walls everyone at its L12 floor; needs an
L15-16 corpus).

- dnd_bestiary.go: underdark elite/boss HP+AC up + caster-lethal proc cuts
  (mind_flayer/drow_mage/roper); feywild HP+AC up.
- bestiary_srd.go: feywild multiattack profiles (fomorian/night_hag/green_hag)
  + Thornmother 2->3 lashes -- HP/AC alone didn't move feywild's low-damage
  roster; multiattack is what pulls facerolling martials into band.
- zone_graph_feywild_crossing.go: free fork1's marsh edge. fork1 was the only
  fork in the game with every exit skill-locked (CHA+Perception, no LockNone)
  and deterministic no-retry rolls -- a prod SOFT-LOCK stranding ~60% of
  players (low CHA+WIS). Kept grove's CHA bargain as a bonus route.
- expedition_sim.go: firstUnlockedForkChoice -- sim fork policy picks the
  first unlocked option instead of blind 'go 1' (which looped forever on
  locked forks); halts fork_all_locked if none.
- tests: graph-wide TestZoneGraphs_NoSoftLockedFork + fork1 free-path guard.

Writeup: sim_results/d8f_findings.md
2026-05-28 19:02:58 -07:00

211 lines
9.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
// SRD-shaped stat blocks for the turn-based elite/boss engine.
//
// The auto-resolve path (SimulateCombat) keeps the simplified one-attack
// DnDMonsterTemplate shape — its Attack value is a gameplay-tuned average that
// bakes a creature's whole action into a single number, and the 2026-05-10
// rebalance is calibrated against it. Do NOT feed these profiles into
// auto-resolve.
//
// The turn-based path can afford canonical SRD multiattack *structure*: the
// player issues a command per round, so a creature making three attack rolls a
// turn is a tactical problem the player can answer (heal, control, flee) rather
// than an un-survivable spike.
//
// Balance basis (2026-05-14 tuning pass): raw SRD damage is calibrated for a
// 4-PC party and one-shots gogobee's single player — slice 1 shipped it
// canonical and it was un-survivable across every tier (marilith ~91/round,
// even Grol ~20/round vs a level-1 mage). So each profile keeps its SRD
// *shape* — attack count, names, per-attack to-hit, and the damage split
// between attacks — but the per-attack Damage values are scaled so the
// profile's round total lands at roughly 1.3x the creature's tuned
// auto-resolve Attack stat (dndBestiary). That 1.3x is deliberate: the
// 2026-05-10 rebalance calibrated the single-attack Attack stat as the
// gameplay floor, and turn-based fights should hit a little harder than
// auto-resolve to make the player's per-round agency (heal / control / flee)
// matter — but multiattack already raises hit reliability vs high-AC builds,
// and monster abilities now fire as riders on top, so the headroom over
// auto-resolve stays modest. Damage flows through the same calcDamage penalty
// formula as the Attack stat, so the two are directly comparable in scale.
//
// File is intentionally NOT dnd_-prefixed (see memory: no new dnd_* identifiers).
// SRDAttack is one attack roll within a creature's multiattack action: a d20
// to-hit and an average damage value for that single strike.
type SRDAttack struct {
Name string // "Longsword", "Bite", "Claw" — surfaces in the round log
AttackBonus int // d20 to-hit modifier; 0 falls back to the template AttackBonus
Damage int // average damage for this single attack
}
// SRDProfile is a creature's full multiattack action — one SRDAttack per attack
// roll it makes on its turn. A creature with no profile registered makes a
// single attack using its DnDMonsterTemplate Attack / AttackBonus.
type SRDProfile struct {
Attacks []SRDAttack
}
// srdProfiles is the turn-based multiattack registry, keyed by bestiary ID.
// Only elites and bosses that reach the turn-based engine need an entry; every
// other creature falls through to the single-attack template path. Per-attack
// Damage is tuned (not raw SRD) — see the balance-basis note above; the
// // -comment after each profile records the tuned auto-resolve Attack stat the
// total was scaled against.
var srdProfiles = map[string]SRDProfile{
// ── Bosses ───────────────────────────────────────────────────────────
"boss_grol_unbroken": {Attacks: []SRDAttack{ // Attack 6 → ~8
{Name: "Morningstar", AttackBonus: 5, Damage: 5},
{Name: "Javelin", AttackBonus: 5, Damage: 3},
}},
"boss_valdris_unburied": {Attacks: []SRDAttack{ // Attack 8 → ~10
{Name: "Paralyzing Touch", AttackBonus: 7, Damage: 6},
{Name: "Necrotic Bolt", AttackBonus: 7, Damage: 4},
}},
"boss_hollow_king": {Attacks: []SRDAttack{ // Attack 10 → ~13
{Name: "Root Slam", AttackBonus: 7, Damage: 7},
{Name: "Root Slam", AttackBonus: 7, Damage: 6},
}},
"boss_dreaming_aboleth": {Attacks: []SRDAttack{ // Attack 16 → ~21
{Name: "Tentacle", AttackBonus: 9, Damage: 7},
{Name: "Tentacle", AttackBonus: 9, Damage: 7},
{Name: "Tentacle", AttackBonus: 9, Damage: 7},
}},
"boss_aldric_blackspire": {Attacks: []SRDAttack{ // Attack 21 → ~27
{Name: "Unarmed Strike", AttackBonus: 9, Damage: 14},
{Name: "Bite", AttackBonus: 9, Damage: 13},
}},
"boss_emberlord_thyrak": {Attacks: []SRDAttack{ // Attack 14 → ~18
{Name: "Molten Maul", AttackBonus: 8, Damage: 9},
{Name: "Molten Maul", AttackBonus: 8, Damage: 9},
}},
"boss_ilvaras_xunyl": {Attacks: []SRDAttack{ // Attack 19 → ~25
{Name: "Scourge", AttackBonus: 9, Damage: 13},
{Name: "Scourge", AttackBonus: 9, Damage: 12},
}},
"boss_thornmother": {Attacks: []SRDAttack{ // D8-f #2: 2→3 lashes, ~23→~36 (faceroll → band)
{Name: "Thorned Lash", AttackBonus: 9, Damage: 13},
{Name: "Thorned Lash", AttackBonus: 9, Damage: 12},
{Name: "Thorned Lash", AttackBonus: 9, Damage: 11},
}},
"boss_infernax": {Attacks: []SRDAttack{ // Attack 38 → ~49
{Name: "Bite", AttackBonus: 11, Damage: 19},
{Name: "Claw", AttackBonus: 11, Damage: 15},
{Name: "Claw", AttackBonus: 11, Damage: 15},
}},
"boss_belaxath": {Attacks: []SRDAttack{ // Attack 31 → ~40
{Name: "Longsword", AttackBonus: 11, Damage: 24},
{Name: "Whip", AttackBonus: 11, Damage: 16},
}},
// ── Multiattack elites ───────────────────────────────────────────────
"hobgoblin_warchief": {Attacks: []SRDAttack{ // Attack 4 → ~5
{Name: "Longsword", AttackBonus: 5, Damage: 3},
{Name: "Longsword", AttackBonus: 5, Damage: 2},
}},
"bandit_captain": {Attacks: []SRDAttack{ // Attack 4 → ~6
{Name: "Scimitar", AttackBonus: 5, Damage: 2},
{Name: "Scimitar", AttackBonus: 5, Damage: 2},
{Name: "Dagger", AttackBonus: 5, Damage: 2},
}},
"owlbear": {Attacks: []SRDAttack{ // Attack 6 → ~8
{Name: "Beak", AttackBonus: 7, Damage: 3},
{Name: "Claws", AttackBonus: 7, Damage: 5},
}},
"merrow": {Attacks: []SRDAttack{ // Attack 4 → ~5
{Name: "Bite", AttackBonus: 5, Damage: 3},
{Name: "Claws", AttackBonus: 5, Damage: 2},
}},
"helmed_horror": {Attacks: []SRDAttack{ // Attack 7 → ~9
{Name: "Longsword", AttackBonus: 6, Damage: 5},
{Name: "Longsword", AttackBonus: 6, Damage: 4},
}},
"drow_elite_warrior": {Attacks: []SRDAttack{ // Attack 8 → ~10
{Name: "Shortsword", AttackBonus: 7, Damage: 4},
{Name: "Shortsword", AttackBonus: 7, Damage: 3},
{Name: "Hand Crossbow", AttackBonus: 7, Damage: 3},
}},
"salamander": {Attacks: []SRDAttack{ // Attack 8 → ~10
{Name: "Spear", AttackBonus: 7, Damage: 5},
{Name: "Tail", AttackBonus: 7, Damage: 5},
}},
"guard_drake": {Attacks: []SRDAttack{ // Attack 4 → ~5
{Name: "Bite", AttackBonus: 5, Damage: 3},
{Name: "Claws", AttackBonus: 5, Damage: 2},
}},
"young_red_dragon": {Attacks: []SRDAttack{ // Attack 16 → ~20
{Name: "Bite", AttackBonus: 10, Damage: 8},
{Name: "Claw", AttackBonus: 10, Damage: 6},
{Name: "Claw", AttackBonus: 10, Damage: 6},
}},
"quickling": {Attacks: []SRDAttack{ // Attack 2 → ~3
{Name: "Dagger", AttackBonus: 5, Damage: 1},
{Name: "Dagger", AttackBonus: 5, Damage: 1},
{Name: "Dagger", AttackBonus: 5, Damage: 1},
}},
"vrock": {Attacks: []SRDAttack{ // Attack 10 → ~13
{Name: "Beak", AttackBonus: 6, Damage: 5},
{Name: "Talons", AttackBonus: 6, Damage: 8},
}},
"hezrou": {Attacks: []SRDAttack{ // Attack 13 → ~17
{Name: "Bite", AttackBonus: 7, Damage: 7},
{Name: "Claw", AttackBonus: 7, Damage: 5},
{Name: "Claw", AttackBonus: 7, Damage: 5},
}},
"nalfeshnee": {Attacks: []SRDAttack{ // Attack 21 → ~28
{Name: "Bite", AttackBonus: 10, Damage: 14},
{Name: "Claw", AttackBonus: 10, Damage: 7},
{Name: "Claw", AttackBonus: 10, Damage: 7},
}},
"marilith": {Attacks: []SRDAttack{ // Attack 26 → ~34
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Longsword", AttackBonus: 11, Damage: 5},
{Name: "Tail", AttackBonus: 11, Damage: 4},
}},
"vampire_spawn": {Attacks: []SRDAttack{ // Attack 8 → ~10
{Name: "Unarmed Strike", AttackBonus: 6, Damage: 6},
{Name: "Bite", AttackBonus: 6, Damage: 4},
}},
// ── Feywild elites (D8-f #2) ─────────────────────────────────────────
// Feywild martials facerolled at 97100% even after an HP/AC raise: the
// roster was single-attack and low-damage, so tankier monsters just took
// longer to kill. Multiattack is the lever that actually pulls leaders
// into band (mirrors underdark's drow_elite). Casters trail (by design).
"fomorian": {Attacks: []SRDAttack{ // Attack 13 → ~21 (2 big fists)
{Name: "Greatclub", AttackBonus: 9, Damage: 11},
{Name: "Greatclub", AttackBonus: 9, Damage: 10},
}},
"night_hag": {Attacks: []SRDAttack{ // Attack 8 → ~12 (claw flurry)
{Name: "Claws", AttackBonus: 7, Damage: 6},
{Name: "Claws", AttackBonus: 7, Damage: 6},
}},
"green_hag": {Attacks: []SRDAttack{ // Attack 6 → ~10 (claw flurry)
{Name: "Claws", AttackBonus: 6, Damage: 5},
{Name: "Claws", AttackBonus: 6, Damage: 5},
}},
}
// enemyAttackProfile returns the attack rolls a creature makes on its turn.
// Registered elites/bosses use their SRD multiattack profile; everyone else
// falls back to a single attack derived from the template stats. A zero
// per-attack AttackBonus falls back to the (tier-scaled) template bonus.
func enemyAttackProfile(enemyID string, stats CombatStats) []SRDAttack {
prof, ok := srdProfiles[enemyID]
if !ok || len(prof.Attacks) == 0 {
return []SRDAttack{{AttackBonus: stats.AttackBonus, Damage: stats.Attack}}
}
out := make([]SRDAttack, len(prof.Attacks))
for i, a := range prof.Attacks {
if a.AttackBonus == 0 {
a.AttackBonus = stats.AttackBonus
}
out[i] = a
}
return out
}