Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc7667d
Add reactive Anti-Magic Shell and real Malkorok (AMS) encounter
darkmaster2133 Jul 27, 2026
10c58f9
Update DK Frost/Unholy golden test results for P5 preset changes
darkmaster2133 Jul 27, 2026
a6ecf66
Fix Malkorok encounter per review: move reactive AMS to APL, correct …
darkmaster2133 Jul 30, 2026
7dc7c69
Add dedicated Malkorok reactive-AMS presets for Frost/Unholy DK
darkmaster2133 Jul 30, 2026
78ff346
Fix native Windows dev server setup
darkmaster2133 Jul 30, 2026
bbd551a
Trim Malkorok AI comments per review, ignore throwaway BiS tools
darkmaster2133 Jul 30, 2026
3f29d2f
Update .gitignore to remove unused entries
darkmaster2133 Jul 30, 2026
c3b2770
Fix Malkorok reactive-AMS presets and encounter data per review
darkmaster2133 Jul 31, 2026
4d92ce2
Fix Malkorok reactive-AMS presets and encounter data per review
darkmaster2133 Jul 31, 2026
3f41767
Generate per-spec index.html before starting the dev server
darkmaster2133 Aug 1, 2026
5991fb3
Add boss_spell_is_known APL condition; consolidate Malkorok DK presets
darkmaster2133 Aug 1, 2026
d0fd460
Merge branch 'master' of https://github.com/darkmaster2133/mop--malko…
darkmaster2133 Aug 1, 2026
73d6cd9
Add boss_spell_known to translation schema
darkmaster2133 Aug 1, 2026
ddfc79e
Simplify Windows dev binary naming per review
darkmaster2133 Aug 2, 2026
cb153e5
Merge upstream/master into malkorok-ams
darkmaster2133 Aug 2, 2026
6859340
Fix Glyph of Regenerative Magic CD reduction not refreshing minReady …
darkmaster2133 Aug 5, 2026
08de19b
Fix AMS autocast regression from Malkorok bossSpellIsKnown check
darkmaster2133 Aug 5, 2026
b576515
Merge upstream/master into malkorok-ams
darkmaster2133 Aug 5, 2026
e4f59e5
Simplify clean target to use wowsimmop$(BIN_EXT) instead of separate …
darkmaster2133 Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions sim/core/imminent_ability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package core

import "time"

// ImminentMagicAbilityProvider is an optional interface a TargetAI can implement so external
Comment thread
darkmaster2133 marked this conversation as resolved.
Outdated
// systems (e.g. a player's reactive defensive-cooldown logic, like Death Knight Anti-Magic
// Shell) can ask "are you about to unleash harmful magic damage?" without needing to know this
// NPC's specific ability set. Implementations should return true if a harmful magic ability is
// either genuinely mid-cast (a real telegraph) or, absent one, close enough to its own CD
// becoming ready that it's likely to be cast on the NPC's next decision tick.
//
// This is checked via a type assertion against Target.AI, so encounters that don't implement it
// (including any boss AI not built for this reactive-AMS project) simply don't participate --
// callers should always have a non-reactive fallback for that case.
type ImminentMagicAbilityProvider interface {
HasImminentMagicAbility(sim *Simulation, reactionWindow time.Duration) bool
}

// SpellbookHasImminentMagicAbility is the default building block most TargetAI implementations
// of ImminentMagicAbilityProvider can just delegate to (see e.g. any soo/*_ai.go boss file's own
// HasImminentMagicAbility method): true if any harmful (non-physical, damage-dealing) spell
// registered on target is either currently mid-cast (a real telegraph -- Target.Hardcast tracks
// any unit's in-progress cast generically) or has spell.TimeToReady(sim) within reactionWindow.
//
// Bosses whose rotation gates casting on something TimeToReady can't see on its own (e.g.
// Paragons of the Klaxxi's per-NPC active-window gating, since a gated ability's CD timer just
// sits "ready" indefinitely while skipped rather than being re-armed) must check that condition
// themselves before falling back to this helper -- see paragons_of_the_klaxxi_ai.go.
func SpellbookHasImminentMagicAbility(target *Target, sim *Simulation, reactionWindow time.Duration) bool {
hardcasting := target.Hardcast.Expires > sim.CurrentTime

for _, spell := range target.Spellbook {
if !isHarmfulMagicSpell(spell) {
continue
}
if hardcasting && spell.ActionID == target.Hardcast.ActionID {
return true
}
if spell.TimeToReady(sim) <= reactionWindow {
return true
}
}

return false
}

func isHarmfulMagicSpell(spell *Spell) bool {
return spell.ProcMask.Matches(ProcMaskSpellDamage) && !spell.SpellSchool.Matches(SpellSchoolPhysical)
}
73 changes: 67 additions & 6 deletions sim/death_knight/anti_magic_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,20 +143,81 @@ func (dk *DeathKnight) registerAntiMagicShell() {
},
})

// When the user models AMS damage intake, autocast the shell as a low-priority DPS
// cooldown once Runic Power is nearly empty, so the RP from the absorbed magic damage
// tops the bar back up without overcapping. Registered only when intake is configured,
// so the shell stays out of the rotation entirely when the feature is disabled. It is
// cast through the autocastOtherCooldowns action present in every DPS preset.
if dk.Inputs.AvgAMSHit > 0 {
// Autocast the shell as a low-priority DPS cooldown, cast through the
// autocastOtherCooldowns action present in every DPS preset. Registered whenever the user
// has configured damage-intake modeling (AvgAMSHit > 0, the pre-existing opt-in), OR
// whenever the encounter has a real-boss-AI target (see antiMagicShellReactiveSignal) --
// the latter needs AMS to autocast reactively even when the user has correctly left
// AvgAMSHit at 0 for those encounters (real damage happens instead of simulated damage, so
// the stat is otherwise meaningless there). core.AddMajorCooldown has real side effects
// beyond just enabling ShouldActivate: it unconditionally ORs SpellFlagMCD onto the spell
// (sim/core/major_cooldown.go), which measurably changed Blood's golden-output tests even
// with ShouldActivate always returning false -- so this must stay a real conditional
// registration, not "always register but never activate."
if dk.Inputs.AvgAMSHit > 0 || dk.encounterHasRealBossAI() {
Comment thread
darkmaster2133 marked this conversation as resolved.
Outdated
dk.AddMajorCooldown(core.MajorCooldown{
Spell: antiMagicShellSpell,
Type: core.CooldownTypeDPS,
Priority: core.CooldownPriorityLow,

// ShouldActivate is ONLY consulted by getFirstReadyMCD, which only the
// "Autocast Other Cooldowns" APL action calls (see
// APLActionAutocastOtherCooldowns.IsReady in apl_actions_casting.go). A
// manual "Cast Spell" action targeting this spell elsewhere in the APL
// goes through APLActionCastSpell.IsReady instead, which never looks at
// ShouldActivate — so a user-authored restriction on Anti-Magic Shell
// always takes priority over this.
ShouldActivate: func(sim *core.Simulation, character *core.Character) bool {
Comment thread
hillerstorm marked this conversation as resolved.
Outdated
if imminent, handled := antiMagicShellReactiveSignal(sim); handled {
return imminent
}
return dk.Inputs.AvgAMSHit > 0
},
})
}
}

// encounterHasRealBossAI reports whether any target in the current encounter implements
// core.ImminentMagicAbilityProvider (see sim/core/imminent_ability.go and the real-boss-AI
// files in sim/encounters/soo/*_ai.go). Safe to call from Character.Initialize(): target
// construction (which sets each Target's AI field) and target.initialize() both run before the
// player-initialize loop in Environment.initialize(), so every target's AI is already a
// concrete, type-assertable value by this point.
func (dk *DeathKnight) encounterHasRealBossAI() bool {
for _, target := range dk.Env.Encounter.AllTargets {
if _, ok := target.AI.(core.ImminentMagicAbilityProvider); ok {
return true
}
}
return false
}

// antiMagicShellReactionWindow is how far ahead of a boss's own next likely cast (or how far
// into an already-started real telegraph) AMS should be proactively cast, giving the shell's own
// activation plus normal reaction latency room to land before the incoming hit actually resolves.
const antiMagicShellReactionWindow = 2 * time.Second

// antiMagicShellReactiveSignal checks every active encounter target for
// core.ImminentMagicAbilityProvider (implemented by the real WCL-data-driven boss AI files in
// sim/encounters/soo/*_ai.go) and reports whether AMS should be cast right now because at least
// one target has a harmful magic ability imminent. handled reports whether ANY target in the
// encounter implements the interface at all: if none do (i.e. this isn't one of the real-boss-AI
// encounters), the caller should fall back to the old heuristic instead of treating "no reactive
// signal" as "never cast."
func antiMagicShellReactiveSignal(sim *core.Simulation) (imminent bool, handled bool) {
for _, target := range sim.Encounter.ActiveTargets {
provider, ok := target.AI.(core.ImminentMagicAbilityProvider)
if !ok {
continue
}
handled = true
if provider.HasImminentMagicAbility(sim, antiMagicShellReactionWindow) {
return true, true
}
}
return false, handled
}

// antiMagicShellTickOffset returns how far into the shell's window tick i (0-indexed) of
// numTicks lands when the simulated hits are spread evenly across it: tick i fires at
// (i+0.5) * window/numTicks. This centres the hits in equal sub-intervals, so e.g. 5 ticks
Expand Down
Loading
Loading