mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Drops D&D-handbook syntax from the player-facing caster UX in favor of
verbs and feelings (per feedback_accessibility_over_dnd_crunch).
R12 — Drop "Spell DC: N / Spell Atk: +N" line from the spellbook view.
The numbers are pure handbook noise; the engine still computes them.
R13 — renderSlotLine swaps "L1 3/4 · L2 1/3" for "Level 1 ●●●○ · Level 2
●○○○". Filled bullets = energy left, hollow = spent.
R14 — Caster passive Description strings rewritten outcome-first: no
"+5%", no "scaled by your Charisma". Verbs and texture instead. All ten
class passives reworded; mechanics in applyClassPassives unchanged.
R18 — Class menu drops the "(d8, INT/WIS)" suffix → "**Mage** — INT &
WIS". The HP-die number was leaking implementation. !setup class confirm
line gets the same treatment.
P5 — Spellbook headers: "Cantrip" → "Cantrips", "L1" → "Level 1".
P6 — Cast queue line: "_(upcast to L2)_" → "_(empowered)_". Queued-line
in the spellbook view follows suit — drops "(L2 slot)" for "(empowered)"
when the slot is above the spell's base level.
P8 — Comment-convention marker added to dnd_passives.go and
dnd_subclass_combat.go file headers: `// internal note (not user-facing)`
flags Phase 2/3 tuning history so future codegen doesn't lift it into
Description / Flavor strings.
P9 — Spellbook line drops the duplicate "(can learn N more)" trailer —
the "%d / %d" already conveys remaining capacity.
Bonus cleanup. The two Arcane-Trickster slot-ceiling errors ("Arcane
Trickster L5 only has up to L1 slots.") and the "No L1 slot available"
out-of-energy messages get the same jargon scrub: "At level 5, your
Arcane Trickster magic only reaches level-1 spells." / "You're out of
level-1 energy."
Tests. Full plugin suite green; no test pinned the old strings.
960 lines
36 KiB
Go
960 lines
36 KiB
Go
package plugin
|
||
|
||
import "math"
|
||
|
||
// 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: throughput — +1 attack and a further +10% damage
|
||
// proxy for the second swing. L10 Battle Magic: a bonus-action
|
||
// strike after a spell — a FlatDmgStart burst. L15: +1 AC, the
|
||
// late-game shield half of Combat Inspiration.
|
||
if c.Level >= 5 {
|
||
mods.DamageBonus += 0.10
|
||
}
|
||
if c.Level >= 7 {
|
||
stats.AttackBonus++
|
||
mods.DamageBonus += 0.10
|
||
}
|
||
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.
|
||
mods.DamageReduct = math.Max(mods.DamageReduct, 0.25)
|
||
},
|
||
}
|
||
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) {
|
||
mods.DamageReduct = math.Max(mods.DamageReduct, 0.40)
|
||
},
|
||
}
|
||
|
||
// 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 pre-combat spell killed the enemy iff the spell_cast event itself
|
||
// dropped EnemyHP to 0. If a later round-event finished the kill, no heal.
|
||
for _, ev := range result.Events {
|
||
if ev.Action == "spell_cast" {
|
||
if ev.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
|
||
}
|