@vibemancer/core 1.0.11 → 1.0.13

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.
@@ -241,6 +241,10 @@ interface WizardState {
241
241
  lastMissileConfig?: MissileConfig;
242
242
  warmupMultiplier?: number;
243
243
  invincible?: boolean;
244
+ advised?: {
245
+ nonFinite?: boolean;
246
+ unfinishableCast?: boolean;
247
+ };
244
248
  }
245
249
  /**
246
250
  * Projectile (missile) state.
@@ -654,7 +658,7 @@ declare function currentRuleset(): Record<string, number>;
654
658
  * Used to gate spectator replays: a recorded match can only be re-simulated when
655
659
  * the runtime engine version matches the version that produced the match.
656
660
  */
657
- declare const ENGINE_VERSION = 2983169471988242;
661
+ declare const ENGINE_VERSION = 4256327861353618;
658
662
  /**
659
663
  * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
660
664
  *
@@ -873,9 +877,6 @@ interface InternalWizardState extends WizardState {
873
877
  * Initialize a new match state.
874
878
  */
875
879
  declare function createInitialState(_seed: number, spawnDist?: number): GameState;
876
- /**
877
- * Process one game tick.
878
- */
879
880
  declare function tick(currentTick: number, wizard1AI: WizardFunction, wizard2AI: WizardFunction, config: GameConfig, wizards: InternalWizardState[], projectiles: ProjectileState[], missileAIs: Map<string, MissileFunction$1>, matchSeed: number, budgets?: {
880
881
  states: [BotBudgetState, BotBudgetState];
881
882
  limits: BudgetLimits;
@@ -1810,9 +1811,14 @@ declare function useMyThreatsToEnemy(): AnalyzedThreat[];
1810
1811
  * @param projectiles - All projectiles in the game
1811
1812
  * @param myProjectiles - Only the bot's own projectiles (used for filtering)
1812
1813
  * @param ticksUntilReady - Ticks until wizard can start a new action
1814
+ * @param options - `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
1815
+ * out of it for one tick's cost. Defaults to false, which is the safe
1816
+ * reading for a caller that does not know its own state.
1813
1817
  * @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
1814
1818
  */
1815
- declare function analyzeThreats(myPos: Position, projectiles: ProjectileState[], myProjectiles: ProjectileState[], ticksUntilReady: number): AnalyzedThreat[];
1819
+ declare function analyzeThreats(myPos: Position, projectiles: ProjectileState[], myProjectiles: ProjectileState[], ticksUntilReady: number, options?: {
1820
+ canCancelCurrentCast?: boolean;
1821
+ }): AnalyzedThreat[];
1816
1822
 
1817
1823
  /**
1818
1824
  * VIBEMANCER - ACTION BUILDERS
@@ -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-CM0uuzWp.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-Dc-Vl1HI.js';
@@ -228,7 +228,7 @@ import {
228
228
  withMissileContext,
229
229
  withWizardContext,
230
230
  wrapWithParams
231
- } from "./chunk-EO7JO2RZ.js";
231
+ } from "./chunk-KGRG7TZS.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-CM0uuzWp.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-CM0uuzWp.js';
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';
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-EO7JO2RZ.js";
237
+ } from "./chunk-KGRG7TZS.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.11",
3
+ "version": "1.0.13",
4
4
  "description": "Vibemancer game engine - competitive programming wizard battle arena",
5
5
  "type": "module",
6
6
  "author": "Low Entry",
@@ -38,10 +38,24 @@ export const MAX_ERRORS_PER_MATCH = 500;
38
38
  */
39
39
  export function describeThrown(thrown: unknown): string
40
40
  {
41
- return truncate(rawDescription(thrown));
41
+ const {text, alreadyBounded} = rawDescription(thrown);
42
+ // `alreadyBounded` exists because truncating twice destroyed the marker. The oversized-
43
+ // Error branch returns a string that is ALREADY capped and ends in
44
+ // "[stack omitted: message too large]"; running truncate() over that re-sliced to the
45
+ // first MAX_MESSAGE_LENGTH characters — all of them original message text — and cut the
46
+ // marker off every single time. The player was told "truncated, N more characters" and
47
+ // never that a stack had been dropped, which was the one thing the branch existed to say.
48
+ return alreadyBounded ? text : truncate(text);
42
49
  }
43
50
 
44
- function rawDescription(thrown: unknown): string
51
+ /** A description plus whether it is already within the size cap and must not be re-cut. */
52
+ interface RawDescription
53
+ {
54
+ text: string;
55
+ alreadyBounded: boolean;
56
+ }
57
+
58
+ function rawDescription(thrown: unknown): RawDescription
45
59
  {
46
60
  if (thrown instanceof Error)
47
61
  {
@@ -62,26 +76,26 @@ function rawDescription(thrown: unknown): string
62
76
  {
63
77
  // Deliberately NOT reading .stack here. The location is worth having, but not at
64
78
  // the cost of letting a bot decide how much the engine allocates.
65
- return truncate(message) + ' [stack omitted: message too large]';
79
+ return {text: truncate(message) + ' [stack omitted: message too large]', alreadyBounded: true};
66
80
  }
67
81
 
68
82
  // The stack's first line is "Error: message", so it carries both the message and the
69
83
  // location — which is the whole point, since nothing else gives a player a line number.
70
84
  const stack = typeof thrown.stack === 'string' ? stripBundleCoordinates(thrown.stack.trim()) : '';
71
- if (stack) return stack;
72
- return message || `${thrown.name || 'Error'} (thrown with no message)`;
85
+ if (stack) return {text: stack, alreadyBounded: false};
86
+ return {text: message || `${thrown.name || 'Error'} (thrown with no message)`, alreadyBounded: false};
73
87
  }
74
88
 
75
- if (typeof thrown === 'string') return thrown || '(empty string thrown)';
76
- if (thrown === null) return 'null was thrown';
77
- if (thrown === undefined) return 'undefined was thrown';
89
+ if (typeof thrown === 'string') return {text: thrown || '(empty string thrown)', alreadyBounded: false};
90
+ if (thrown === null) return {text: 'null was thrown', alreadyBounded: false};
91
+ if (thrown === undefined) return {text: 'undefined was thrown', alreadyBounded: false};
78
92
 
79
93
  // A thrown object is a deliberate choice by the author — a custom error type, usually —
80
94
  // so its fields are exactly what they wanted to see. String() would discard all of them.
81
95
  try
82
96
  {
83
97
  const json = JSON.stringify(thrown);
84
- if (typeof json === 'string' && json !== '{}') return json;
98
+ if (typeof json === 'string' && json !== '{}') return {text: json, alreadyBounded: false};
85
99
  }
86
100
  catch
87
101
  {
@@ -96,12 +110,12 @@ function rawDescription(thrown: unknown): string
96
110
  // returning it is a silent failure to describe. It happens when JSON.stringify refuses
97
111
  // — a circular reference, or a getter that throws — and a player who sees it learns
98
112
  // nothing at all. Say what we DO know instead: the constructor name and the keys.
99
- if (text === '[object Object]') return describeOpaqueObject(thrown);
100
- return text;
113
+ if (text === '[object Object]') return {text: describeOpaqueObject(thrown), alreadyBounded: false};
114
+ return {text, alreadyBounded: false};
101
115
  }
102
116
  catch
103
117
  {
104
- return '(a value was thrown that could not be described)';
118
+ return {text: '(a value was thrown that could not be described)', alreadyBounded: false};
105
119
  }
106
120
  }
107
121
 
@@ -152,7 +166,13 @@ function describeOpaqueObject(thrown: object): string
152
166
  */
153
167
  function stripBundleCoordinates(stack: string): string
154
168
  {
155
- return stack.replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, '');
169
+ return stack
170
+ // `at helper (match-bundle.js:9108:16)` — the common case, name kept, coordinates gone.
171
+ .replace(/\s*\((?:[^()\s]*match-bundle\.js|<isolated-vm>)[^()]*\)/g, '')
172
+ // `at match-bundle.js:911:62` — V8 emits this for an ANONYMOUS frame, with no
173
+ // parentheses at all, so the version above never matched it and the bogus line number
174
+ // survived in precisely the case with no function name to make up for it.
175
+ .replace(/(\s*at )(?:[^()\s]*match-bundle\.js|<isolated-vm>)[^\s)]*/g, '$1<your bot>');
156
176
  }
157
177
 
158
178
  function truncate(message: string): string
@@ -160,6 +160,33 @@ function buildWizardContext(state: GameState, config: GameConfig, random: () =>
160
160
  /**
161
161
  * Process one game tick.
162
162
  */
163
+
164
+ /**
165
+ * Name the first non-finite number a bot handed the engine this tick, or null.
166
+ *
167
+ * Checks the three doors that were each found separately: the move vector, the blink target,
168
+ * and the aim angle. Returns WHICH field so the message can point at the player's own call
169
+ * rather than saying "something went wrong".
170
+ */
171
+ function nonFiniteField(
172
+ move: {x?: number; y?: number},
173
+ action: {startCast?: {spell?: string; target?: {x?: number; y?: number}}; aimDirection?: number},
174
+ wizard: {blinkTarget?: {x?: number; y?: number}},
175
+ ): {what: string; value: string} | null
176
+ {
177
+ const bad = (v: unknown): boolean => typeof v === 'number' && !Number.isFinite(v);
178
+ if (bad(move?.x)) return {what: 'move(x, y): x', value: String(move.x)};
179
+ if (bad(move?.y)) return {what: 'move(x, y): y', value: String(move.y)};
180
+
181
+ const target = action.startCast?.spell === 'blink' ? action.startCast.target : undefined;
182
+ if (bad(target?.x)) return {what: 'blink(x, y): x', value: String(target!.x)};
183
+ if (bad(target?.y)) return {what: 'blink(x, y): y', value: String(target!.y)};
184
+ if (bad(wizard.blinkTarget?.x)) return {what: 'blink(x, y): x', value: String(wizard.blinkTarget!.x)};
185
+ if (bad(wizard.blinkTarget?.y)) return {what: 'blink(x, y): y', value: String(wizard.blinkTarget!.y)};
186
+
187
+ if (bad(action.aimDirection)) return {what: 'aim(degrees)', value: String(action.aimDirection)};
188
+ return null;
189
+ }
163
190
  export function tick(
164
191
  currentTick: number,
165
192
  wizard1AI: WizardFunction,
@@ -430,6 +457,35 @@ export function tick(
430
457
  // Handle movement (default to no movement if bot didn't provide move)
431
458
  const oldPos = wizard.position;
432
459
  const move = action.move ?? {x: 0, y: 0};
460
+
461
+ // The non-finite guards work, and were completely silent. A bot that divides by zero
462
+ // somewhere just stops moving; the guard saves the match but tells the author nothing,
463
+ // which is the same "looks passive, is broken" failure as an unreported throw — and
464
+ // reached by accident rather than by anything hostile.
465
+ //
466
+ // Reporting deliberately does NOT change behaviour: the clamp still stands and the
467
+ // match plays out identically. Throwing instead would alter results for bots already
468
+ // on the ladder and could feed crash-deactivation, which is far too large a
469
+ // consequence to attach to a diagnostic.
470
+ // The flag lives on the WIZARD, not in a Set here: tick() runs afresh every frame, so a
471
+ // Set declared inside it deduped nothing and the first version of this reported the
472
+ // same paragraph 500 times, straight into the error cap.
473
+ if (!wizard.advised?.nonFinite)
474
+ {
475
+ const bad = nonFiniteField(move, action, wizard);
476
+ if (bad)
477
+ {
478
+ wizard.advised = {...wizard.advised, nonFinite: true};
479
+ errors.push({
480
+ tick: nextTick,
481
+ entityId: wizard.id,
482
+ message: `${bad.what} is not a finite number (it was ${bad.value}). The engine ignored it `
483
+ + 'to keep the match running — the value is treated as zero, so your wizard simply does '
484
+ + 'not do what you asked. This is almost always a divide-by-zero or a subtraction of '
485
+ + 'two equal positions somewhere in your maths. Reported once per match.',
486
+ });
487
+ }
488
+ }
433
489
  wizard.position = moveWizard(wizard, move, 1);
434
490
 
435
491
  // Process delayed knockback (apply impulse after delay expires)
@@ -500,6 +556,32 @@ export function tick(
500
556
  startCast(wizard, castingSpell, castingSpell === 'missile' ? action.startCast.config : undefined);
501
557
  if (castingSpell === 'missile')
502
558
  {
559
+ // A cast longer than the whole match can never complete, so the wizard
560
+ // stands still until time runs out and NOTHING is reported. Every field is
561
+ // clamped by validateMissileConfig — nothing here is NaN — but turnRate is
562
+ // clamped only to `>= 0`, and turnRate 9999 prices the cast at 130,017
563
+ // ticks against a 30,000-tick match. Measured against TargetDummy, a bot
564
+ // that does nothing at all, the result was ten draws: the bot could not
565
+ // even lose in a way that hinted at the cause.
566
+ //
567
+ // The cast is still allowed — refusing it would silently change what bots
568
+ // do, and the defect was never the cast, it was the silence.
569
+ if ((wizard.castDuration ?? 0) > MATCH_DURATION && !wizard.advised?.unfinishableCast)
570
+ {
571
+ wizard.advised = {...wizard.advised, unfinishableCast: true};
572
+ const config = action.startCast.config;
573
+ errors.push({
574
+ tick: nextTick,
575
+ entityId: wizard.id,
576
+ message: `This missile takes ${wizard.castDuration} ticks to cast, but a match `
577
+ + `is only ${MATCH_DURATION} ticks long, so the cast can never finish and this `
578
+ + 'wizard will stand still for the rest of the match. The cost is dominated by '
579
+ + `turnRate (${config?.turnRate ?? '?'}) — it is the most expensive field by far, and `
580
+ + 'unlike the others it is not clamped to a sane maximum. Try a turnRate in the '
581
+ + 'single digits, then use missile-calc or getMissileCastTime() to check the cast '
582
+ + 'time before committing to a config.',
583
+ });
584
+ }
503
585
  wizard.missileConfig = action.startCast.config;
504
586
  wizard.missileAI = action.startCast.missileAI;
505
587
  if (action.startCast.direction !== undefined)
@@ -4,6 +4,7 @@ import {
4
4
  TICKS_PER_SECOND,
5
5
  calculateMissileCastTime,
6
6
  calculateWarmupMultiplier,
7
+ validateMissileConfig,
7
8
  } from '../rules.js';
8
9
 
9
10
  /**
@@ -19,8 +20,19 @@ export function startCast(wizard: WizardState, spell: 'missile' | 'shield' | 'bl
19
20
  // Stryker disable next-line ConditionalExpression: `true && config` equivalent for valid calls (spell is always correct type)
20
21
  if (spell === 'missile' && config)
21
22
  {
23
+ // VALIDATE FIRST. This used to pass the raw config, and the raw formula returns NaN for
24
+ // a config a beginner can easily type — `{damage: -999, speed: 0, duration: -5}` —
25
+ // because of the powers and divisions inside it. `Math.max(1, Math.round(NaN))` is NaN,
26
+ // not 1, so castDuration became NaN and `castProgress >= castDuration` was never true:
27
+ // the cast could NEVER complete and the wizard stood still for the whole match, silently.
28
+ // Measured against TargetDummy, a bot that does nothing: ten draws, zero errors.
29
+ //
30
+ // getMissileCastTime — the function bots call to PREDICT this exact number — has always
31
+ // validated first, so the engine and its own prediction disagreed for precisely the
32
+ // configs where a player most needed them to agree.
33
+ const validated = validateMissileConfig(config);
22
34
  // calculateMissileCastTime includes warmup when lastMissileConfig is passed
23
- const castTimeSec = calculateMissileCastTime(config, wizard.lastMissileConfig);
35
+ const castTimeSec = calculateMissileCastTime(validated, wizard.lastMissileConfig);
24
36
  wizard.castDuration = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
25
37
  // Store warmup multiplier for visualization
26
38
  wizard.warmupMultiplier = calculateWarmupMultiplier(wizard.lastMissileConfig, config);
@@ -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 = 2983169471988242;
12
+ export const ENGINE_VERSION = 4256327861353618;
13
13
 
14
14
  /**
15
15
  * Historical versions that denote the SAME engine as `ENGINE_VERSION`.
@@ -279,12 +279,47 @@ export function flyStraight(): MissileAction
279
279
  };
280
280
  }
281
281
 
282
+ /**
283
+ * Name what a bot handed back, for an error message it can act on.
284
+ *
285
+ * Named specifically rather than by bare typeof: "a object" is both ungrammatical and
286
+ * unhelpful, and the realistic version of this mistake is `async function MyBot()`, where the
287
+ * value is a Promise and the fix is to remove the `async`.
288
+ */
289
+ function describeReturned(value: unknown): string
290
+ {
291
+ if (value === null) return 'null';
292
+ if (value === undefined) return 'undefined';
293
+ if (Array.isArray(value)) return 'an array';
294
+ if (typeof value === 'object')
295
+ {
296
+ if ('then' in value && typeof (value as {then?: unknown}).then === 'function')
297
+ {
298
+ return 'a Promise — your function is `async`, and it must not be: remove the async keyword';
299
+ }
300
+ return 'a plain object';
301
+ }
302
+ return `a ${typeof value}`;
303
+ }
304
+
282
305
  /**
283
306
  * Extract MissileActions from a MissileAction.
284
307
  * Used by the engine to get the actual missile action.
285
308
  */
286
309
  export function extractMissileAction(action: MissileAction): MissileActions
287
310
  {
311
+ // The wizard-level path got a readable message for a bad return; this one did not, and a
312
+ // missile AI is the SECOND function a player writes. Returning `{}` from it produced
313
+ // "TypeError: action._toMissileAction is not a function" over an engine-internal stack —
314
+ // naming a private field that appears nowhere in the API, 84 times in one match.
315
+ if (action === null || action === undefined || typeof action._toMissileAction !== 'function')
316
+ {
317
+ throw new Error(
318
+ `Your missile AI returned ${describeReturned(action)}. Return turnToward(x, y), `
319
+ + 'turnToAngle(degrees) or flyStraight(). The missile AI runs once per tick for every '
320
+ + 'missile you have in flight, and it must return one of those every time.',
321
+ );
322
+ }
288
323
  return action._toMissileAction();
289
324
  }
290
325
 
@@ -312,21 +347,6 @@ export function extractAction(finalAction: FinalAction): WizardActions
312
347
  // Named specifically rather than by bare typeof: "a object" is both ungrammatical and
313
348
  // unhelpful, and the realistic version of this mistake is `async function MyBot()`,
314
349
  // where the value is a Promise and the fix is to remove the `async`.
315
- const describeReturned = (value: unknown): string =>
316
- {
317
- if (value === null) return 'null';
318
- if (value === undefined) return 'undefined';
319
- if (Array.isArray(value)) return 'an array';
320
- if (typeof value === 'object')
321
- {
322
- if ('then' in value && typeof value.then === 'function')
323
- {
324
- return 'a Promise — your bot function is `async`, and it must not be: remove the async keyword';
325
- }
326
- return 'a plain object';
327
- }
328
- return `a ${typeof value}`;
329
- };
330
350
  const got = describeReturned(finalAction);
331
351
  throw new Error(
332
352
  `Your bot returned ${got}. Return one of: idle(), move(x, y), missile(config, ai, angle), `
@@ -366,8 +366,12 @@ 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
373
  );
370
- }, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady]);
374
+ }, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady, ctx.state]);
371
375
  }
372
376
 
373
377
  /**
@@ -19,6 +19,14 @@ import {distanceTo} from '../utils/distance.js';
19
19
  import {angleTo, normalizeAngle, angleDiff} from '../utils/angles.js';
20
20
  import type {AnalyzedThreat} from './types.js';
21
21
 
22
+ /**
23
+ * Ticks lost to cancel() before a new cast can begin.
24
+ *
25
+ * The guide states it: cancel() "returns you to idle immediately and costs NO cooldown, but
26
+ * it takes effect after this tick's cast check — so you can shield on the NEXT tick".
27
+ */
28
+ const CANCEL_COST_TICKS = 1;
29
+
22
30
  // Maximum distance at which a missile is still tracked as a potential threat
23
31
  // (even if simulation says it will miss — homing missiles can change course)
24
32
  const THREAT_RELEVANCE_DISTANCE = 500;
@@ -30,6 +38,9 @@ const THREAT_RELEVANCE_DISTANCE = 500;
30
38
  * @param projectiles - All projectiles in the game
31
39
  * @param myProjectiles - Only the bot's own projectiles (used for filtering)
32
40
  * @param ticksUntilReady - Ticks until wizard can start a new action
41
+ * @param options - `canCancelCurrentCast` when the wizard is mid-CAST and could cancel()
42
+ * out of it for one tick's cost. Defaults to false, which is the safe
43
+ * reading for a caller that does not know its own state.
33
44
  * @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
34
45
  */
35
46
  export function analyzeThreats(
@@ -37,6 +48,7 @@ export function analyzeThreats(
37
48
  projectiles: ProjectileState[],
38
49
  myProjectiles: ProjectileState[],
39
50
  ticksUntilReady: number,
51
+ options?: {canCancelCurrentCast?: boolean},
40
52
  ): AnalyzedThreat[]
41
53
  {
42
54
  // Filter to enemy projectiles by excluding our own
@@ -48,7 +60,7 @@ export function analyzeThreats(
48
60
 
49
61
  for (const projectile of enemyProjectiles)
50
62
  {
51
- const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady);
63
+ const analysis = analyzeOneThreat(myPos, projectile, ticksUntilReady, options?.canCancelCurrentCast ?? false);
52
64
  // Only include if it will hit or missile is within reasonable distance
53
65
  const missileDistance = Math.sqrt(
54
66
  (projectile.position.x - myPos.x) ** 2 +
@@ -74,6 +86,7 @@ function analyzeOneThreat(
74
86
  targetPos: Position,
75
87
  projectile: ProjectileState,
76
88
  ticksUntilReady: number,
89
+ canCancelCurrentCast: boolean,
77
90
  ): AnalyzedThreat
78
91
  {
79
92
  // Ask the ENGINE for the collision radius rather than re-deriving it.
@@ -123,13 +136,22 @@ function analyzeOneThreat(
123
136
  );
124
137
  }
125
138
 
126
- // Calculate shield timing
127
- // Can block if: ticksUntilReady + shield cast time < ticksToImpact
128
- // Stryker disable next-line EqualityOperator,ArithmeticOperator: ±1 tick boundary is intentional buffer, not precisely testable
129
- const canBlockInTime = ticksToImpact > ticksUntilReady + RULES.SHIELD_CAST_TIME + 1;
130
-
131
- // When to start casting shield (leave 1 tick buffer)
132
- const ticksToStartShield = Math.max(0, ticksToImpact - RULES.SHIELD_CAST_TIME - 1);
139
+ // Shield timing. These two fields MUST be satisfiable together: a bot acts on
140
+ // `canBlockInTime && ticksToStartShield === 0`, so if they can never both hold the bot
141
+ // never shields. They used to disagree canBlockInTime charged the wizard the whole
142
+ // remainder of its cast while ticksToStartShield ignored the cast entirely — and mid-cast
143
+ // that made them mutually exclusive. Measured cost: zero shields raised in a full match.
144
+ //
145
+ // The pessimism was wrong as well as inconsistent. cancel() leaves a CAST for one tick's
146
+ // cost, so a casting wizard's real delay is 1, not the couple of hundred ticks remaining.
147
+ // A GCD is different: nothing can be started during it and it cannot be cancelled, so
148
+ // there the full ticksUntilReady genuinely does apply.
149
+ const delayBeforeShield = canCancelCurrentCast ? CANCEL_COST_TICKS : ticksUntilReady;
150
+
151
+ // One tick of slack, so the window is not a single exact frame.
152
+ const ticksNeededToBlock = delayBeforeShield + RULES.SHIELD_CAST_TIME + 1;
153
+ const canBlockInTime = ticksToImpact >= ticksNeededToBlock;
154
+ const ticksToStartShield = Math.max(0, ticksToImpact - ticksNeededToBlock);
133
155
 
134
156
  return {
135
157
  id: projectile.id,
package/src/types.ts CHANGED
@@ -44,6 +44,7 @@ export interface WizardState
44
44
  lastMissileConfig?: MissileConfig; // last fired missile (for warmup system)
45
45
  warmupMultiplier?: number; // warmup multiplier applied to most recent missile cast (0.80 = full bonus, 1.20 = full penalty)
46
46
  invincible?: boolean; // manual play mode: damage is ignored when true
47
+ advised?: {nonFinite?: boolean; unfinishableCast?: boolean}; // engine advisories already given, so each is reported once per match
47
48
  }
48
49
 
49
50
  /**