package plugin

// Phase 10 SUB2a — subclass combat hooks.
//
// applySubclassPassives layers subclass-driven flags onto CombatModifiers
// the same way applyClassPassives does. Called from combat_bridge after
// applyClassPassives so subclass effects can stack on top of (or override)
// class baseline.
//
// Subclass active abilities (currently just Berserker's rage) are
// registered via init() into dndActiveAbilities below, gated by
// DnDAbility.Subclass.
//
// Comment convention: `// internal note (not user-facing)` marks block
// comments documenting tuning history (Phase 2/3 balance notes, scaling
// rationale, etc.). They are engineering context and must NOT be lifted
// into Description / Flavor strings by codegen.

func applySubclassPassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacter) {
	if c == nil || c.Subclass == "" {
		return
	}
	switch c.Subclass {
	case SubclassChampion:
		// L5 Improved Critical: crit on nat 19+. L15 Superior Critical
		// lowers this further to 18.
		if c.Level >= 5 {
			mods.CritThreshold = 19
		}
		// Phase 10 SUB3a-i — L10 Additional Fighting Style. 5e lets the
		// player pick a second style; in 1v1 we can't surface a UI for
		// that, so we collapse to the two most generally useful
		// approximations stacked: Defense (+1 AC) and Dueling (+10%
		// damage when one-handed — we don't track grip state, so this is
		// a flat damage bump regardless).
		if c.Level >= 10 {
			stats.AC++
			mods.DamageBonus += 0.10
		}
		// Phase 10 SUB3a-i — L15 Superior Critical: crit floor drops to 18.
		if c.Level >= 15 {
			mods.CritThreshold = 18
		}
	case SubclassBerserker:
		// L5/L7 Berserker is rage-driven (active ability — see init() below)
		// plus L7 Mindless Rage which is condition immunity, irrelevant in
		// our 1v1 model. Phase 10 SUB3a-i adds the always-on L10/L15 layer.
		//
		// L10 Intimidating Presence: 5e is an Action that frightens one
		// target on a failed WIS save. One-shot combat has no action
		// economy to spend on a non-damage Action, so we proxy as 2 rounds
		// of 15% enemy miss chance via SporeCloud (the same channel
		// fog-cloud / Cloak of Shadows uses).
		// L15 Retaliation: 5e Reaction — when struck in melee, one melee
		// attack back. We don't model reactions, so the average per-fight
		// damage uplift is approximated as a +15% damage multiplier.
		if c.Level >= 10 {
			mods.SporeCloud += 2
		}
		if c.Level >= 15 {
			mods.DamageBonus += 0.15
		}
	case SubclassBattleMaster:
		// L7 Know Your Enemy: 5e gives factual info about the target after
		// 1 min observation. We don't model "observation time" — proxy the
		// tactical edge as a flat +1 to attack rolls from L7 onward.
		if c.Level >= 7 {
			stats.AttackBonus++
		}
	case SubclassThief:
		// Phase 10 SUB3a-iii — Thief L10/L15.
		//
		// L10 Use Magic Device (5e L13: ignore class/race/level requirements
		// on magic items, including spell scrolls and wands). We don't have a
		// scroll/wand item system, so we proxy the fantasy as a small free
		// "magical implement" effect every fight: a pre-combat wand-zap and a
		// flat damage bump representing better tactical use of magic items
		// the Thief is now allowed to wield.
		// L15 Thief's Reflexes (5e L17: take two turns during the first round
		// of combat). Engine has no "extra turn" primitive, so we ride the
		// AssassinateBonusDmg + AssassinateAdvantage channel — the same one
		// Gloom Stalker's Dread Ambusher uses for bonus-opener damage. The
		// flat-damage proxy approximates the value of one extra turn's hit.
		if c.Level >= 10 {
			mods.FlatDmgStart += 6
			mods.DamageBonus += 0.05
		}
		if c.Level >= 15 {
			mods.AssassinateAdvantage = true
			mods.AssassinateBonusDmg += 8
		}
	case SubclassAbjuration:
		// L5 Arcane Ward: HP buffer = 2× Mage level. 5e fluffs this as
		// recharged when casting an abjuration spell of L1+; we approximate
		// "always-on" by refilling at the start of every combat — the player
		// can refresh by simply casting any abjuration spell pre-combat
		// (mage_armor, shield_of_faith, etc.), which our combat already
		// handles via pending_cast. L7 Improved Abjuration: + proficiency
		// bonus to ward HP (5e: + prof to Counterspell/Dispel checks; we
		// fold the static piece into the durability buff since Counterspell
		// is reaction-deferred to Phase 11).
		if c.Level >= 5 {
			ward := 2 * c.Level
			if c.Level >= 7 {
				ward += proficiencyBonus(c.Level)
			}
			mods.ArcaneWardHP = ward
		}
		// Phase 10 SUB3b-i — L10 Spell Resistance: 5e gives advantage on
		// saves vs. spells *and* resistance to spell damage. Our 1v1 enemies
		// rarely force spell saves, so the "advantage" half is largely inert;
		// we collapse the effect to a flat 8% incoming-damage reduction
		// (DamageReduct is multiplicative, so 0.92 = ~8% off everything,
		// which is close to "resistance against the spell-damage subset").
		if c.Level >= 10 {
			mods.DamageReduct *= 0.92
		}
		// Phase 10 SUB3b-i — L15 Spell Reflection: 5e lets a successful
		// counterspell redirect the spell back at the caster. We don't model
		// counterspell (reaction primitive deferred to Phase 11), so we ride
		// the ReflectNext channel — the first incoming hit reflects 30% of
		// its damage back. Closest in-engine analogue to "the magic hits the
		// caster instead".
		if c.Level >= 15 {
			mods.ReflectNext += 0.30
		}
	case SubclassNecromancy:
		// Phase 10 SUB3b-i — L15 Improved Undead Thralls. 5e: undead thralls
		// gain +CON mod to HP and attack damage. We don't model an Animate
		// Dead summoning system (SUB2b skipped Undead Thralls for the same
		// reason), but the L15 capstone is too central to skip — proxy a
		// permanent skeletal minion via the PetAttack channel. CON-mod scales
		// dmg, mirroring the 5e formula. L10 Command Undead's combat surface
		// is folded into grimHarvestHeal (deeper authority over death = more
		// life harvested per spell-kill); see dnd_subclass_combat.go below.
		if c.Level >= 15 {
			conMod := abilityModifier(c.CON)
			if conMod < 0 {
				conMod = 0
			}
			thrallDmg := 6 + conMod
			if mods.PetAttackProc < 0.30 {
				mods.PetAttackProc = 0.30
			}
			if mods.PetAttackDmg < thrallDmg {
				mods.PetAttackDmg = thrallDmg
			}
		}
	case SubclassAssassin:
		// L5 Assassinate: advantage on the opening strike + bonus damage
		// stacked on top of the Rogue's existing Sneak Attack auto-crit
		// (proxy for "crits vs. surprised targets"). Bonus scales with
		// level; L7 Impostor — flavored as deeper study of the mark — adds
		// a small surprise-damage bump.
		if c.Level >= 5 {
			mods.AssassinateAdvantage = true
			bonus := c.Level
			if c.Level >= 7 {
				bonus += 3
			}
			mods.AssassinateBonusDmg = bonus
		}
	case SubclassLifeDomain:
		// Phase 10 SUB3c — Life Domain L10 Divine Strike. 5e: +1d8 radiant
		// damage on a weapon hit, once per turn (+2d8 at L14). We collapse
		// the once-per-turn cadence to "every weapon hit" since our 1v1 model
		// has no turn-boundary primitive; avg 1d8 = 4, 2d8 = 9 at L14+.
		// L5 Disciple of Life rides lifeDomainHealBonus and L15 Supreme
		// Healing rides lifeDomainSupremeHealing — both out-of-combat heal
		// hooks, so they don't surface here.
		if c.Level >= 14 {
			mods.DivineStrikePerHit += 9
		} else if c.Level >= 10 {
			mods.DivineStrikePerHit += 4
		}
	case SubclassWarDomain:
		// L5 War Priest: bonus-action weapon attack, usable WIS-mod times per
		// long rest. One-shot combat can't model an extra discrete attack, so
		// we proxy throughput with a flat +1 attack bonus and a small damage
		// bump. L7 War God's Blessing is a reaction granting an ally +10 to
		// attack — no allies in 1v1 combat, so it's a no-op.
		if c.Level >= 5 {
			stats.AttackBonus++
			mods.DamageBonus += 0.15
		}
		// Phase 10 SUB3c — War Domain L10 Divine Strike: +1d8 weapon-type
		// damage on every weapon hit (+2d8 at L14). Same channel as Life
		// Domain's radiant version — engine doesn't track damage types.
		if c.Level >= 14 {
			mods.DivineStrikePerHit += 9
		} else if c.Level >= 10 {
			mods.DivineStrikePerHit += 4
		}
		// Phase 10 SUB3c — L15 Avatar of Battle: 5e gives resistance to
		// bludgeoning/piercing/slashing from non-magical weapons. Most enemy
		// damage in our model is non-magical physical, so resistance lands
		// on the bulk of incoming damage. We collapse to a flat 20% incoming
		// reduction — softer than full 50% physical resistance to account
		// for magical/elemental hits the resistance wouldn't catch.
		if c.Level >= 15 {
			mods.DamageReduct *= 0.80
		}
	case SubclassTrickeryDomain:
		// L7 Cloak of Shadows: turn invisible until you attack/cast/end of
		// next turn. We proxy the brief defensive window as 2 rounds of 15%
		// enemy miss chance via SporeCloud (same channel a fog-cloud-style
		// consumable would use). L5 Blessing of the Trickster (Stealth
		// advantage on an ally) is non-combat flavor — skipped.
		if c.Level >= 7 {
			mods.SporeCloud += 2
		}
		// Phase 10 SUB3c — Trickery Domain L10 Divine Strike: +1d8 poison
		// (+2d8 at L14). Same engine channel as the other two domains.
		if c.Level >= 14 {
			mods.DivineStrikePerHit += 9
		} else if c.Level >= 10 {
			mods.DivineStrikePerHit += 4
		}
		// Phase 10 SUB3c — L15 Improved Duplicity: 5e spawns up to 4
		// duplicates and grants advantage to allies adjacent to one. No
		// allies in 1v1, so the literal effect is inert. We proxy the
		// permanent-illusion fantasy passively: +1 round of SporeCloud
		// (duplicates flicker around the foe creating extra confusion on
		// top of Cloak of Shadows) and a small flanking damage bump.
		if c.Level >= 15 {
			mods.SporeCloud++
			mods.DamageBonus += 0.05
		}
	case SubclassHunter:
		// L5 Hunter's Prey: 5e gives a one-of-three pick (Colossus Slayer:
		// +1d8 vs. damaged foes once per turn / Giant Killer reaction /
		// Horde Breaker second attack). One-shot combat can't model the
		// pick UI, so we collapse to the most generally-applicable flavor
		// — Colossus Slayer — as a flat +10% damage bump (any hit after
		// the opener lands on an already-damaged target).
		// L7 Defensive Tactics: collapse to Multiattack Defense — +1 AC.
		if c.Level >= 5 {
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			stats.AC++
		}
		// Phase 10 SUB3d — Hunter L10 Multiattack (Volley/Whirlwind). Both 5e
		// options are AoE; in 1v1 there's only one target, so we collapse the
		// extra-target throughput to "extra hit per round on the one foe" — a
		// flat +15% damage bump (close to a half-extra-attack's value, soft
		// on the upside since AoE wouldn't all land on a single target).
		if c.Level >= 10 {
			mods.DamageBonus += 0.15
		}
		// Phase 10 SUB3d — Hunter L15 Superior Hunter's Defense. 5e picks one
		// of Evasion / Stand Against the Tide / Uncanny Dodge. Uncanny Dodge
		// (halve damage from one attack/turn) is the most generally-applicable
		// — proxy as a permanent ~15% incoming damage reduction.
		if c.Level >= 15 {
			mods.DamageReduct *= 0.85
		}
	case SubclassBeastMaster:
		// L5 Ranger's Companion + Exceptional Training: a baseline combat
		// pet shows up on initiative and pokes the enemy. Use max-style
		// floors — if the player already has a stronger NPC pet active
		// (e.g. a higher-level adventure pet via combat_stats.go), keep
		// theirs. PetAttackDmg scales with level so the companion grows
		// with the Ranger.
		// L7 Bestial Fury: pet attacks twice when commanded. We can't
		// schedule a second swing in the existing pet hook, so we bump
		// the proc rate (extra swings per fight) and per-hit damage.
		if c.Level >= 5 {
			proc := 0.20
			dmg := 2 + c.Level/2
			if c.Level >= 7 {
				proc = 0.35
				dmg += 3
			}
			if mods.PetAttackProc < proc {
				mods.PetAttackProc = proc
			}
			if mods.PetAttackDmg < dmg {
				mods.PetAttackDmg = dmg
			}
		}
		// Phase 10 SUB3d — Beast Master L10 Share Spells. 5e: any spell the
		// Ranger casts on themselves can also affect the pet within 30 ft.
		// In practice this means the pet rides the Ranger's self-buffs
		// (Hunter's Mark damage rider, Pass without Trace, etc.). We fold
		// that into the pet channel: bump per-hit damage and proc rate to
		// reflect the buffed pet, plus a small DamageBonus rider matching
		// the player's own buffed swings sharing.
		if c.Level >= 10 {
			if mods.PetAttackProc < 0.45 {
				mods.PetAttackProc = 0.45
			}
			if mods.PetAttackDmg < 6+c.Level/2 {
				mods.PetAttackDmg = 6 + c.Level/2
			}
			mods.DamageBonus += 0.05
		}
		// Phase 10 SUB3d — Beast Master L15 Superior Bond. 5e: pet immune to
		// charm/fright, returns at 1 HP after a long rest if killed. We don't
		// simulate pet death, so we proxy the "always-there partner" fantasy
		// as a further pet-throughput bump and a small DamageReduct (pet
		// body-blocks more reliably).
		if c.Level >= 15 {
			if mods.PetAttackProc < 0.55 {
				mods.PetAttackProc = 0.55
			}
			extra := 8 + c.Level/2
			if mods.PetAttackDmg < extra {
				mods.PetAttackDmg = extra
			}
			mods.DamageReduct *= 0.92
		}
	case SubclassGloomStalker:
		// L5 Dread Ambusher: +WIS to initiative and +1 attack with +1d8 in
		// the first round. Initiative rides on Speed in our model; the
		// extra opener damage rides AssassinateBonusDmg (consumed on first
		// hit only — the same channel the Assassin uses).
		// L5 Umbral Sight: the in-darkness stealth advantage is wired in
		// subclassSkillAdvantage; here we just give the combat half.
		// L7 Stalker's Flurry: 5e re-rolls a missed attack. The closest
		// engine primitive is AssassinateAdvantage (best of two on the
		// first roll), which is a slightly tighter expression of the
		// same fantasy ("you don't whiff your opener").
		if c.Level >= 5 {
			stats.Speed += abilityModifier(c.WIS)
			mods.AssassinateBonusDmg += 5 + abilityModifier(c.WIS)
		}
		if c.Level >= 7 {
			mods.AssassinateAdvantage = true
		}
		// Phase 10 SUB3d — Gloom Stalker L10 Stalker's Flurry. 5e: re-roll a
		// missed attack once per turn. AssassinateAdvantage already grants
		// "best of two" on the opener (L7 Stalker's Flurry proxy), so the
		// L10 expansion to "every turn" rides as steady throughput — a flat
		// +12% damage bump representing recovered misses across the fight.
		if c.Level >= 10 {
			mods.DamageBonus += 0.12
		}
		// Phase 10 SUB3d — Gloom Stalker L15 Shadowy Dodge. 5e: Reaction —
		// when attacked, impose disadvantage on that attack. We don't model
		// reactions, so collapse the "one attack per round at disadvantage"
		// to a permanent ~10% incoming damage reduction.
		if c.Level >= 15 {
			mods.DamageReduct *= 0.90
		}

	// ── Open5e caster scaffold — Druid / Bard / Sorcerer / Warlock /
	// Paladin subclasses. Passive-only, consistent with the majority of the
	// original fifteen (Champion, Thief, the Mage and Ranger subclasses):
	// no new resource pools or active abilities. Each L5/7/10/15 tier rides
	// an existing CombatModifiers channel — per-case notes name the 5e
	// ability each proxy stands in for. DamageReduct is initialized to 1.0
	// by DerivePlayerStats, so the `*=` reductions compose cleanly.
	case SubclassCircleLand:
		// L5 Natural Recovery: a measured self-heal — rides HealItem (fires
		// once at <50% HP). L7 Land's Stride: +2 Speed (initiative). L10
		// Nature's Ward: resistance to a slice of elemental/poison damage,
		// collapsed to a flat 8% incoming reduction. L15 Nature's Sanctuary:
		// foes hesitate to close — 1 round of SporeCloud — plus a further 10%.
		if c.Level >= 5 {
			mods.HealItem += 5
		}
		if c.Level >= 7 {
			stats.Speed += 2
		}
		if c.Level >= 10 {
			mods.DamageReduct *= 0.92
		}
		if c.Level >= 15 {
			mods.SporeCloud++
			mods.DamageReduct *= 0.90
		}
	case SubclassCircleMoon:
		// L5 Combat Wild Shape: a bestial form's HP pool — an ArcaneWardHP
		// buffer scaling with level — plus the form's heavier hits (+10%
		// damage). L7 Primal Strike: beast attacks bite harder (+5%). L10
		// Elemental Wild Shape: an elemental form's opening slam — a
		// FlatDmgStart burst. L15 Archdruid: the form barely tires — a flat
		// 15% incoming reduction.
		if c.Level >= 5 {
			if buf := 3 * c.Level; mods.ArcaneWardHP < buf {
				mods.ArcaneWardHP = buf
			}
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			mods.DamageBonus += 0.05
		}
		if c.Level >= 10 {
			mods.FlatDmgStart += 8
		}
		if c.Level >= 15 {
			mods.DamageReduct *= 0.85
		}
	case SubclassCircleShepherd:
		// L5 Spirit Totem (Bear): a warding spirit grants a temp-HP buffer.
		// L7 Mighty Summoner: a conjured beast joins the fight — rides the
		// pet channel. L10 Guardian Spirit: the spirits mend you — HealItem.
		// L15 Faithful Summons: the conjured ally is tougher and strikes
		// more often.
		if c.Level >= 5 {
			if buf := 2 * c.Level; mods.ArcaneWardHP < buf {
				mods.ArcaneWardHP = buf
			}
		}
		if c.Level >= 7 {
			if mods.PetAttackProc < 0.25 {
				mods.PetAttackProc = 0.25
			}
			if dmg := 3 + c.Level/2; mods.PetAttackDmg < dmg {
				mods.PetAttackDmg = dmg
			}
		}
		if c.Level >= 10 {
			mods.HealItem += c.Level / 2
		}
		if c.Level >= 15 {
			if mods.PetAttackProc < 0.45 {
				mods.PetAttackProc = 0.45
			}
			if dmg := 8 + c.Level/2; mods.PetAttackDmg < dmg {
				mods.PetAttackDmg = dmg
			}
		}
	case SubclassCollegeLore:
		// L5 Cutting Words: a barbed quip throws the foe off — 2 rounds of
		// SporeCloud miss chance. L7 Additional Magical Secrets: borrowed
		// versatility, a flat +1 to attack rolls. L10 Peerless Skill: a
		// self-aimed Bardic Inspiration die on the opener (FirstAttackBonus).
		// L15: deeper Magical Secrets keep paying out — +10% damage.
		if c.Level >= 5 {
			mods.SporeCloud += 2
		}
		if c.Level >= 7 {
			stats.AttackBonus++
		}
		if c.Level >= 10 {
			mods.FirstAttackBonus += 5
		}
		if c.Level >= 15 {
			mods.DamageBonus += 0.10
		}
	case SubclassCollegeValor:
		// L5 Combat Inspiration: songs that sharpen the blade (+10% damage).
		// L7 Extra Attack: a real second swing (5e literal Extra Attack).
		// Class-identity audit (2026-05-16) — previously proxied as +1
		// AttackBonus + 10% damage; now uses ExtraAttacks. L10 Battle
		// Magic: a bonus-action strike after a spell — a FlatDmgStart
		// burst. L15: +1 AC, late-game shield half of Combat Inspiration.
		if c.Level >= 5 {
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			mods.ExtraAttacks += 1
		}
		if c.Level >= 10 {
			mods.FlatDmgStart += 6
		}
		if c.Level >= 15 {
			stats.AC++
		}
	case SubclassCollegeGlamour:
		// L5 Mantle of Inspiration: a fey-glamour temp-HP buffer. L7 Mantle
		// of Majesty: the foe is briefly enthralled — skips its first
		// attack. L10 Enthralling Performance: lingering fascination — 2
		// rounds of SporeCloud miss chance. L15 Unbreakable Majesty: foes
		// struggle to bring themselves to strike you — a flat 15% reduction.
		if c.Level >= 5 {
			if buf := 2 * c.Level; mods.ArcaneWardHP < buf {
				mods.ArcaneWardHP = buf
			}
		}
		if c.Level >= 7 {
			mods.SpellEnemySkipFirst = true
		}
		if c.Level >= 10 {
			mods.SporeCloud += 2
		}
		if c.Level >= 15 {
			mods.DamageReduct *= 0.85
		}
	case SubclassDraconicBloodline:
		// L5 Draconic Resilience: scaled hide — +1 AC and an HP buffer. L7
		// Elemental Affinity: CHA folded into spell damage — +10%. L10
		// Dragon Wings: mobility and a tougher frame — an 8% reduction and a
		// small damage bump. L15 Draconic Presence: a frightful aura — 2
		// rounds of SporeCloud miss chance.
		// Phase 2 class-balance: L5 was defense-only; gave it a small bite
		// (10% damage) so the L5 chassis isn't pure attrition vs T4 monsters.
		if c.Level >= 5 {
			stats.AC++
			if mods.ArcaneWardHP < c.Level {
				mods.ArcaneWardHP = c.Level
			}
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			mods.DamageBonus += 0.10
		}
		if c.Level >= 10 {
			mods.DamageReduct *= 0.92
			mods.DamageBonus += 0.05
		}
		if c.Level >= 15 {
			mods.SporeCloud += 2
		}
	case SubclassWildMagic:
		// L5 Tides of Chaos: fortune favors the opener — advantage on the
		// first attack. L7 Bend Luck: nudge a roll your way —
		// FirstAttackBonus. L10 Controlled Chaos: a surge you actually aimed
		// — a FlatDmgStart burst. L15 Spell Bombardment: dice that keep
		// exploding — +15% damage.
		// Phase 2 class-balance: bare advantage-on-opener left this subclass
		// the weakest L5 caster pick (0.065 at T4 pre-tune vs Champion 0.92).
		// Added a 10% damage rider — Wild Magic surges as flavor, ~+1d6 of
		// chaos damage per round in mechanics.
		if c.Level >= 5 {
			mods.AssassinateAdvantage = true
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			mods.FirstAttackBonus += 3
		}
		if c.Level >= 10 {
			mods.FlatDmgStart += 10
		}
		if c.Level >= 15 {
			mods.DamageBonus += 0.15
		}
	case SubclassStormSorcery:
		// L5 Tempestuous Magic: the storm moves first — +1 initiative and
		// +2 Speed. L7 Heart of the Storm: a thunderclap as you cast — a
		// level-scaled FlatDmgStart burst. L10 Storm Guide: you ride the
		// weather — a small 5% reduction and 1 round of SporeCloud. L15
		// Storm's Fury: incoming blows are answered with lightning —
		// ReflectNext on the first hit.
		// Phase 2 class-balance: init/speed alone left this subclass at 0.035
		// T4 pre-tune. Pull a small slice of the L7 thunderclap forward so
		// the L5 chassis has actual damage output.
		if c.Level >= 5 {
			mods.InitiativeBias += 1
			stats.Speed += 2
			mods.FlatDmgStart += 4 + c.Level/2
		}
		if c.Level >= 7 {
			mods.FlatDmgStart += 5 + c.Level/2
		}
		if c.Level >= 10 {
			mods.DamageReduct *= 0.95
			mods.SporeCloud++
		}
		if c.Level >= 15 {
			mods.ReflectNext += 0.30
		}
	case SubclassFiendPatron:
		// L5 Dark One's Blessing: temp HP as foes fall — modeled as an
		// up-front CHA-scaled buffer. L7 Dark One's Own Luck: a nudged roll
		// — FirstAttackBonus. L10 Fiendish Resilience: chosen resistance — a
		// flat 10% incoming reduction. L15 Hurl Through Hell: a foe sent
		// briefly to the lower planes — a heavy FlatDmgStart burst.
		if c.Level >= 5 {
			chaMod := abilityModifier(c.CHA)
			if chaMod < 0 {
				chaMod = 0
			}
			if buf := c.Level + chaMod; mods.ArcaneWardHP < buf {
				mods.ArcaneWardHP = buf
			}
		}
		if c.Level >= 7 {
			mods.FirstAttackBonus += 3
		}
		if c.Level >= 10 {
			mods.DamageReduct *= 0.90
		}
		if c.Level >= 15 {
			mods.FlatDmgStart += 12
		}
	case SubclassArchfeyPatron:
		// L5 Fey Presence: the foe is briefly charmed/frightened — skips its
		// first attack. L7 Misty Escape: you blink out of harm's way — 2
		// rounds of SporeCloud miss chance. L10 Beguiling Defenses: hostile
		// magic rebounds — ReflectNext on the first hit. L15 Dark Delirium:
		// the foe is lost in illusion — more SporeCloud and a small edge.
		if c.Level >= 5 {
			mods.SpellEnemySkipFirst = true
		}
		if c.Level >= 7 {
			mods.SporeCloud += 2
		}
		if c.Level >= 10 {
			mods.ReflectNext += 0.25
		}
		if c.Level >= 15 {
			mods.SporeCloud += 2
			mods.DamageBonus += 0.05
		}
	case SubclassGreatOldOne:
		// L5 Entropic Ward: a missed attack tilts the next exchange — a
		// small reduction and 1 round of SporeCloud. L7 Thought Shield:
		// psychic resilience — a flat 8% incoming reduction. L10 Create
		// Thrall: a dominated mind fights beside you — the pet channel. L15:
		// deeper psychic dominion keeps the thrall in the fight and sharpens
		// your own assault (+10% damage).
		// Phase 2 class-balance: L5 was defense-only (0.130 T4 pre-tune).
		// Added a small bite so the L5 chassis can press the advantage when
		// the foe misses.
		if c.Level >= 5 {
			mods.DamageReduct *= 0.95
			mods.SporeCloud++
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			mods.DamageReduct *= 0.92
		}
		if c.Level >= 10 {
			if mods.PetAttackProc < 0.30 {
				mods.PetAttackProc = 0.30
			}
			if dmg := 4 + c.Level/2; mods.PetAttackDmg < dmg {
				mods.PetAttackDmg = dmg
			}
		}
		if c.Level >= 15 {
			mods.DamageBonus += 0.10
		}
	case SubclassOathDevotion:
		// L5 Sacred Weapon: a blade lit with the oath — +1 attack and
		// radiant damage on every hit (DivineStrikePerHit, the channel the
		// Cleric domains use). L7 Aura of Devotion: a small warding aura —
		// 5% reduction. L10 Aura of Courage/Purity: the aura hardens — a
		// further 8% reduction. L15 Holy Nimbus: a radiant aura that sears
		// the foe on engagement — a FlatDmgStart burst.
		if c.Level >= 5 {
			stats.AttackBonus++
			mods.DivineStrikePerHit += 4
		}
		if c.Level >= 7 {
			mods.DamageReduct *= 0.95
		}
		if c.Level >= 10 {
			mods.DamageReduct *= 0.92
		}
		if c.Level >= 15 {
			mods.FlatDmgStart += 10
		}
	case SubclassOathVengeance:
		// L5 Vow of Enmity: advantage against the marked foe plus the focus
		// to press it — advantage on the opener and +10% damage. L7
		// Relentless Avenger: you keep pace with a fleeing target — +2 Speed
		// and a small damage bump. L10 Soul of Vengeance: an extra strike at
		// the marked foe — +10% damage. L15: the hunt never lets up —
		// a FlatDmgStart opener.
		if c.Level >= 5 {
			mods.AssassinateAdvantage = true
			mods.DamageBonus += 0.10
		}
		if c.Level >= 7 {
			stats.Speed += 2
			mods.DamageBonus += 0.05
		}
		if c.Level >= 10 {
			mods.DamageBonus += 0.10
		}
		if c.Level >= 15 {
			mods.FlatDmgStart += 6
		}
	case SubclassOathAncients:
		// L5 Nature's Wrath: the green and growing things slow the foe — a
		// small reduction. L7 Aura of Warding: resistance to spell damage —
		// a further 8% reduction. L10 Undying Sentinel: you refuse to fall —
		// survive one otherwise-lethal hit (DeathSave). L15 Elder Champion:
		// the old light blazes up — +1 attack and +10% damage.
		if c.Level >= 5 {
			mods.DamageReduct *= 0.95
		}
		if c.Level >= 7 {
			mods.DamageReduct *= 0.92
		}
		if c.Level >= 10 {
			mods.DeathSave = true
		}
		if c.Level >= 15 {
			stats.AttackBonus++
			mods.DamageBonus += 0.10
		}
	}
}

func init() {
	// Berserker rage — active ability that piggybacks on the Fighter
	// stamina pool. 5e specs 3 uses/long-rest as a separate pool, but
	// stamina is already sized at 3 for Fighter and the design's
	// "approachability" constraint preferred reusing the existing pool
	// over introducing per-subclass resource types in SUB2a.
	//
	// While armed and fired:
	//   - +2 flat damage per hit (RageMeleeDmg)
	//   - incoming weapon damage halved (PhysicalResistRage)
	//   - +50% damage multiplier for Frenzy approximation
	//     (one-shot combat can't model "1 bonus attack per turn" literally)
	//
	// Post-combat exhaustion increment lives in
	// persistDnDPostCombatSubclass — Frenzy specs +1 exhaustion after rage
	// ends, and in our model rage spans the whole one-shot combat so we
	// always tick exhaustion when rage fired.
	dndActiveAbilities["rage"] = DnDAbility{
		ID:          "rage",
		Name:        "Rage",
		Class:       ClassFighter,
		Subclass:    SubclassBerserker,
		Resource:    "stamina",
		Description: "Enter rage for the next combat: +2 damage per hit, halve incoming weapon damage, Frenzy bonus attack approximation. +1 exhaustion after.",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.BerserkerRage = true
			mods.RageMeleeDmg = 2
			mods.PhysicalResistRage = true
			mods.FrenzyDmgBonus = 0.5
		},
	}

	// Phase 10 SUB2a-ii — Battle Master superiority-die maneuvers. Each
	// consumes one die from the new "superiority" resource pool (4/long-rest
	// at L5; long-rest refresh in our model — 5e refreshes on short rest).
	// We don't model 5e's interactive "before/after the roll" choice; a die
	// commits at !arm time and fires on the next combat.
	//
	// Three non-reaction maneuvers covered:
	//   - Precision Attack → +d8 (≈+4 flat) on first attack roll
	//   - Tripping Attack  → enemy skips its first attack (reuses Phase 9
	//     SpellEnemySkipFirst)
	//   - Rally            → temp-HP buffer at <50% (reuses HealItem; self
	//     buff since one-shot combat has no ally-target UI)
	dndActiveAbilities["precision_attack"] = DnDAbility{
		ID:          "precision_attack",
		Name:        "Precision Attack",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Add +d8 (≈+4, +5 at L10) to your first attack roll this combat (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// Phase 10 SUB3a-ii — L10 Improved Combat Superiority bumps the
			// die from d8 (avg 4.5) to d10 (avg 5.5).
			bonus := 4
			if c != nil && c.Level >= 10 {
				bonus = 5
			}
			mods.FirstAttackBonus += bonus
		},
	}
	dndActiveAbilities["trip_attack"] = DnDAbility{
		ID:          "trip_attack",
		Name:        "Tripping Attack",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Trip the enemy on engagement: they skip their first attack (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.SpellEnemySkipFirst = true
		},
	}
	dndActiveAbilities["rally"] = DnDAbility{
		ID:          "rally",
		Name:        "Rally",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Steel yourself for the fight: heal at low HP for d8 + CHA mod (d10 at L10) (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// Phase 10 SUB3a-ii — L10 Improved Combat Superiority: d8 → d10.
			base := 4
			if c != nil && c.Level >= 10 {
				base = 5
			}
			mods.HealItem += base + abilityModifier(c.CHA)
		},
	}
	// Three more maneuvers using existing CombatModifiers fields. The
	// remaining four spec maneuvers (Pushing, Goading, Riposte, Commander's
	// Strike) need ally-targeting / reaction mechanics that the one-shot
	// engine doesn't model — they're omitted rather than approximated
	// inaccurately.
	dndActiveAbilities["disarming_attack"] = DnDAbility{
		ID:          "disarming_attack",
		Name:        "Disarming Attack",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Knock the enemy's weapon aside: their damage is reduced for the rest of the fight (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// 25% incoming-damage cut models a fighter who is now
			// swinging an improvised weapon at -d4-ish. DamageReduct
			// is multiplicative (1.0 neutral, lower = less damage),
			// so multiply down.
			mods.DamageReduct *= 0.75
		},
	}
	dndActiveAbilities["menacing_attack"] = DnDAbility{
		ID:          "menacing_attack",
		Name:        "Menacing Attack",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Snarl as you swing — the enemy hesitates and skips its first attack (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.SpellEnemySkipFirst = true
		},
	}
	dndActiveAbilities["parry"] = DnDAbility{
		ID:          "parry",
		Name:        "Parry",
		Class:       ClassFighter,
		Subclass:    SubclassBattleMaster,
		Resource:    "superiority",
		Description: "Set yourself for incoming blows — physical damage taken is sharply reduced (consumes 1 superiority die).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// 40% reduction; multiplicative (see disarming_attack note).
			mods.DamageReduct *= 0.60
		},
	}

	// Phase 10 SUB2c — Cleric Channel Divinity. All three Cleric subclasses
	// share the "channel_divinity" resource pool (2/long-rest in our model;
	// 5e refreshes 1/short-rest at L2 and 2/short-rest at L6). One armed
	// expression per fight, mirroring the Battle Master pattern.
	dndActiveAbilities["preserve_life"] = DnDAbility{
		ID:          "preserve_life",
		Name:        "Preserve Life",
		Class:       ClassCleric,
		Subclass:    SubclassLifeDomain,
		Resource:    "channel_divinity",
		Description: "Channel Divinity: pour life force in at low HP — heals 5× Cleric level (consumes 1 channel divinity).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			// 5e caps the heal at half max HP per target; in 1v1 the entire
			// pool lands on the caster. HealItem fires once when player
			// drops below 50%, which matches Preserve Life's "in trouble"
			// fantasy.
			heal := 5 * c.Level
			if c.HPMax > 0 && heal > c.HPMax/2 {
				heal = c.HPMax / 2
			}
			mods.HealItem += heal
		},
	}
	dndActiveAbilities["guided_strike"] = DnDAbility{
		ID:          "guided_strike",
		Name:        "Guided Strike",
		Class:       ClassCleric,
		Subclass:    SubclassWarDomain,
		Resource:    "channel_divinity",
		Description: "Channel Divinity: +10 to your first attack roll this combat (consumes 1 channel divinity).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.FirstAttackBonus += 10
		},
	}
	dndActiveAbilities["invoke_duplicity"] = DnDAbility{
		ID:          "invoke_duplicity",
		Name:        "Invoke Duplicity",
		Class:       ClassCleric,
		Subclass:    SubclassTrickeryDomain,
		Resource:    "channel_divinity",
		Description: "Channel Divinity: an illusory duplicate flanks the foe — advantage on your first attack and +10% damage (consumes 1 channel divinity).",
		Apply: func(c *DnDCharacter, mods *CombatModifiers) {
			mods.AssassinateAdvantage = true
			mods.DamageBonus += 0.10
		},
	}
}

// persistDnDPostCombatSubclass handles end-of-combat subclass bookkeeping.
// Bumps Exhaustion if Berserker rage fired and applies Grim Harvest healing
// if a Necromancy Mage's queued spell delivered the killing blow. Called
// from combat_bridge after SimulateCombat returns.
//
// raged is captured before combat (mods.BerserkerRage at combat-start time);
// result + mods are read here to detect the spell-kill and look up the
// stashed slot level / damage type.
func persistDnDPostCombatSubclass(c *DnDCharacter, raged bool, result CombatResult, mods CombatModifiers) error {
	if c == nil {
		return nil
	}
	dirty := false
	if raged {
		c.Exhaustion++
		dirty = true
	}
	if heal := grimHarvestHeal(c, result, mods); heal > 0 {
		c.HPCurrent = min(c.HPMax, c.HPCurrent+heal)
		dirty = true
	}
	if !dirty {
		return nil
	}
	return SaveDnDCharacter(c)
}

// lifeDomainHealBonus returns the extra HP a Life Domain Cleric's healing
// spell restores. Disciple of Life (L5): +(2 + spell level) HP, with cantrips
// counting as level 1 for the formula. Blessed Healer (L7) — "when casting a
// healing spell on an ally, you regain 2 + slot level HP" — has no ally in
// 1v1 play and is intentionally skipped here. Returns 0 for non-Life-Domain
// callers, non-heal spells, or pre-L5 levels.
func lifeDomainHealBonus(c *DnDCharacter, spell SpellDefinition, slotLevel int) int {
	if c == nil || c.Class != ClassCleric || c.Subclass != SubclassLifeDomain {
		return 0
	}
	if c.Level < 5 || spell.Effect != EffectSpellHeal {
		return 0
	}
	lvl := slotLevel
	if lvl < 1 {
		lvl = 1
	}
	return 2 + lvl
}

// lifeDomainSupremeHealing reports whether a Life Domain Cleric has reached
// L15 Supreme Healing — when true, healing-spell dice should resolve at max
// face value instead of being rolled. resolveHealOutOfCombat consults this to
// decide whether to roll or take max for each die.
func lifeDomainSupremeHealing(c *DnDCharacter) bool {
	return c != nil && c.Class == ClassCleric &&
		c.Subclass == SubclassLifeDomain && c.Level >= 15
}

// grimHarvestHeal returns the HP to restore when a Necromancy Mage's queued
// spell delivered the killing blow. 5e: heal 2× spell level on kill (3× if
// the spell is necrotic). Slot=0 = nothing was stashed → no heal. Returns 0
// if the player lost or the killing blow wasn't the pre-combat spell.
func grimHarvestHeal(c *DnDCharacter, result CombatResult, mods CombatModifiers) int {
	if c == nil || c.Class != ClassMage || c.Subclass != SubclassNecromancy || c.Level < 5 {
		return 0
	}
	if !result.PlayerWon || mods.GrimHarvestSlot <= 0 {
		return 0
	}
	// The spell killed the enemy iff the spell_cast event itself dropped EnemyHP
	// to 0. If a later round-event (a weapon swing, the pet, a concentration
	// aura re-tick) finished the kill, no heal.
	//
	// It is the *last* spell_cast that has to be lethal, not the first. The
	// auto-resolve path casts once, pre-combat, so the two coincide there; the
	// turn-based path can cast every round, and there the stash on the seat's
	// statuses describes that last cast. Reading the first event would let a
	// non-lethal opening cantrip veto a heal the killing spell had earned.
	//
	// No spell_cast event at all cannot happen while the stash is set — both
	// surfaces emit one for every damaging cast — so the loop falling through
	// is not a case, just an absence.
	for i := len(result.Events) - 1; i >= 0; i-- {
		if result.Events[i].Action == "spell_cast" {
			if result.Events[i].EnemyHP > 0 {
				return 0
			}
			break
		}
	}
	// Phase 10 SUB3b-i — L10 Command Undead. The 5e ability is "take control
	// of any undead", which has no surface in our 1v1 combat (no allies to
	// hand summons over to). We re-fluff it here as deeper authority over
	// death amplifying the harvest itself: multipliers tick up by 1 at L10
	// (2× → 3× non-necrotic, 3× → 4× necrotic).
	mult := 2
	if mods.GrimHarvestNecrotic {
		mult = 3
	}
	if c.Level >= 10 {
		mult++
	}
	return mult * mods.GrimHarvestSlot
}
