@vibemancer/core 1.0.13 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -658,7 +658,7 @@ declare function currentRuleset(): Record<string, number>;
658
658
  * Used to gate spectator replays: a recorded match can only be re-simulated when
659
659
  * the runtime engine version matches the version that produced the match.
660
660
  */
661
- declare const ENGINE_VERSION = 4256327861353618;
661
+ declare const ENGINE_VERSION = 3372706385669861;
662
662
  /**
663
663
  * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
664
664
  *
@@ -1811,13 +1811,16 @@ declare function useMyThreatsToEnemy(): AnalyzedThreat[];
1811
1811
  * @param projectiles - All projectiles in the game
1812
1812
  * @param myProjectiles - Only the bot's own projectiles (used for filtering)
1813
1813
  * @param ticksUntilReady - Ticks until wizard can start a new action
1814
- * @param options - `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
1814
+ * @param options - `movementMultiplier` is the fraction of MOVEMENT_SPEED the wizard can
1815
+ * actually use right now (casting and channeling are slowed); defaults to 1.
1816
+ * `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
1815
1817
  * out of it for one tick's cost. Defaults to false, which is the safe
1816
1818
  * reading for a caller that does not know its own state.
1817
1819
  * @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
1818
1820
  */
1819
1821
  declare function analyzeThreats(myPos: Position, projectiles: ProjectileState[], myProjectiles: ProjectileState[], ticksUntilReady: number, options?: {
1820
1822
  canCancelCurrentCast?: boolean;
1823
+ movementMultiplier?: number;
1821
1824
  }): AnalyzedThreat[];
1822
1825
 
1823
1826
  /**
@@ -1 +1 @@
1
- export { A as ALL_BOTS, a as ARENA_MAX, b as ARENA_MIN, c as ARENA_SIZE, d as ARENA_WATER_BUFFER, e as ActionBuilder, f as AnalyzedThreat, g as Archlich, h as Archmage, B as BLINK_CAST_TIME, i as BLINK_COOLDOWN, j as BLINK_MAX_COOLDOWN, k as BLINK_MAX_RANGE, l as BLINK_MIN_COOLDOWN, m as BLINK_RANGE, n as BOT_GROUPS, o as Battlemage, p as BlinkEvent, q as Bonemancer, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, w as BudgetExhaustion, C as CASTING_MOVEMENT_MULT, y as COLLISION_RADIUS, z as CastCancelEvent, D as CastStartEvent, E as Critter, I as DEFAULT_SEED, J as Doombringer, K as ENGINE_VERSION, L as EQUIVALENT_ENGINE_VERSIONS, M as EnemyState, N as FIGHT_SPAWN_DISTANCES, F as FightResult, P as FightStats, Q as FightWinner, R as FinalAction, T as Flamecaller, U as GCD_DURATION, V as GameConfig, W as GameState, X as Golem, Y as Hero, Z as Hogger, _ as HomingParams, $ as HookState, a0 as Infernalist, a1 as InternalWizardState, a2 as KNOCKBACK_DAMAGE_THRESHOLD, a3 as KNOCKBACK_DECAY, a4 as KNOCKBACK_DELAY, a5 as KNOCKBACK_SPEED_PER_DAMAGE, a6 as LAVA_BORDER_WIDTH, a7 as Lich, a8 as MATCH_DURATION, a9 as MAX_HEALTH, aa as MISSILE_BASE_CAST, ab as MISSILE_BASE_RADIUS, ac as MISSILE_DAMAGE_POWER, ad as MISSILE_DAMAGE_RADIUS_SCALE, ae as MISSILE_DAMAGE_SCALE, af as MISSILE_HOMING_COEFF, ag as MISSILE_MIN_CAST_TIME, ah as MISSILE_MIN_DAMAGE, ai as MISSILE_MIN_DURATION, aj as MISSILE_MIN_SPEED, ak as MISSILE_RADIUS_PER_DAMAGE, al as MISSILE_SPEED_DURATION_BASELINE, am as MISSILE_SPEED_DURATION_COEFF, an as MISSILE_TURN_DURATION_COEFF, ao as MOVEMENT_SPEED, ap as MOVE_SPEED, aq as ManualMatch, ar as ManualMatchInitOptions, as as ManualMatchOptions, at as ManualMatchStepRequest, au as MatchWinner, av as MissileAction, aw as MissileActions, ax as MissileConfig, ay as MissileContext, az as MissileExpiredEvent, aA as MissileFunction, aB as MissileHitEvent, aC as MissileLaunchEvent, aD as MissileOobEvent, aE as MissileTemplate, aF as Nightblade, aG as ParamDeclaration, aH as Position, aI as ProjectileState, aJ as Pyromancer, aK as RULES, aL as RULESET_RANGES, aM as RefObject, aN as Rookie, aO as SHIELD_CAST_TIME, aP as SHIELD_DECAY_PER_SECOND, aQ as SHIELD_DECAY_RATE, aR as SHIELD_MAX_BLOCK, aS as SHIELD_MAX_STRENGTH, aT as SHIELD_MIN_BLOCK, aU as SHIELD_MIN_STRENGTH, aV as SPAWN_DISTANCE, aW as SeekerParams, aX as Sentinel, aY as Shadowblade, aZ as ShieldBlockEvent, a_ as ShieldStartEvent, a$ as SimEvent, S as SimulateResult, b0 as Spellbinder, b1 as Spellseeker, b2 as Spellshot, b3 as Spellspinner, b4 as Spelltracer, b5 as Spellweaver, b6 as SpiralParams, b7 as StepResult, b8 as Stormcaller, b9 as Stormchaser, ba as Stormforger, bb as StraightParams, bc as TICKS_PER_SECOND, bd as TICK_DURATION_MS, be as TargetDummy, bf as TestBotBuilder, bg as TestFightResult, bh as TestSimulateResult, bi as TraceBotSummary, bj as TraceEvent, bk as TraceEventType, bl as TraceSummary, bm as Turtle, bn as Velocity, bo as Voidblade, bp as WARMUP_DURATION_TOLERANCE, bq as WARMUP_MAX_BONUS, br as WARMUP_MAX_PENALTY, bs as WARMUP_SPEED_TOLERANCE, bt as WARMUP_TURN_TOLERANCE, bu as WIZARD_HEALTH, bv as WIZARD_RADIUS, bw as Warmage, bx as WizardActions, by as WizardContext, bz as WizardDeathEvent, bA as WizardEntry, bB as WizardFunction, bC as WizardGroup, bD as WizardLavaDeathEvent, bE as WizardState, bF as WorkerFactory, bG as WorkerLike, bH as aim, bI as analyzeThreats, bJ as angleDiff, bK as angleInRange, bL as angleTo, bM as applyDamage, bN as applyRulesetOverrides, bO as blink, bP as browserSandboxFight, bQ as browserSandboxSimulate, bR as calculateBlinkCooldown, bS as calculateMissileCastTime, bT as calculateMissileRadius, bU as calculateMissileSimilarity, bV as calculateShieldBlock, bW as calculateWarmupMultiplier, bX as canReplayMatch, bY as cancel, bZ as cancelCast, b_ as clampPositionToArena, b$ as clampToArena, c0 as clampToSafeZone, c1 as clearHooks, c2 as clearParamValues, c3 as completeCast, c5 as createEntitySeed, c7 as createInitialState, c8 as createRandom, c9 as createWorkerScript, ca as currentRuleset, cb as diagnoseTrace, cc as directionAway, cd as directionTo, ce as distanceTo, cf as effectiveTurnRateCost, cg as extractAction, ch as extractMissileAction, ci as extractStats, cj as extractTraceEvents, ck as fight, cl as findInRange, cm as findNearest, cn as fitMissileForEscapingTarget, co as fitMissileToBudget, cp as flyStraight, cq as formatDiagnosis, cr as formatStats, cs as formatTraceEvents, ct as formatTraceSummary, cu as generateCandidates, cv as generateCombos, cw as getAdaptiveMissileConfig, cx as getEffectiveRange, cy as getLeadPosition, cz as getMissileCastTime, cA as getMissileContext, cB as getPlayerState, cC as getWizardContext, cD as hashCombine, cE as homingMissile, cF as idle, cG as inRange, cH as interceptAngle, cI as isBannedBotName, cJ as isInLava, cK as magnitude, cL as matchesAnyBanRule, cN as missile, cO as move, cP as moveInDirection, cQ as moveProjectile, cR as moveTowardSafely, cS as moveWizard, cT as narrowRange, cU as nextRandom, cV as normalize, cW as normalizeAngle, cX as predictPosition, cZ as resetAllHooks, c_ as resetRuleset, c$ as resolveWizardCollision, d0 as runWithHooks, d1 as runWizardWithContext, d2 as scoreFight, d3 as scoreFightAsWizard2, d4 as seekerMissile, d5 as setParamValues, d6 as shield, d7 as simulate, d8 as simulateMinDuration, d9 as sortByDistance, da as spiralMissile, db as startCast, dc as startDiscovery, dd as stopDiscovery, de as straightMissile, df as summarizeTrace, dg as sweptCircleCollision, dh as testBot, di as tick, dj as turnToAngle, dk as turnToward, dl as updateShield, dm as useArenaSize, dn as useBlinkCooldown, dp as useCastProgress, dq as useCastingSpell, dr as useClosestThreat, ds as useDamageDealt, dt as useDamageTaken, du as useEffect, dv as useEnemy, dw as useHealth, dx as useLastHitTick, dy as useLastMissileConfig, dz as useMemo, dA as useMyProjectiles, dB as useMyThreatsToEnemy, dC as useParam, dD as usePosition, dE as useProjectiles, dF as useRandom, dG as useRef, dH as useShieldStrength, dI as useState, dJ as useStatus, dK as useThreats, dL as useTick, dM as useTicksUntilReady, dN as useVelocity, dO as validateHookCall, dP as validateMissileConfig, dQ as withMissileContext, dR as withWizardContext, dS as wrapWithParams } from './index-browser-Dc-Vl1HI.js';
1
+ export { A as ALL_BOTS, a as ARENA_MAX, b as ARENA_MIN, c as ARENA_SIZE, d as ARENA_WATER_BUFFER, e as ActionBuilder, f as AnalyzedThreat, g as Archlich, h as Archmage, B as BLINK_CAST_TIME, i as BLINK_COOLDOWN, j as BLINK_MAX_COOLDOWN, k as BLINK_MAX_RANGE, l as BLINK_MIN_COOLDOWN, m as BLINK_RANGE, n as BOT_GROUPS, o as Battlemage, p as BlinkEvent, q as Bonemancer, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, w as BudgetExhaustion, C as CASTING_MOVEMENT_MULT, y as COLLISION_RADIUS, z as CastCancelEvent, D as CastStartEvent, E as Critter, I as DEFAULT_SEED, J as Doombringer, K as ENGINE_VERSION, L as EQUIVALENT_ENGINE_VERSIONS, M as EnemyState, N as FIGHT_SPAWN_DISTANCES, F as FightResult, P as FightStats, Q as FightWinner, R as FinalAction, T as Flamecaller, U as GCD_DURATION, V as GameConfig, W as GameState, X as Golem, Y as Hero, Z as Hogger, _ as HomingParams, $ as HookState, a0 as Infernalist, a1 as InternalWizardState, a2 as KNOCKBACK_DAMAGE_THRESHOLD, a3 as KNOCKBACK_DECAY, a4 as KNOCKBACK_DELAY, a5 as KNOCKBACK_SPEED_PER_DAMAGE, a6 as LAVA_BORDER_WIDTH, a7 as Lich, a8 as MATCH_DURATION, a9 as MAX_HEALTH, aa as MISSILE_BASE_CAST, ab as MISSILE_BASE_RADIUS, ac as MISSILE_DAMAGE_POWER, ad as MISSILE_DAMAGE_RADIUS_SCALE, ae as MISSILE_DAMAGE_SCALE, af as MISSILE_HOMING_COEFF, ag as MISSILE_MIN_CAST_TIME, ah as MISSILE_MIN_DAMAGE, ai as MISSILE_MIN_DURATION, aj as MISSILE_MIN_SPEED, ak as MISSILE_RADIUS_PER_DAMAGE, al as MISSILE_SPEED_DURATION_BASELINE, am as MISSILE_SPEED_DURATION_COEFF, an as MISSILE_TURN_DURATION_COEFF, ao as MOVEMENT_SPEED, ap as MOVE_SPEED, aq as ManualMatch, ar as ManualMatchInitOptions, as as ManualMatchOptions, at as ManualMatchStepRequest, au as MatchWinner, av as MissileAction, aw as MissileActions, ax as MissileConfig, ay as MissileContext, az as MissileExpiredEvent, aA as MissileFunction, aB as MissileHitEvent, aC as MissileLaunchEvent, aD as MissileOobEvent, aE as MissileTemplate, aF as Nightblade, aG as ParamDeclaration, aH as Position, aI as ProjectileState, aJ as Pyromancer, aK as RULES, aL as RULESET_RANGES, aM as RefObject, aN as Rookie, aO as SHIELD_CAST_TIME, aP as SHIELD_DECAY_PER_SECOND, aQ as SHIELD_DECAY_RATE, aR as SHIELD_MAX_BLOCK, aS as SHIELD_MAX_STRENGTH, aT as SHIELD_MIN_BLOCK, aU as SHIELD_MIN_STRENGTH, aV as SPAWN_DISTANCE, aW as SeekerParams, aX as Sentinel, aY as Shadowblade, aZ as ShieldBlockEvent, a_ as ShieldStartEvent, a$ as SimEvent, S as SimulateResult, b0 as Spellbinder, b1 as Spellseeker, b2 as Spellshot, b3 as Spellspinner, b4 as Spelltracer, b5 as Spellweaver, b6 as SpiralParams, b7 as StepResult, b8 as Stormcaller, b9 as Stormchaser, ba as Stormforger, bb as StraightParams, bc as TICKS_PER_SECOND, bd as TICK_DURATION_MS, be as TargetDummy, bf as TestBotBuilder, bg as TestFightResult, bh as TestSimulateResult, bi as TraceBotSummary, bj as TraceEvent, bk as TraceEventType, bl as TraceSummary, bm as Turtle, bn as Velocity, bo as Voidblade, bp as WARMUP_DURATION_TOLERANCE, bq as WARMUP_MAX_BONUS, br as WARMUP_MAX_PENALTY, bs as WARMUP_SPEED_TOLERANCE, bt as WARMUP_TURN_TOLERANCE, bu as WIZARD_HEALTH, bv as WIZARD_RADIUS, bw as Warmage, bx as WizardActions, by as WizardContext, bz as WizardDeathEvent, bA as WizardEntry, bB as WizardFunction, bC as WizardGroup, bD as WizardLavaDeathEvent, bE as WizardState, bF as WorkerFactory, bG as WorkerLike, bH as aim, bI as analyzeThreats, bJ as angleDiff, bK as angleInRange, bL as angleTo, bM as applyDamage, bN as applyRulesetOverrides, bO as blink, bP as browserSandboxFight, bQ as browserSandboxSimulate, bR as calculateBlinkCooldown, bS as calculateMissileCastTime, bT as calculateMissileRadius, bU as calculateMissileSimilarity, bV as calculateShieldBlock, bW as calculateWarmupMultiplier, bX as canReplayMatch, bY as cancel, bZ as cancelCast, b_ as clampPositionToArena, b$ as clampToArena, c0 as clampToSafeZone, c1 as clearHooks, c2 as clearParamValues, c3 as completeCast, c5 as createEntitySeed, c7 as createInitialState, c8 as createRandom, c9 as createWorkerScript, ca as currentRuleset, cb as diagnoseTrace, cc as directionAway, cd as directionTo, ce as distanceTo, cf as effectiveTurnRateCost, cg as extractAction, ch as extractMissileAction, ci as extractStats, cj as extractTraceEvents, ck as fight, cl as findInRange, cm as findNearest, cn as fitMissileForEscapingTarget, co as fitMissileToBudget, cp as flyStraight, cq as formatDiagnosis, cr as formatStats, cs as formatTraceEvents, ct as formatTraceSummary, cu as generateCandidates, cv as generateCombos, cw as getAdaptiveMissileConfig, cx as getEffectiveRange, cy as getLeadPosition, cz as getMissileCastTime, cA as getMissileContext, cB as getPlayerState, cC as getWizardContext, cD as hashCombine, cE as homingMissile, cF as idle, cG as inRange, cH as interceptAngle, cI as isBannedBotName, cJ as isInLava, cK as magnitude, cL as matchesAnyBanRule, cN as missile, cO as move, cP as moveInDirection, cQ as moveProjectile, cR as moveTowardSafely, cS as moveWizard, cT as narrowRange, cU as nextRandom, cV as normalize, cW as normalizeAngle, cX as predictPosition, cZ as resetAllHooks, c_ as resetRuleset, c$ as resolveWizardCollision, d0 as runWithHooks, d1 as runWizardWithContext, d2 as scoreFight, d3 as scoreFightAsWizard2, d4 as seekerMissile, d5 as setParamValues, d6 as shield, d7 as simulate, d8 as simulateMinDuration, d9 as sortByDistance, da as spiralMissile, db as startCast, dc as startDiscovery, dd as stopDiscovery, de as straightMissile, df as summarizeTrace, dg as sweptCircleCollision, dh as testBot, di as tick, dj as turnToAngle, dk as turnToward, dl as updateShield, dm as useArenaSize, dn as useBlinkCooldown, dp as useCastProgress, dq as useCastingSpell, dr as useClosestThreat, ds as useDamageDealt, dt as useDamageTaken, du as useEffect, dv as useEnemy, dw as useHealth, dx as useLastHitTick, dy as useLastMissileConfig, dz as useMemo, dA as useMyProjectiles, dB as useMyThreatsToEnemy, dC as useParam, dD as usePosition, dE as useProjectiles, dF as useRandom, dG as useRef, dH as useShieldStrength, dI as useState, dJ as useStatus, dK as useThreats, dL as useTick, dM as useTicksUntilReady, dN as useVelocity, dO as validateHookCall, dP as validateMissileConfig, dQ as withMissileContext, dR as withWizardContext, dS as wrapWithParams } from './index-browser-oQNgZFLr.js';
@@ -228,7 +228,7 @@ import {
228
228
  withMissileContext,
229
229
  withWizardContext,
230
230
  wrapWithParams
231
- } from "./chunk-KGRG7TZS.js";
231
+ } from "./chunk-2AVERV6J.js";
232
232
  export {
233
233
  ALL_BOTS,
234
234
  ARENA_MAX,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { F as FightResult, S as SimulateResult } from './index-browser-Dc-Vl1HI.js';
2
- export { A as ALL_BOTS, a as ARENA_MAX, b as ARENA_MIN, c as ARENA_SIZE, d as ARENA_WATER_BUFFER, e as ActionBuilder, f as AnalyzedThreat, g as Archlich, h as Archmage, B as BLINK_CAST_TIME, i as BLINK_COOLDOWN, j as BLINK_MAX_COOLDOWN, k as BLINK_MAX_RANGE, l as BLINK_MIN_COOLDOWN, m as BLINK_RANGE, n as BOT_GROUPS, o as Battlemage, p as BlinkEvent, q as Bonemancer, r as BotBudgetState, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, w as BudgetExhaustion, x as BudgetLimits, C as CASTING_MOVEMENT_MULT, y as COLLISION_RADIUS, z as CastCancelEvent, D as CastStartEvent, E as Critter, G as DEFAULT_BUDGET, H as DEFAULT_FIGHT_BACKSTOP_MS, I as DEFAULT_SEED, J as Doombringer, K as ENGINE_VERSION, L as EQUIVALENT_ENGINE_VERSIONS, M as EnemyState, N as FIGHT_SPAWN_DISTANCES, O as FightBudget, P as FightStats, Q as FightWinner, R as FinalAction, T as Flamecaller, U as GCD_DURATION, V as GameConfig, W as GameState, X as Golem, Y as Hero, Z as Hogger, _ as HomingParams, $ as HookState, a0 as Infernalist, a1 as InternalWizardState, a2 as KNOCKBACK_DAMAGE_THRESHOLD, a3 as KNOCKBACK_DECAY, a4 as KNOCKBACK_DELAY, a5 as KNOCKBACK_SPEED_PER_DAMAGE, a6 as LAVA_BORDER_WIDTH, a7 as Lich, a8 as MATCH_DURATION, a9 as MAX_HEALTH, aa as MISSILE_BASE_CAST, ab as MISSILE_BASE_RADIUS, ac as MISSILE_DAMAGE_POWER, ad as MISSILE_DAMAGE_RADIUS_SCALE, ae as MISSILE_DAMAGE_SCALE, af as MISSILE_HOMING_COEFF, ag as MISSILE_MIN_CAST_TIME, ah as MISSILE_MIN_DAMAGE, ai as MISSILE_MIN_DURATION, aj as MISSILE_MIN_SPEED, ak as MISSILE_RADIUS_PER_DAMAGE, al as MISSILE_SPEED_DURATION_BASELINE, am as MISSILE_SPEED_DURATION_COEFF, an as MISSILE_TURN_DURATION_COEFF, ao as MOVEMENT_SPEED, ap as MOVE_SPEED, aq as ManualMatch, ar as ManualMatchInitOptions, as as ManualMatchOptions, at as ManualMatchStepRequest, au as MatchWinner, av as MissileAction, aw as MissileActions, ax as MissileConfig, ay as MissileContext, az as MissileExpiredEvent, aA as MissileFunction, aB as MissileHitEvent, aC as MissileLaunchEvent, aD as MissileOobEvent, aE as MissileTemplate, aF as Nightblade, aG as ParamDeclaration, aH as Position, aI as ProjectileState, aJ as Pyromancer, aK as RULES, aL as RULESET_RANGES, aM as RefObject, aN as Rookie, aO as SHIELD_CAST_TIME, aP as SHIELD_DECAY_PER_SECOND, aQ as SHIELD_DECAY_RATE, aR as SHIELD_MAX_BLOCK, aS as SHIELD_MAX_STRENGTH, aT as SHIELD_MIN_BLOCK, aU as SHIELD_MIN_STRENGTH, aV as SPAWN_DISTANCE, aW as SeekerParams, aX as Sentinel, aY as Shadowblade, aZ as ShieldBlockEvent, a_ as ShieldStartEvent, a$ as SimEvent, b0 as Spellbinder, b1 as Spellseeker, b2 as Spellshot, b3 as Spellspinner, b4 as Spelltracer, b5 as Spellweaver, b6 as SpiralParams, b7 as StepResult, b8 as Stormcaller, b9 as Stormchaser, ba as Stormforger, bb as StraightParams, bc as TICKS_PER_SECOND, bd as TICK_DURATION_MS, be as TargetDummy, bf as TestBotBuilder, bg as TestFightResult, bh as TestSimulateResult, bi as TraceBotSummary, bj as TraceEvent, bk as TraceEventType, bl as TraceSummary, bm as Turtle, bn as Velocity, bo as Voidblade, bp as WARMUP_DURATION_TOLERANCE, bq as WARMUP_MAX_BONUS, br as WARMUP_MAX_PENALTY, bs as WARMUP_SPEED_TOLERANCE, bt as WARMUP_TURN_TOLERANCE, bu as WIZARD_HEALTH, bv as WIZARD_RADIUS, bw as Warmage, bx as WizardActions, by as WizardContext, bz as WizardDeathEvent, bA as WizardEntry, bB as WizardFunction, bC as WizardGroup, bD as WizardLavaDeathEvent, bE as WizardState, bF as WorkerFactory, bG as WorkerLike, bH as aim, bI as analyzeThreats, bJ as angleDiff, bK as angleInRange, bL as angleTo, bM as applyDamage, bN as applyRulesetOverrides, bO as blink, bP as browserSandboxFight, bQ as browserSandboxSimulate, bR as calculateBlinkCooldown, bS as calculateMissileCastTime, bT as calculateMissileRadius, bU as calculateMissileSimilarity, bV as calculateShieldBlock, bW as calculateWarmupMultiplier, bX as canReplayMatch, bY as cancel, bZ as cancelCast, b_ as clampPositionToArena, b$ as clampToArena, c0 as clampToSafeZone, c1 as clearHooks, c2 as clearParamValues, c3 as completeCast, c4 as createBudgetState, c5 as createEntitySeed, c6 as createFightBudget, c7 as createInitialState, c8 as createRandom, c9 as createWorkerScript, ca as currentRuleset, cb as diagnoseTrace, cc as directionAway, cd as directionTo, ce as distanceTo, cf as effectiveTurnRateCost, cg as extractAction, ch as extractMissileAction, ci as extractStats, cj as extractTraceEvents, ck as fight, cl as findInRange, cm as findNearest, cn as fitMissileForEscapingTarget, co as fitMissileToBudget, cp as flyStraight, cq as formatDiagnosis, cr as formatStats, cs as formatTraceEvents, ct as formatTraceSummary, cu as generateCandidates, cv as generateCombos, cw as getAdaptiveMissileConfig, cx as getEffectiveRange, cy as getLeadPosition, cz as getMissileCastTime, cA as getMissileContext, cB as getPlayerState, cC as getWizardContext, cD as hashCombine, cE as homingMissile, cF as idle, cG as inRange, cH as interceptAngle, cI as isBannedBotName, cJ as isInLava, cK as magnitude, cL as matchesAnyBanRule, cM as mayAct, cN as missile, cO as move, cP as moveInDirection, cQ as moveProjectile, cR as moveTowardSafely, cS as moveWizard, cT as narrowRange, cU as nextRandom, cV as normalize, cW as normalizeAngle, cX as predictPosition, cY as recordSpend, cZ as resetAllHooks, c_ as resetRuleset, c$ as resolveWizardCollision, d0 as runWithHooks, d1 as runWizardWithContext, d2 as scoreFight, d3 as scoreFightAsWizard2, d4 as seekerMissile, d5 as setParamValues, d6 as shield, d7 as simulate, d8 as simulateMinDuration, d9 as sortByDistance, da as spiralMissile, db as startCast, dc as startDiscovery, dd as stopDiscovery, de as straightMissile, df as summarizeTrace, dg as sweptCircleCollision, dh as testBot, di as tick, dj as turnToAngle, dk as turnToward, dl as updateShield, dm as useArenaSize, dn as useBlinkCooldown, dp as useCastProgress, dq as useCastingSpell, dr as useClosestThreat, ds as useDamageDealt, dt as useDamageTaken, du as useEffect, dv as useEnemy, dw as useHealth, dx as useLastHitTick, dy as useLastMissileConfig, dz as useMemo, dA as useMyProjectiles, dB as useMyThreatsToEnemy, dC as useParam, dD as usePosition, dE as useProjectiles, dF as useRandom, dG as useRef, dH as useShieldStrength, dI as useState, dJ as useStatus, dK as useThreats, dL as useTick, dM as useTicksUntilReady, dN as useVelocity, dO as validateHookCall, dP as validateMissileConfig, dQ as withMissileContext, dR as withWizardContext, dS as wrapWithParams } from './index-browser-Dc-Vl1HI.js';
1
+ import { F as FightResult, S as SimulateResult } from './index-browser-oQNgZFLr.js';
2
+ export { A as ALL_BOTS, a as ARENA_MAX, b as ARENA_MIN, c as ARENA_SIZE, d as ARENA_WATER_BUFFER, e as ActionBuilder, f as AnalyzedThreat, g as Archlich, h as Archmage, B as BLINK_CAST_TIME, i as BLINK_COOLDOWN, j as BLINK_MAX_COOLDOWN, k as BLINK_MAX_RANGE, l as BLINK_MIN_COOLDOWN, m as BLINK_RANGE, n as BOT_GROUPS, o as Battlemage, p as BlinkEvent, q as Bonemancer, r as BotBudgetState, s as BotError, t as BrowserManualMatchSandbox, u as BrowserMatchSandbox, v as BrowserSandboxOptions, w as BudgetExhaustion, x as BudgetLimits, C as CASTING_MOVEMENT_MULT, y as COLLISION_RADIUS, z as CastCancelEvent, D as CastStartEvent, E as Critter, G as DEFAULT_BUDGET, H as DEFAULT_FIGHT_BACKSTOP_MS, I as DEFAULT_SEED, J as Doombringer, K as ENGINE_VERSION, L as EQUIVALENT_ENGINE_VERSIONS, M as EnemyState, N as FIGHT_SPAWN_DISTANCES, O as FightBudget, P as FightStats, Q as FightWinner, R as FinalAction, T as Flamecaller, U as GCD_DURATION, V as GameConfig, W as GameState, X as Golem, Y as Hero, Z as Hogger, _ as HomingParams, $ as HookState, a0 as Infernalist, a1 as InternalWizardState, a2 as KNOCKBACK_DAMAGE_THRESHOLD, a3 as KNOCKBACK_DECAY, a4 as KNOCKBACK_DELAY, a5 as KNOCKBACK_SPEED_PER_DAMAGE, a6 as LAVA_BORDER_WIDTH, a7 as Lich, a8 as MATCH_DURATION, a9 as MAX_HEALTH, aa as MISSILE_BASE_CAST, ab as MISSILE_BASE_RADIUS, ac as MISSILE_DAMAGE_POWER, ad as MISSILE_DAMAGE_RADIUS_SCALE, ae as MISSILE_DAMAGE_SCALE, af as MISSILE_HOMING_COEFF, ag as MISSILE_MIN_CAST_TIME, ah as MISSILE_MIN_DAMAGE, ai as MISSILE_MIN_DURATION, aj as MISSILE_MIN_SPEED, ak as MISSILE_RADIUS_PER_DAMAGE, al as MISSILE_SPEED_DURATION_BASELINE, am as MISSILE_SPEED_DURATION_COEFF, an as MISSILE_TURN_DURATION_COEFF, ao as MOVEMENT_SPEED, ap as MOVE_SPEED, aq as ManualMatch, ar as ManualMatchInitOptions, as as ManualMatchOptions, at as ManualMatchStepRequest, au as MatchWinner, av as MissileAction, aw as MissileActions, ax as MissileConfig, ay as MissileContext, az as MissileExpiredEvent, aA as MissileFunction, aB as MissileHitEvent, aC as MissileLaunchEvent, aD as MissileOobEvent, aE as MissileTemplate, aF as Nightblade, aG as ParamDeclaration, aH as Position, aI as ProjectileState, aJ as Pyromancer, aK as RULES, aL as RULESET_RANGES, aM as RefObject, aN as Rookie, aO as SHIELD_CAST_TIME, aP as SHIELD_DECAY_PER_SECOND, aQ as SHIELD_DECAY_RATE, aR as SHIELD_MAX_BLOCK, aS as SHIELD_MAX_STRENGTH, aT as SHIELD_MIN_BLOCK, aU as SHIELD_MIN_STRENGTH, aV as SPAWN_DISTANCE, aW as SeekerParams, aX as Sentinel, aY as Shadowblade, aZ as ShieldBlockEvent, a_ as ShieldStartEvent, a$ as SimEvent, b0 as Spellbinder, b1 as Spellseeker, b2 as Spellshot, b3 as Spellspinner, b4 as Spelltracer, b5 as Spellweaver, b6 as SpiralParams, b7 as StepResult, b8 as Stormcaller, b9 as Stormchaser, ba as Stormforger, bb as StraightParams, bc as TICKS_PER_SECOND, bd as TICK_DURATION_MS, be as TargetDummy, bf as TestBotBuilder, bg as TestFightResult, bh as TestSimulateResult, bi as TraceBotSummary, bj as TraceEvent, bk as TraceEventType, bl as TraceSummary, bm as Turtle, bn as Velocity, bo as Voidblade, bp as WARMUP_DURATION_TOLERANCE, bq as WARMUP_MAX_BONUS, br as WARMUP_MAX_PENALTY, bs as WARMUP_SPEED_TOLERANCE, bt as WARMUP_TURN_TOLERANCE, bu as WIZARD_HEALTH, bv as WIZARD_RADIUS, bw as Warmage, bx as WizardActions, by as WizardContext, bz as WizardDeathEvent, bA as WizardEntry, bB as WizardFunction, bC as WizardGroup, bD as WizardLavaDeathEvent, bE as WizardState, bF as WorkerFactory, bG as WorkerLike, bH as aim, bI as analyzeThreats, bJ as angleDiff, bK as angleInRange, bL as angleTo, bM as applyDamage, bN as applyRulesetOverrides, bO as blink, bP as browserSandboxFight, bQ as browserSandboxSimulate, bR as calculateBlinkCooldown, bS as calculateMissileCastTime, bT as calculateMissileRadius, bU as calculateMissileSimilarity, bV as calculateShieldBlock, bW as calculateWarmupMultiplier, bX as canReplayMatch, bY as cancel, bZ as cancelCast, b_ as clampPositionToArena, b$ as clampToArena, c0 as clampToSafeZone, c1 as clearHooks, c2 as clearParamValues, c3 as completeCast, c4 as createBudgetState, c5 as createEntitySeed, c6 as createFightBudget, c7 as createInitialState, c8 as createRandom, c9 as createWorkerScript, ca as currentRuleset, cb as diagnoseTrace, cc as directionAway, cd as directionTo, ce as distanceTo, cf as effectiveTurnRateCost, cg as extractAction, ch as extractMissileAction, ci as extractStats, cj as extractTraceEvents, ck as fight, cl as findInRange, cm as findNearest, cn as fitMissileForEscapingTarget, co as fitMissileToBudget, cp as flyStraight, cq as formatDiagnosis, cr as formatStats, cs as formatTraceEvents, ct as formatTraceSummary, cu as generateCandidates, cv as generateCombos, cw as getAdaptiveMissileConfig, cx as getEffectiveRange, cy as getLeadPosition, cz as getMissileCastTime, cA as getMissileContext, cB as getPlayerState, cC as getWizardContext, cD as hashCombine, cE as homingMissile, cF as idle, cG as inRange, cH as interceptAngle, cI as isBannedBotName, cJ as isInLava, cK as magnitude, cL as matchesAnyBanRule, cM as mayAct, cN as missile, cO as move, cP as moveInDirection, cQ as moveProjectile, cR as moveTowardSafely, cS as moveWizard, cT as narrowRange, cU as nextRandom, cV as normalize, cW as normalizeAngle, cX as predictPosition, cY as recordSpend, cZ as resetAllHooks, c_ as resetRuleset, c$ as resolveWizardCollision, d0 as runWithHooks, d1 as runWizardWithContext, d2 as scoreFight, d3 as scoreFightAsWizard2, d4 as seekerMissile, d5 as setParamValues, d6 as shield, d7 as simulate, d8 as simulateMinDuration, d9 as sortByDistance, da as spiralMissile, db as startCast, dc as startDiscovery, dd as stopDiscovery, de as straightMissile, df as summarizeTrace, dg as sweptCircleCollision, dh as testBot, di as tick, dj as turnToAngle, dk as turnToward, dl as updateShield, dm as useArenaSize, dn as useBlinkCooldown, dp as useCastProgress, dq as useCastingSpell, dr as useClosestThreat, ds as useDamageDealt, dt as useDamageTaken, du as useEffect, dv as useEnemy, dw as useHealth, dx as useLastHitTick, dy as useLastMissileConfig, dz as useMemo, dA as useMyProjectiles, dB as useMyThreatsToEnemy, dC as useParam, dD as usePosition, dE as useProjectiles, dF as useRandom, dG as useRef, dH as useShieldStrength, dI as useState, dJ as useStatus, dK as useThreats, dL as useTick, dM as useTicksUntilReady, dN as useVelocity, dO as validateHookCall, dP as validateMissileConfig, dQ as withMissileContext, dR as withWizardContext, dS as wrapWithParams } from './index-browser-oQNgZFLr.js';
3
3
 
4
4
  /**
5
5
  * A compiled bot ready for sandboxed execution.
package/dist/index.js CHANGED
@@ -234,7 +234,7 @@ import {
234
234
  withMissileContext,
235
235
  withWizardContext,
236
236
  wrapWithParams
237
- } from "./chunk-KGRG7TZS.js";
237
+ } from "./chunk-2AVERV6J.js";
238
238
 
239
239
  // src/engine/sandbox.ts
240
240
  import ivm from "isolated-vm";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibemancer/core",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Vibemancer game engine - competitive programming wizard battle arena",
5
5
  "type": "module",
6
6
  "author": "Low Entry",
@@ -23,6 +23,55 @@
23
23
  * So: a bot may not decide how much memory the engine allocates.
24
24
  */
25
25
 
26
+ /**
27
+ * The engine's OWN `Error.prototype.stack` getter, captured once from a known-clean Error.
28
+ *
29
+ * The size bound was written against a huge MESSAGE and missed the other door. `.stack` is an
30
+ * ACCESSOR in V8 — even on an ordinary Error — so a thrown subclass can install a getter that
31
+ * returns 20MB on every read. The message stays one character long, the oversized-message
32
+ * branch never fires, and reading `.stack` kills the isolate. Truncating afterwards refunds
33
+ * nothing: the allocation has already happened, which is why a length assertion cannot catch
34
+ * this and only the isolate dying shows it.
35
+ *
36
+ * The payoff is worse than a crash: "isolate was disposed" is an INFRASTRUCTURE marker, so the
37
+ * ladder SKIPS the match — no rating change, no crash strike. A bot could void any match it
38
+ * was about to lose, on demand.
39
+ *
40
+ * So the engine invokes its own getter and never a bot's. The identity is stable across
41
+ * Errors, survives `error.stack = '...'` assignment, and is inherited by subclasses, so an
42
+ * honest bot keeps its stack and a hostile one simply does not get its getter called.
43
+ */
44
+ const NATIVE_STACK_GETTER = Object.getOwnPropertyDescriptor(new Error('probe'), 'stack')?.get;
45
+
46
+ /**
47
+ * Read `.stack` only when it is the engine's own accessor.
48
+ *
49
+ * Returns null for a bot-installed getter, which is never invoked — so it cannot allocate,
50
+ * cannot throw, and cannot stall.
51
+ */
52
+ function safeStack(error: Error): string | null
53
+ {
54
+ try
55
+ {
56
+ const descriptor = Object.getOwnPropertyDescriptor(error, 'stack');
57
+ // No own `stack` at all: the prototype's native accessor still applies.
58
+ if (!descriptor) return typeof error.stack === 'string' ? error.stack : null;
59
+ if (typeof descriptor.value === 'string') return descriptor.value;
60
+ if (descriptor.get && descriptor.get === NATIVE_STACK_GETTER)
61
+ {
62
+ const stack = descriptor.get.call(error) as unknown;
63
+ return typeof stack === 'string' ? stack : null;
64
+ }
65
+ return null;
66
+ }
67
+ catch
68
+ {
69
+ // Even reading a descriptor can throw on a Proxy. Failing to describe an error must
70
+ // never itself become an error.
71
+ return null;
72
+ }
73
+ }
74
+
26
75
  /** Longest message kept. Generous for a real stack, far below anything that strains memory. */
27
76
  export const MAX_MESSAGE_LENGTH = 1000;
28
77
 
@@ -81,7 +130,8 @@ function rawDescription(thrown: unknown): RawDescription
81
130
 
82
131
  // The stack's first line is "Error: message", so it carries both the message and the
83
132
  // location — which is the whole point, since nothing else gives a player a line number.
84
- const stack = typeof thrown.stack === 'string' ? stripBundleCoordinates(thrown.stack.trim()) : '';
133
+ const raw = safeStack(thrown);
134
+ const stack = raw === null ? '' : stripBundleCoordinates(raw.trim());
85
135
  if (stack) return {text: stack, alreadyBounded: false};
86
136
  return {text: message || `${thrown.name || 'Error'} (thrown with no message)`, alreadyBounded: false};
87
137
  }
@@ -376,8 +376,10 @@ export function tick(
376
376
 
377
377
  if (spell === 'missile' && wizard.missileConfig && wizard.missileAI)
378
378
  {
379
- // Validate missile config to enforce minimums (Lesson #9)
380
- const validConfig = validateMissileConfig(wizard.missileConfig);
379
+ // Already validated and frozen when the cast STARTED, which is the whole
380
+ // point: re-validating the bot's live object here is what let a mutated
381
+ // config be fired. Kept as a local for readability only.
382
+ const validConfig = wizard.missileConfig;
381
383
  const id = `missile-${wizard.id}-${nextTick}`;
382
384
  const projectile: ProjectileState = {
383
385
  id,
@@ -582,7 +584,17 @@ export function tick(
582
584
  + 'time before committing to a config.',
583
585
  });
584
586
  }
585
- wizard.missileConfig = action.startCast.config;
587
+ // SNAPSHOT, not the bot's own object. This used to store the caller's
588
+ // reference, and the cast-completion path re-validated that same reference
589
+ // — so a bot could hand over the cheapest legal missile, keep its reference,
590
+ // and mutate the object while the cast ran. Measured: a 29-tick cast firing
591
+ // a missile whose honest price is 780,205 ticks, killing a full-health
592
+ // opponent at tick 30 of every match, unshieldable because the blast radius
593
+ // scales with damage.
594
+ //
595
+ // Validating here also means the stored config is the SAME one startCast
596
+ // priced, so the engine can never fire something it did not charge for.
597
+ wizard.missileConfig = Object.freeze(validateMissileConfig(action.startCast.config));
586
598
  wizard.missileAI = action.startCast.missileAI;
587
599
  if (action.startCast.direction !== undefined)
588
600
  {
@@ -9,7 +9,7 @@
9
9
  * Used to gate spectator replays: a recorded match can only be re-simulated when
10
10
  * the runtime engine version matches the version that produced the match.
11
11
  */
12
- export const ENGINE_VERSION = 4256327861353618;
12
+ export const ENGINE_VERSION = 3372706385669861;
13
13
 
14
14
  /**
15
15
  * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
@@ -22,4 +22,4 @@ export const ENGINE_VERSION = 4256327861353618;
22
22
  * Generated from `scripts/engine-version-equivalents.json`, which holds the reason for
23
23
  * each entry and the rule for adding one. A wrong entry fails OPEN; read it first.
24
24
  */
25
- export const EQUIVALENT_ENGINE_VERSIONS: readonly number[] = [1688330189011034];
25
+ export const EQUIVALENT_ENGINE_VERSIONS: readonly number[] = [];
@@ -366,10 +366,19 @@ export function useThreats(): AnalyzedThreat[]
366
366
  ctx.projectiles,
367
367
  ctx.myProjectiles,
368
368
  ticksUntilReady,
369
- // Mid-CAST you are one cancel() away from being able to shield, so the analysis
370
- // must not charge you the whole remainder of the cast. A GCD cannot be cancelled,
371
- // so it keeps the pessimistic reading.
372
- {canCancelCurrentCast: ctx.state === 'casting'},
369
+ {
370
+ // Mid-CAST you are one cancel() away from being able to shield, so the analysis
371
+ // must not charge you the whole remainder of the cast. A GCD cannot be
372
+ // cancelled, so it keeps the pessimistic reading.
373
+ canCancelCurrentCast: ctx.state === 'casting',
374
+ // And the dodge half must use the speed you ACTUALLY have. The engine slows a
375
+ // casting wizard to 33% and pins a channeling one to zero (physics.ts), while
376
+ // the analysis stepped at full speed regardless — so it promised dodges that
377
+ // could not be made, for the ~78% of a match a bot spends casting.
378
+ movementMultiplier: ctx.state === 'casting' ? RULES.CASTING_MOVEMENT_MULT
379
+ : ctx.state === 'channeling' ? 0
380
+ : 1,
381
+ },
373
382
  );
374
383
  }, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady, ctx.state]);
375
384
  }
@@ -38,7 +38,9 @@ const THREAT_RELEVANCE_DISTANCE = 500;
38
38
  * @param projectiles - All projectiles in the game
39
39
  * @param myProjectiles - Only the bot's own projectiles (used for filtering)
40
40
  * @param ticksUntilReady - Ticks until wizard can start a new action
41
- * @param options - `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
41
+ * @param options - `movementMultiplier` is the fraction of MOVEMENT_SPEED the wizard can
42
+ * actually use right now (casting and channeling are slowed); defaults to 1.
43
+ * `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
42
44
  * out of it for one tick's cost. Defaults to false, which is the safe
43
45
  * reading for a caller that does not know its own state.
44
46
  * @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
@@ -48,7 +50,7 @@ export function analyzeThreats(
48
50
  projectiles: ProjectileState[],
49
51
  myProjectiles: ProjectileState[],
50
52
  ticksUntilReady: number,
51
- options?: {canCancelCurrentCast?: boolean},
53
+ options?: {canCancelCurrentCast?: boolean; movementMultiplier?: number},
52
54
  ): AnalyzedThreat[]
53
55
  {
54
56
  // Filter to enemy projectiles by excluding our own
@@ -60,7 +62,11 @@ export function analyzeThreats(
60
62
 
61
63
  for (const projectile of enemyProjectiles)
62
64
  {
63
- const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady, options?.canCancelCurrentCast ?? false);
65
+ // A non-finite or absurd multiplier must not poison the analysis, so it is clamped to
66
+ // the only range that means anything: none of your speed, up to all of it.
67
+ const rawMultiplier = options?.movementMultiplier;
68
+ const multiplier = Number.isFinite(rawMultiplier) ? Math.min(1, Math.max(0, rawMultiplier!)) : 1;
69
+ const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady, options?.canCancelCurrentCast ?? false, multiplier);
64
70
  // Only include if it will hit or missile is within reasonable distance
65
71
  const missileDistance = Math.sqrt(
66
72
  (projectile.position.x - myPos.x) ** 2 +
@@ -87,6 +93,7 @@ function analyzeOneThreat(
87
93
  projectile: ProjectileState,
88
94
  ticksUntilReady: number,
89
95
  canCancelCurrentCast: boolean,
96
+ movementMultiplier: number,
90
97
  ): AnalyzedThreat
91
98
  {
92
99
  // Ask the ENGINE for the collision radius rather than re-deriving it.
@@ -122,9 +129,9 @@ function analyzeOneThreat(
122
129
 
123
130
  if (willHit)
124
131
  {
125
- canDodgeLeft = simulateDodge(projectile, targetPos, 'left', dodgeCollisionDist);
126
- canDodgeRight = simulateDodge(projectile, targetPos, 'right', dodgeCollisionDist);
127
- canOutrun = simulateDodge(projectile, targetPos, 'away', dodgeCollisionDist);
132
+ canDodgeLeft = simulateDodge(projectile, targetPos, 'left', dodgeCollisionDist, movementMultiplier);
133
+ canDodgeRight = simulateDodge(projectile, targetPos, 'right', dodgeCollisionDist, movementMultiplier);
134
+ canOutrun = simulateDodge(projectile, targetPos, 'away', dodgeCollisionDist, movementMultiplier);
128
135
 
129
136
  bestDodgeDirection = calculateBestDodgeDirection(
130
137
  projectile,
@@ -240,8 +247,15 @@ function simulateDodge(
240
247
  startPos: Position,
241
248
  direction: 'left' | 'right' | 'away',
242
249
  collisionDist: number,
250
+ movementMultiplier: number,
243
251
  ): boolean
244
252
  {
253
+ // The engine slows a casting wizard to CASTING_MOVEMENT_MULT of full speed, and this
254
+ // stepped at full speed regardless — so for the ~78% of a match a bot spends casting, the
255
+ // analysis promised dodges the engine would not deliver. A tester watched the guide's own
256
+ // template take `bestDodgeDirection` exactly as instructed and die to the missile it had
257
+ // just been told it could avoid.
258
+ const stepDistance = RULES.MOVEMENT_SPEED * movementMultiplier;
245
259
  // Calculate dodge direction vector
246
260
  const dodgeDir = getDodgeDirection(projectile, startPos, direction);
247
261
 
@@ -263,9 +277,9 @@ function simulateDodge(
263
277
  for (let tick = 1; tick <= maxTicks; tick++)
264
278
  {
265
279
  // Stryker disable next-line ArithmeticOperator: RULES.MOVEMENT_SPEED=1 makes * and / identical; sign flip equivalent when wizard is wall-clamped
266
- const newX = targetPos.x + dodgeDir.x * RULES.MOVEMENT_SPEED;
280
+ const newX = targetPos.x + dodgeDir.x * stepDistance;
267
281
  // Stryker disable next-line ArithmeticOperator: same as above
268
- const newY = targetPos.y + dodgeDir.y * RULES.MOVEMENT_SPEED;
282
+ const newY = targetPos.y + dodgeDir.y * stepDistance;
269
283
 
270
284
  // Clamped to the survivable band, which models "dodge as far as you safely can" —
271
285
  // the wizard stops at the lava rather than walking into it. Feasibility is judged on