fix(sor-cold): remediate number overload contract and wire reactive combat hooks (Issue #194, #204, #214)

- Fix calculateShiverArmorDamage and calculateChillingArmorDamage number overload to treat number as allocated synergy points (synPoints) with 1.13c percentage scaling, supporting explicit options.synMultiplier override
- Guard duration and chill/freeze length calculators against negative synergy inputs with Math.max(0, synPoints)
- Wire reactive combat hooks in combat.ts (tickMonsters/updateMonsters and hitPlayerWithMissile):
  * Frozen Armor: reactively freezes attacking monster upon taking physical melee damage
  * Shiver Armor: proactively chills and damages attacking monster upon melee attack initiation
  * Chilling Armor: reflects retaliatory chillingarmorbolt on incoming ranged missiles with returnFire=false
- Wire proactive and reactive combat events in combat-pipeline.ts (executeSUnitDmg):
  * Dispatches attackedinmelee proactively before hit resolution
  * Dispatches damagedinmelee post-damage on melee hits
  * Dispatches hitbymissile post-damage on missile hits
  * Exposes attackerStatesApplied, attackerColdDamage256, reflectedPhys256, and spawnedMissiles on SUnitDmgOutcome
- Add comprehensive remediation test suite tests/skills/sor/cold-armors-remediation.test.ts
This commit is contained in:
troytt 2026-09-24 01:06:25 +00:00
parent e7d105dce0
commit 2d6bbb93a2
6 changed files with 2125 additions and 22 deletions

View File

@ -30,6 +30,13 @@ import {
PACK_RADIUS_PX,
} from './monster-rooms.ts'
import type { MapRoom } from './monster-rooms.ts'
import {
calculateFrozenArmorFreezeLength,
calculateShiverArmorDamage,
calculateShiverArmorChillLength,
calculateChillingArmorDamage,
calculateChillingArmorChillLength,
} from './engine/missile-engine.ts'
export {
allocatePacksToRooms,
@ -875,7 +882,7 @@ function tickPlayer(
* @param terrain - collision predicate.
* @param targets - the players that were alive when the tick began.
*/
function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: CombatTerrain, targets: readonly CombatPlayer[]): void {
export function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: CombatTerrain, targets: readonly CombatPlayer[]): void {
for (const monster of world.monsters) {
if (monster.sleeping) continue
if (monster.state === 'dead') {
@ -923,6 +930,40 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
if (monster.cooldown === 0) {
monster.cooldown = monster.stats.cooldownTicks
let dmg = monster.stats.damage
const isRanged = monster.stats.reach > 40
// Cold Armor: Shiver Armor (Skill 50) proactive melee defense
if (!isRanged && player.coldArmorType === 'shiverarmor') {
const slvl = player.coldArmorSlvl ?? 1
const dmgRange = calculateShiverArmorDamage(slvl)
const coldDmg = Math.round((dmgRange.min + dmgRange.max) / 2)
damageMonster(world, monster.index, coldDmg, player)
const chillFrames = calculateShiverArmorChillLength(slvl)
monster.chillTicks = Math.max(monster.chillTicks ?? 0, chillFrames)
}
// Cold Armor: Chilling Armor (Skill 60) ranged retaliatory reflection
if (isRanged && player.coldArmorType === 'chillingarmor') {
const slvl = player.coldArmorSlvl ?? 1
const dmgRange = calculateChillingArmorDamage(slvl)
const boltDmg = Math.round((dmgRange.min + dmgRange.max) / 2)
world.events.push({
kind: 'missileHit',
x: player.x,
y: player.y,
missileType: 'chillingarmorbolt',
skillId: '60',
amount: boltDmg,
})
damageMonster(world, monster.index, boltDmg, player)
const chillFrames = calculateChillingArmorChillLength(slvl)
monster.chillTicks = Math.max(monster.chillTicks ?? 0, chillFrames)
}
if (monster.hp <= 0) {
continue
}
if (player.energyshieldTicks && player.energyshieldTicks > 0 && player.mana > 0) {
const absorbPct = player.energyshieldPct ?? 20
const absorbed = Math.floor((dmg * absorbPct) / 100)
@ -940,6 +981,14 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
}
player.hp -= dmg
world.events.push({ kind: 'playerHit', x: player.x, y: player.y, amount: dmg })
// Cold Armor: Frozen Armor (Skill 40) reactive melee freeze on physical damage
if (!isRanged && player.coldArmorType === 'frozenarmor' && dmg > 0) {
const slvl = player.coldArmorSlvl ?? 1
const freezeFrames = calculateFrozenArmorFreezeLength(slvl)
monster.frozenTicks = Math.max(monster.frozenTicks ?? 0, freezeFrames)
}
if (player.hp <= 0) {
player.hp = 0
player.alive = false
@ -957,3 +1006,93 @@ function tickMonsters(world: CombatWorld, options: CombatOptions, terrain: Comba
monster.y = movedMonster.y
}
}
export const updateMonsters = tickMonsters
export interface PlayerMissileHitOptions {
readonly damage?: number
readonly missileType?: string
readonly attackerIndex?: number
readonly attacker?: Monster | CombatPlayer
readonly returnFire?: boolean
readonly sourceX?: number
readonly sourceY?: number
}
/**
* Handle a ranged missile hit on the player.
* When player has Chilling Armor active and returnFire !== false,
* reflects a retaliatory ice bolt missile ('chillingarmorbolt') toward the attacker with returnFire: false.
*/
export function hitPlayerWithMissile(
world: CombatWorld,
options: PlayerMissileHitOptions,
player: CombatPlayer = world.player,
): { reflected: boolean; retaliatoryMissile?: string; damageDealt: number } {
let dmg = options.damage ?? 10
const returnFire = options.returnFire !== false
let reflected = false
let retaliatoryMissile: string | undefined
if (player.coldArmorType === 'chillingarmor' && returnFire) {
reflected = true
retaliatoryMissile = 'chillingarmorbolt'
const slvl = player.coldArmorSlvl ?? 1
const dmgRange = calculateChillingArmorDamage(slvl)
const boltDmg = Math.round((dmgRange.min + dmgRange.max) / 2)
const chillFrames = calculateChillingArmorChillLength(slvl)
world.events.push({
kind: 'missileHit',
x: player.x,
y: player.y,
missileType: 'chillingarmorbolt',
skillId: '60',
amount: boltDmg,
})
if (options.attackerIndex !== undefined) {
damageMonster(world, options.attackerIndex, boltDmg, player)
const m = world.monsters[options.attackerIndex]
if (m) {
m.chillTicks = Math.max(m.chillTicks ?? 0, chillFrames)
}
} else if (options.attacker && 'stats' in options.attacker) {
const m = options.attacker as Monster
damageMonster(world, m.index, boltDmg, player)
m.chillTicks = Math.max(m.chillTicks ?? 0, chillFrames)
}
}
// Energy shield absorption
if (player.energyshieldTicks && player.energyshieldTicks > 0 && player.mana > 0) {
const absorbPct = player.energyshieldPct ?? 20
const absorbed = Math.floor((dmg * absorbPct) / 100)
const ratio = 2.0
const manaNeeded = Math.ceil(absorbed * ratio)
if (player.mana >= manaNeeded) {
player.mana -= manaNeeded
dmg -= absorbed
} else {
const actualAbsorbed = Math.floor(player.mana / ratio)
player.mana = 0
dmg -= actualAbsorbed
player.energyshieldTicks = 0
}
}
player.hp -= dmg
world.events.push({ kind: 'playerHit', x: player.x, y: player.y, amount: dmg })
if (player.hp <= 0) {
player.hp = 0
player.alive = false
player.respawnIn = 100
world.events.push({ kind: 'playerDeath', x: player.x, y: player.y, text: 'you died' })
}
return {
reflected,
...(retaliatoryMissile !== undefined ? { retaliatoryMissile } : {}),
damageDealt: dmg,
}
}

View File

@ -300,6 +300,7 @@ export interface SUnitDmgPacket {
readonly roll100?: number
readonly isPoison?: boolean
readonly isOpenWounds?: boolean
readonly returnFire?: boolean
}
export interface SUnitDmgOutcome {
@ -314,6 +315,10 @@ export interface SUnitDmgOutcome {
readonly targetHpBefore256: number
readonly targetHpAfter256: number
readonly targetKilled: boolean
readonly attackerStatesApplied?: readonly string[]
readonly attackerColdDamage256?: number
readonly reflectedPhys256?: number
readonly spawnedMissiles?: readonly string[]
}
/**
@ -328,6 +333,29 @@ export function executeSUnitDmg(
const attackKind = packet.attackKind ?? (packet.srcDam && packet.srcDam > 0 ? 'melee' : 'spell')
const roll = packet.roll100 ?? 25
const attackerStatesApplied: string[] = []
let attackerColdDamage256 = 0
let reflectedPhys256 = 0
const spawnedMissiles: string[] = []
// If melee attack, trigger 'attackedinmelee' proactively on attack initiation
if (attackKind === 'melee') {
const initialPhys = packet.flatPhysMax256 ?? ((attacker.weaponMaxPhys ?? 45) * FIXED_ONE)
const reactiveAttack = defender.stateBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: initialPhys,
attackerStateBus: attacker.stateBus,
})
if (reactiveAttack.attackerStatesApplied.length > 0) {
attackerStatesApplied.push(...reactiveAttack.attackerStatesApplied)
}
if (reactiveAttack.attackerColdDamage256 > 0) {
attackerColdDamage256 += reactiveAttack.attackerColdDamage256
const attackerHp = attacker.statList.getHp256()
attacker.statList.setHp256(Math.max(0, attackerHp - reactiveAttack.attackerColdDamage256))
}
}
// 1. RollToHit check for weapon attacks (unless autoHit, e.g. Smite 97 / Guided Arrow 22 / Spells)
const isAutoHit = packet.autoHit ?? (packet.skillId === 97 || packet.skillId === 22 || attackKind === 'spell')
if (!isAutoHit) {
@ -350,6 +378,8 @@ export function executeSUnitDmg(
targetHpBefore256: hpBefore256,
targetHpAfter256: hpBefore256,
targetKilled: false,
...(attackerStatesApplied.length > 0 ? { attackerStatesApplied } : {}),
...(attackerColdDamage256 > 0 ? { attackerColdDamage256 } : {}),
}
}
}
@ -374,6 +404,8 @@ export function executeSUnitDmg(
targetHpBefore256: hpBefore256,
targetHpAfter256: hpBefore256,
targetKilled: false,
...(attackerStatesApplied.length > 0 ? { attackerStatesApplied } : {}),
...(attackerColdDamage256 > 0 ? { attackerColdDamage256 } : {}),
}
}
@ -538,6 +570,45 @@ export function executeSUnitDmg(
const hpAfter256 = Math.max(0, hpBefore256 - totalDamage256)
defender.statList.setHp256(hpAfter256)
// 9. Reactive events post-damage (damagedinmelee for melee, hitbymissile for missile)
if (attackKind === 'melee') {
const reactiveDamage = defender.stateBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: finalPhys256,
incomingElem256: finalElem256,
elemType,
attackerStateBus: attacker.stateBus,
})
if (reactiveDamage.attackerStatesApplied.length > 0) {
attackerStatesApplied.push(...reactiveDamage.attackerStatesApplied)
}
if (reactiveDamage.attackerColdDamage256 > 0) {
attackerColdDamage256 += reactiveDamage.attackerColdDamage256
const curHp = attacker.statList.getHp256()
attacker.statList.setHp256(Math.max(0, curHp - reactiveDamage.attackerColdDamage256))
}
if (reactiveDamage.reflectedPhys256 > 0) {
reflectedPhys256 += reactiveDamage.reflectedPhys256
const curHp = attacker.statList.getHp256()
attacker.statList.setHp256(Math.max(0, curHp - reactiveDamage.reflectedPhys256))
}
} else if (attackKind === 'missile') {
const reactiveMissile = defender.stateBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: finalPhys256,
incomingElem256: finalElem256,
elemType,
...(packet.returnFire !== undefined ? { returnFire: packet.returnFire } : {}),
attackerStateBus: attacker.stateBus,
})
if (reactiveMissile.spawnedMissiles.length > 0) {
spawnedMissiles.push(...reactiveMissile.spawnedMissiles)
}
if (reactiveMissile.attackerStatesApplied.length > 0) {
attackerStatesApplied.push(...reactiveMissile.attackerStatesApplied)
}
}
return {
hit: true,
avoidedReason: 'none',
@ -550,5 +621,9 @@ export function executeSUnitDmg(
targetHpBefore256: hpBefore256,
targetHpAfter256: hpAfter256,
targetKilled: hpAfter256 === 0 && hpBefore256 > 0,
...(attackerStatesApplied.length > 0 ? { attackerStatesApplied } : {}),
...(attackerColdDamage256 > 0 ? { attackerColdDamage256 } : {}),
...(reflectedPhys256 > 0 ? { reflectedPhys256 } : {}),
...(spawnedMissiles.length > 0 ? { spawnedMissiles } : {}),
}
}

View File

@ -1792,10 +1792,12 @@ export function calculateFrozenArmorDuration(
options?: { base1200?: boolean },
): number {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.shiverArmor ?? 0) + (synergies?.chillingArmor ?? 0)
: (synergies?.shiverArmor ?? 0) + (synergies?.chillingArmor ?? 0),
)
if (options?.base1200) {
return Math.floor((1200 + (lvl - 1) * 120) * (1 + (synPoints * 10) / 100))
}
@ -1813,10 +1815,12 @@ export function calculateFrozenArmorFreezeLength(
synergies?: { shiverArmor?: number; chillingArmor?: number } | number,
): number {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.shiverArmor ?? 0) + (synergies?.chillingArmor ?? 0)
: (synergies?.shiverArmor ?? 0) + (synergies?.chillingArmor ?? 0),
)
const baseFrames = 30 + (lvl - 1) * 3
const synFactor = 100 + synPoints * 5
return Math.floor((baseFrames * synFactor) / 100)
@ -1844,10 +1848,12 @@ export function calculateShiverArmorDuration(
options?: { base1200?: boolean },
): number {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.frozenArmor ?? 0) + (synergies?.chillingArmor ?? 0)
: (synergies?.frozenArmor ?? 0) + (synergies?.chillingArmor ?? 0),
)
if (options?.base1200) {
return Math.floor((1200 + (lvl - 1) * 120) * (1 + (synPoints * 10) / 100))
}
@ -1863,7 +1869,7 @@ export function calculateShiverArmorDuration(
export function calculateShiverArmorDamage(
slvl: number,
synergies?: { frozenArmor?: number; chillingArmor?: number; synPctPerLvl?: number } | number,
options?: { synPctPerLvl?: number },
options?: { synPctPerLvl?: number; synMultiplier?: number },
): { min: number; max: number } {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
let rawMin = 12
@ -1898,10 +1904,14 @@ export function calculateShiverArmorDamage(
let max = rawMax / 2
let synMultiplier = 1.0
if (typeof synergies === 'number') {
synMultiplier = synergies > 0 && Number.isFinite(synergies) ? synergies : 1.0
if (typeof options?.synMultiplier === 'number' && Number.isFinite(options.synMultiplier)) {
synMultiplier = Math.max(0, options.synMultiplier)
} else if (typeof synergies === 'number') {
const synPoints = Number.isFinite(synergies) ? Math.max(0, synergies) : 0
const pctPerLvl = options?.synPctPerLvl ?? 9
synMultiplier = 1.0 + (synPoints * pctPerLvl) / 100
} else if (typeof synergies === 'object' && synergies !== null) {
const synPoints = (synergies.frozenArmor ?? 0) + (synergies.chillingArmor ?? 0)
const synPoints = Math.max(0, (synergies.frozenArmor ?? 0) + (synergies.chillingArmor ?? 0))
const pctPerLvl = options?.synPctPerLvl ?? synergies.synPctPerLvl ?? 9
synMultiplier = 1.0 + (synPoints * pctPerLvl) / 100
}
@ -1931,10 +1941,12 @@ export function calculateShiverArmorChillLength(
const b3 = lvl - 16
frames += b3 * 50
}
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.frozenArmor ?? 0) + (synergies?.chillingArmor ?? 0)
: (synergies?.frozenArmor ?? 0) + (synergies?.chillingArmor ?? 0),
)
if (synPoints > 0) {
frames = Math.floor((frames * (100 + synPoints * 7)) / 100)
}
@ -1963,10 +1975,12 @@ export function calculateChillingArmorDuration(
options?: { base1200?: boolean },
): number {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.frozenArmor ?? 0) + (synergies?.shiverArmor ?? 0)
: (synergies?.frozenArmor ?? 0) + (synergies?.shiverArmor ?? 0),
)
if (options?.base1200) {
return Math.floor((1200 + (lvl - 1) * 120) * (1 + (synPoints * 10) / 100))
}
@ -1982,7 +1996,7 @@ export function calculateChillingArmorDuration(
export function calculateChillingArmorDamage(
slvl: number,
synergies?: { frozenArmor?: number; shiverArmor?: number; synPctPerLvl?: number } | number,
options?: { synPctPerLvl?: number },
options?: { synPctPerLvl?: number; synMultiplier?: number },
): { min: number; max: number } {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
let rawMin = 8
@ -2017,10 +2031,14 @@ export function calculateChillingArmorDamage(
let max = rawMax / 2
let synMultiplier = 1.0
if (typeof synergies === 'number') {
synMultiplier = synergies > 0 && Number.isFinite(synergies) ? synergies : 1.0
if (typeof options?.synMultiplier === 'number' && Number.isFinite(options.synMultiplier)) {
synMultiplier = Math.max(0, options.synMultiplier)
} else if (typeof synergies === 'number') {
const synPoints = Number.isFinite(synergies) ? Math.max(0, synergies) : 0
const pctPerLvl = options?.synPctPerLvl ?? 7
synMultiplier = 1.0 + (synPoints * pctPerLvl) / 100
} else if (typeof synergies === 'object' && synergies !== null) {
const synPoints = (synergies.frozenArmor ?? 0) + (synergies.shiverArmor ?? 0)
const synPoints = Math.max(0, (synergies.frozenArmor ?? 0) + (synergies.shiverArmor ?? 0))
const pctPerLvl = options?.synPctPerLvl ?? synergies.synPctPerLvl ?? 7
synMultiplier = 1.0 + (synPoints * pctPerLvl) / 100
}
@ -2050,10 +2068,12 @@ export function calculateChillingArmorChillLength(
const b3 = lvl - 16
frames += b3 * 25
}
const synPoints =
const synPoints = Math.max(
0,
typeof synergies === 'number'
? synergies
: (synergies?.frozenArmor ?? 0) + (synergies?.shiverArmor ?? 0)
: (synergies?.frozenArmor ?? 0) + (synergies?.shiverArmor ?? 0),
)
if (synPoints > 0) {
frames = Math.floor((frames * (100 + synPoints * 10)) / 100)
}
@ -2071,7 +2091,8 @@ export function calculateColdArmorDuration(
synPoints = 0,
): number {
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
return Math.floor((baseFrames + (lvl - 1) * perLvlFrames) * (1 + (synPoints * synBonusPct) / 100))
const safeSyn = Math.max(0, synPoints)
return Math.floor((baseFrames + (lvl - 1) * perLvlFrames) * (1 + (safeSyn * synBonusPct) / 100))
}

View File

@ -0,0 +1,849 @@
/**
* Adversarial Stress & Correctness Verification Suite — Milestone 1: Three Sorceress Cold Armors
* Skills Under Test:
* - Frozen Armor (Skill #040, State 10, Group 1)
* - Shiver Armor (Skill #050, State 88, Group 1)
* - Chilling Armor (Skill #060, State 20, Group 1)
*
* Target Commit: e7d105d
* Author: Challenger 1 (teamwork_preview_challenger)
* Ground Truth Reference: Diablo II v1.13c (D2Game.dll, D2Common.dll, Skills.txt, Missiles.txt, Overlay.txt, States.txt)
*
* Verification Dimensions:
* 1. Exhaustive Mathematical Boundary Sweeps (slvl 1..99 x synergies 0..20 / 0..40):
* - Monotonicity of defense bonus, duration, cold damage, and freeze/chill lengths.
* - Zero NaN, zero undefined, and positive non-zero outputs for all valid inputs.
* - Exact formula validation against 1.13c ground truth analytical models.
* 2. Degenerate, Extreme & Adversarial Inputs:
* - Non-finite, negative, zero, and float slvl handling.
* - Missing, malformed, negative, and mixed synergy structures.
* 3. State Group 1 Mutual Exclusion Stress Testing:
* - Rapid successive casting bursts & 100-step randomized permutations.
* - Invariant: strictly 1 active Group 1 state at all times.
* - Level-override rules (different armor overwrites regardless of slvl; same armor rejects lower slvl).
* - Clean eviction of StatList defense modifiers upon armor transition.
* 4. Reactive Trigger Adversarial Edge Cases:
* - Frozen Armor: damagedinmelee with physical damage = 0 must NOT freeze.
* - Chilling Armor: hitbymissile with returnFire = false must NOT reflect.
* - Shiver Armor: attackedinmelee triggers proactively before attack connects regardless of damage.
* - Event cross-trigger isolation (no cross-firing between melee and missile events).
* 5. Full Invariant & System Integrity Checks:
* - BATCH1_SKILLS length strictly 38.
* - Mana costs: flat 7 (40), flat 11 (50), flat 17 (60).
* - Cooldown ticks: 0 for all three armors.
* - calculateSkillDamage routing parity.
*/
import { describe, expect, it } from 'vitest'
import {
calculateFrozenArmorDefense,
calculateFrozenArmorDuration,
calculateFrozenArmorFreezeLength,
calculateShiverArmorDefense,
calculateShiverArmorDuration,
calculateShiverArmorDamage,
calculateShiverArmorChillLength,
calculateChillingArmorDefense,
calculateChillingArmorDuration,
calculateChillingArmorDamage,
calculateChillingArmorChillLength,
calculateColdArmorDuration,
calculateSkillDamage,
getSkillManaCost,
getSkillCooldownTicks,
getSkillCastOverlay,
BATCH1_SKILLS,
} from '../src/game/skills.ts'
import { getSharedDataRegistry } from '../src/game/engine/data-registry.ts'
import { UnitStatList, FIXED_ONE } from '../src/game/engine/stat-list.ts'
import { StateBus } from '../src/game/engine/state-bus.ts'
import { OVERLAY_METAS } from '../src/render/overlays-meta.ts'
import { MISSILE_METAS } from '../src/render/missiles-meta.ts'
describe('Challenger 1 Stress Suite — Three Sorceress Cold Armors (M1: #040, #050, #060)', () => {
// =========================================================================
// Dimension 1: Exhaustive Mathematical Boundary Sweeps (slvl 1..99 x syn 0..40)
// =========================================================================
describe('Dimension 1: Exhaustive Mathematical Boundary Sweeps & Monotonicity', () => {
describe('Frozen Armor (Skill 40)', () => {
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
let prevDef = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const def = calculateFrozenArmorDefense(slvl)
expect(Number.isFinite(def)).toBe(true)
expect(Number.isNaN(def)).toBe(false)
expect(def).toBeGreaterThan(0)
// 1.13c formula: 30 + (slvl - 1) * 5
expect(def).toBe(30 + (slvl - 1) * 5)
expect(def).toBeGreaterThan(prevDef)
prevDef = def
}
expect(calculateFrozenArmorDefense(1)).toBe(30)
expect(calculateFrozenArmorDefense(10)).toBe(75)
expect(calculateFrozenArmorDefense(20)).toBe(125)
expect(calculateFrozenArmorDefense(99)).toBe(520)
})
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
// Sweep 99 * 41 = 4059 test cases
for (let syn = 0; syn <= 40; syn++) {
let prevDur = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const dur = calculateFrozenArmorDuration(slvl, syn)
expect(Number.isFinite(dur)).toBe(true)
expect(Number.isNaN(dur)).toBe(false)
expect(dur).toBeGreaterThan(0)
// 1.13c formula: 3000 + (slvl - 1) * 300 + syn * 250
const expected = 3000 + (slvl - 1) * 300 + syn * 250
expect(dur).toBe(expected)
expect(dur).toBeGreaterThan(prevDur)
prevDur = dur
}
}
// Synergy monotonicity at fixed slvl
for (let slvl = 1; slvl <= 20; slvl++) {
let prevDur = 0
for (let syn = 0; syn <= 40; syn++) {
const dur = calculateFrozenArmorDuration(slvl, syn)
expect(dur).toBeGreaterThan(prevDur)
prevDur = dur
}
}
// Spot-checks
expect(calculateFrozenArmorDuration(1, 0)).toBe(3000) // 120s
expect(calculateFrozenArmorDuration(10, 0)).toBe(5700) // 228s
expect(calculateFrozenArmorDuration(20, 0)).toBe(8700) // 348s
expect(calculateFrozenArmorDuration(20, 40)).toBe(18700) // 748s
expect(calculateFrozenArmorDuration(99, 40)).toBe(42400) // 1696s
})
it('evaluates Freeze Length across slvl 1..99 and syn 0..40 with monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn++) {
let prevFreeze = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const frz = calculateFrozenArmorFreezeLength(slvl, syn)
expect(Number.isFinite(frz)).toBe(true)
expect(Number.isNaN(frz)).toBe(false)
expect(frz).toBeGreaterThan(0)
// 1.13c formula: Math.floor((30 + (slvl - 1) * 3) * (100 + syn * 5) / 100)
const expected = Math.floor((30 + (slvl - 1) * 3) * (100 + syn * 5) / 100)
expect(frz).toBe(expected)
expect(frz).toBeGreaterThan(prevFreeze)
prevFreeze = frz
}
}
// Synergy monotonicity at fixed slvl
for (let slvl = 1; slvl <= 20; slvl++) {
let prevFreeze = 0
for (let syn = 0; syn <= 40; syn++) {
const frz = calculateFrozenArmorFreezeLength(slvl, syn)
expect(frz).toBeGreaterThanOrEqual(prevFreeze)
prevFreeze = frz
}
}
// Spot-checks
expect(calculateFrozenArmorFreezeLength(1, 0)).toBe(30) // 1.20s
expect(calculateFrozenArmorFreezeLength(10, 0)).toBe(57) // 2.28s
expect(calculateFrozenArmorFreezeLength(20, 0)).toBe(87) // 3.48s
expect(calculateFrozenArmorFreezeLength(20, 40)).toBe(261) // 10.44s
expect(calculateFrozenArmorFreezeLength(99, 40)).toBe(972) // 38.88s
})
it('asserts Frozen Armor deals strictly 0 damage across slvl 1..99', () => {
for (let slvl = 1; slvl <= 99; slvl++) {
const dmg = calculateSkillDamage(40, slvl)
expect(dmg).toEqual({ min: 0, max: 0 })
}
})
})
describe('Shiver Armor (Skill 50)', () => {
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
let prevDef = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const def = calculateShiverArmorDefense(slvl)
expect(Number.isFinite(def)).toBe(true)
expect(Number.isNaN(def)).toBe(false)
expect(def).toBeGreaterThan(0)
// 1.13c formula: 45 + (slvl - 1) * 6
expect(def).toBe(45 + (slvl - 1) * 6)
expect(def).toBeGreaterThan(prevDef)
prevDef = def
}
expect(calculateShiverArmorDefense(1)).toBe(45)
expect(calculateShiverArmorDefense(10)).toBe(99)
expect(calculateShiverArmorDefense(20)).toBe(159)
expect(calculateShiverArmorDefense(99)).toBe(633)
})
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn++) {
let prevDur = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const dur = calculateShiverArmorDuration(slvl, syn)
expect(Number.isFinite(dur)).toBe(true)
expect(Number.isNaN(dur)).toBe(false)
expect(dur).toBeGreaterThan(0)
const expected = 3000 + (slvl - 1) * 300 + syn * 250
expect(dur).toBe(expected)
expect(dur).toBeGreaterThan(prevDur)
prevDur = dur
}
}
expect(calculateShiverArmorDuration(1, 0)).toBe(3000)
expect(calculateShiverArmorDuration(20, 0)).toBe(8700)
expect(calculateShiverArmorDuration(20, 40)).toBe(18700)
})
it('evaluates Cold Damage across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn += 5) {
let prevMin = 0
let prevMax = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const dmg = calculateShiverArmorDamage(slvl, { frozenArmor: syn, chillingArmor: 0 })
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isNaN(dmg.min)).toBe(false)
expect(Number.isNaN(dmg.max)).toBe(false)
expect(dmg.min).toBeGreaterThan(0)
expect(dmg.max).toBeGreaterThanOrEqual(dmg.min)
expect(dmg.min).toBeGreaterThan(prevMin)
expect(dmg.max).toBeGreaterThan(prevMax)
prevMin = dmg.min
prevMax = dmg.max
}
}
// Synergy monotonicity at fixed slvl
for (let slvl = 1; slvl <= 20; slvl += 5) {
let prevMin = 0
for (let syn = 0; syn <= 40; syn++) {
const dmg = calculateShiverArmorDamage(slvl, syn)
expect(dmg.min).toBeGreaterThanOrEqual(prevMin)
prevMin = dmg.min
}
}
// Authentic 1.13c spot-checks (HitShift = 7 -> divide by 2)
expect(calculateShiverArmorDamage(1, 0)).toEqual({ min: 6, max: 8 })
expect(calculateShiverArmorDamage(10, 0)).toEqual({ min: 26, max: 32.5 })
expect(calculateShiverArmorDamage(20, 0)).toEqual({ min: 60, max: 71.5 })
// +40 syn points via object (+360% -> multiplier 4.6x): 60 * 4.6 = 276.0, 71.5 * 4.6 = 328.9
expect(calculateShiverArmorDamage(20, { frozenArmor: 20, chillingArmor: 20 })).toEqual({ min: 276.0, max: 328.9 })
// +40 syn points via numeric synergy points (40 pts -> 1 + 40*0.09 = 4.6x)
expect(calculateShiverArmorDamage(20, 40)).toEqual({ min: 276.0, max: 328.9 })
// +40 syn points via options.synMultiplier (4.6x)
expect(calculateShiverArmorDamage(20, undefined, { synMultiplier: 4.6 })).toEqual({ min: 276.0, max: 328.9 })
})
it('evaluates Chill Length across slvl 1..99 and syn 0..40 with piecewise monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn += 10) {
let prevChill = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const chill = calculateShiverArmorChillLength(slvl, syn)
expect(Number.isFinite(chill)).toBe(true)
expect(Number.isNaN(chill)).toBe(false)
expect(chill).toBeGreaterThan(0)
expect(chill).toBeGreaterThanOrEqual(prevChill)
prevChill = chill
}
}
// Level band step checks without synergy:
// slvl 1-8: flat 100 frames
for (let lvl = 1; lvl <= 8; lvl++) {
expect(calculateShiverArmorChillLength(lvl, 0)).toBe(100)
}
// slvl 9-16: +25 frames / level
expect(calculateShiverArmorChillLength(9, 0)).toBe(125)
expect(calculateShiverArmorChillLength(10, 0)).toBe(150)
expect(calculateShiverArmorChillLength(16, 0)).toBe(300)
// slvl 17+: +50 frames / level
expect(calculateShiverArmorChillLength(17, 0)).toBe(350)
expect(calculateShiverArmorChillLength(20, 0)).toBe(500)
expect(calculateShiverArmorChillLength(99, 0)).toBe(4450)
// With +40 syn (+280% factor = 3.8x): 500 * 3.8 = 1900
expect(calculateShiverArmorChillLength(20, 40)).toBe(1900)
expect(calculateShiverArmorChillLength(20, { frozenArmor: 20, chillingArmor: 20 })).toBe(1900)
})
})
describe('Chilling Armor (Skill 60)', () => {
it('evaluates % Enhanced Defense across slvl 1..99 with strict monotonicity and zero NaN', () => {
let prevDef = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const def = calculateChillingArmorDefense(slvl)
expect(Number.isFinite(def)).toBe(true)
expect(Number.isNaN(def)).toBe(false)
expect(def).toBeGreaterThan(0)
expect(def).toBe(45 + (slvl - 1) * 5)
expect(def).toBeGreaterThan(prevDef)
prevDef = def
}
expect(calculateChillingArmorDefense(1)).toBe(45)
expect(calculateChillingArmorDefense(10)).toBe(90)
expect(calculateChillingArmorDefense(20)).toBe(140)
expect(calculateChillingArmorDefense(99)).toBe(535)
})
it('evaluates Duration across slvl 1..99 and syn 0..40 with strict monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn++) {
let prevDur = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const dur = calculateChillingArmorDuration(slvl, syn)
expect(Number.isFinite(dur)).toBe(true)
expect(Number.isNaN(dur)).toBe(false)
expect(dur).toBeGreaterThan(0)
const expected = 3600 + (slvl - 1) * 150 + syn * 250
expect(dur).toBe(expected)
expect(dur).toBeGreaterThan(prevDur)
prevDur = dur
}
}
expect(calculateChillingArmorDuration(1, 0)).toBe(3600) // 144s
expect(calculateChillingArmorDuration(10, 0)).toBe(4950) // 198s
expect(calculateChillingArmorDuration(20, 0)).toBe(6450) // 258s
expect(calculateChillingArmorDuration(20, 40)).toBe(16450) // 658s
expect(calculateChillingArmorDuration(99, 40)).toBe(28300) // 1132s
})
it('evaluates Retaliatory Bolt Cold Damage across slvl 1..99 and syn 0..40 with monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn += 5) {
let prevMin = 0
let prevMax = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const dmg = calculateChillingArmorDamage(slvl, { frozenArmor: syn, shiverArmor: 0 })
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
expect(Number.isNaN(dmg.min)).toBe(false)
expect(Number.isNaN(dmg.max)).toBe(false)
expect(dmg.min).toBeGreaterThan(0)
expect(dmg.max).toBeGreaterThanOrEqual(dmg.min)
expect(dmg.min).toBeGreaterThan(prevMin)
expect(dmg.max).toBeGreaterThan(prevMax)
prevMin = dmg.min
prevMax = dmg.max
}
}
// Spot-checks
expect(calculateChillingArmorDamage(1, 0)).toEqual({ min: 4, max: 6 })
expect(calculateChillingArmorDamage(10, 0)).toEqual({ min: 15, max: 21.5 })
expect(calculateChillingArmorDamage(20, 0)).toEqual({ min: 39, max: 50.5 })
// +40 syn points via object (+280% -> multiplier 3.8x): 39 * 3.8 = 148.2, 50.5 * 3.8 = 191.9
expect(calculateChillingArmorDamage(20, { frozenArmor: 20, shiverArmor: 20 })).toEqual({ min: 148.2, max: 191.9 })
// +40 syn points via numeric synergy points (40 pts -> 1 + 40*0.07 = 3.8x)
expect(calculateChillingArmorDamage(20, 40)).toEqual({ min: 148.2, max: 191.9 })
// +40 syn points via options.synMultiplier (3.8x)
expect(calculateChillingArmorDamage(20, undefined, { synMultiplier: 3.8 })).toEqual({ min: 148.2, max: 191.9 })
})
it('evaluates Chill Length across slvl 1..99 and syn 0..40 with piecewise monotonicity and zero NaN', () => {
for (let syn = 0; syn <= 40; syn += 10) {
let prevChill = 0
for (let slvl = 1; slvl <= 99; slvl++) {
const chill = calculateChillingArmorChillLength(slvl, syn)
expect(Number.isFinite(chill)).toBe(true)
expect(Number.isNaN(chill)).toBe(false)
expect(chill).toBeGreaterThan(0)
expect(chill).toBeGreaterThanOrEqual(prevChill)
prevChill = chill
}
}
// Band step checks without synergy:
for (let lvl = 1; lvl <= 8; lvl++) {
expect(calculateChillingArmorChillLength(lvl, 0)).toBe(100)
}
expect(calculateChillingArmorChillLength(9, 0)).toBe(125)
expect(calculateChillingArmorChillLength(10, 0)).toBe(150)
expect(calculateChillingArmorChillLength(16, 0)).toBe(300)
// slvl 17+: +25 frames / level
expect(calculateChillingArmorChillLength(17, 0)).toBe(325)
expect(calculateChillingArmorChillLength(20, 0)).toBe(400)
expect(calculateChillingArmorChillLength(99, 0)).toBe(2375)
// With +40 syn (+400% factor = 5.0x): 400 * 5.0 = 2000
expect(calculateChillingArmorChillLength(20, 40)).toBe(2000)
expect(calculateChillingArmorChillLength(20, { frozenArmor: 20, shiverArmor: 20 })).toBe(2000)
})
})
})
// =========================================================================
// Dimension 2: Degenerate, Extreme & Adversarial Inputs
// =========================================================================
describe('Dimension 2: Degenerate, Extreme & Adversarial Inputs', () => {
it('clamps negative, zero, and non-integer slvl gracefully without throwing or returning NaN', () => {
const dirtyLvls = [0, -1, -99, 0.5, 12.8, NaN, Infinity, -Infinity]
for (const slvl of dirtyLvls) {
// Frozen Armor
const fDef = calculateFrozenArmorDefense(slvl)
const fDur = calculateFrozenArmorDuration(slvl)
const fFrz = calculateFrozenArmorFreezeLength(slvl)
expect(Number.isFinite(fDef)).toBe(true)
expect(Number.isFinite(fDur)).toBe(true)
expect(Number.isFinite(fFrz)).toBe(true)
// Shiver Armor
const sDef = calculateShiverArmorDefense(slvl)
const sDur = calculateShiverArmorDuration(slvl)
const sDmg = calculateShiverArmorDamage(slvl)
const sChl = calculateShiverArmorChillLength(slvl)
expect(Number.isFinite(sDef)).toBe(true)
expect(Number.isFinite(sDur)).toBe(true)
expect(Number.isFinite(sDmg.min)).toBe(true)
expect(Number.isFinite(sDmg.max)).toBe(true)
expect(Number.isFinite(sChl)).toBe(true)
// Chilling Armor
const cDef = calculateChillingArmorDefense(slvl)
const cDur = calculateChillingArmorDuration(slvl)
const cDmg = calculateChillingArmorDamage(slvl)
const cChl = calculateChillingArmorChillLength(slvl)
expect(Number.isFinite(cDef)).toBe(true)
expect(Number.isFinite(cDur)).toBe(true)
expect(Number.isFinite(cDmg.min)).toBe(true)
expect(Number.isFinite(cDmg.max)).toBe(true)
expect(Number.isFinite(cChl)).toBe(true)
}
})
it('handles degenerate synergy input shapes (undefined, empty, invalid objects)', () => {
const benignSynergies = [undefined, {}, { fake: 99 }, null as any]
for (const syn of benignSynergies) {
expect(() => calculateFrozenArmorDuration(20, syn)).not.toThrow()
expect(() => calculateShiverArmorDamage(20, syn)).not.toThrow()
expect(() => calculateChillingArmorDamage(20, syn)).not.toThrow()
expect(calculateFrozenArmorDuration(20, syn)).toBe(8700)
expect(calculateShiverArmorDamage(20, syn)).toEqual({ min: 60, max: 71.5 })
expect(calculateChillingArmorDamage(20, syn)).toEqual({ min: 39, max: 50.5 })
}
// Damage functions safely default to multiplier 1.0 when negative or non-finite numbers are passed
expect(calculateShiverArmorDamage(20, -10)).toEqual({ min: 60, max: 71.5 })
expect(calculateShiverArmorDamage(20, NaN)).toEqual({ min: 60, max: 71.5 })
expect(calculateShiverArmorDamage(20, Infinity)).toEqual({ min: 60, max: 71.5 })
expect(calculateChillingArmorDamage(20, -10)).toEqual({ min: 39, max: 50.5 })
expect(calculateChillingArmorDamage(20, NaN)).toEqual({ min: 39, max: 50.5 })
expect(calculateChillingArmorDamage(20, Infinity)).toEqual({ min: 39, max: 50.5 })
})
})
// =========================================================================
// Dimension 3: State Group 1 Mutual Exclusion Stress Testing
// =========================================================================
describe('Dimension 3: State Group 1 Mutual Exclusion Stress Testing', () => {
it('strictly maintains exactly 1 active Cold Armor during rapid alternating casts', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
const armors: { name: string; id: number }[] = [
{ name: 'frozenarmor', id: 40 },
{ name: 'shiverarmor', id: 50 },
{ name: 'chillingarmor', id: 60 },
]
// 30 cycles of Frozen -> Shiver -> Chilling (90 casts total)
for (let cycle = 0; cycle < 30; cycle++) {
for (const armor of armors) {
const res = bus.applyState({
stateNameOrId: armor.name,
sourceSkillId: armor.id,
slvl: 20,
stats: { skill_armor_percent: 100 + armor.id },
})
expect(res.applied).toBe(true)
// Invariant 1: Exactly 1 state with group === 1
const activeGroup1 = bus.getActiveEntries().filter(e => e.group === 1)
expect(activeGroup1.length).toBe(1)
expect(activeGroup1[0]!.stateName).toBe(armor.name)
// Invariant 2: hasState confirms only current armor is active
for (const other of armors) {
if (other.name === armor.name) {
expect(bus.hasState(other.name)).toBe(true)
} else {
expect(bus.hasState(other.name)).toBe(false)
}
}
// Invariant 3: statList modifiers from previous cold armor are completely removed
const activeMods = stats.getActiveModifiers()
const armorMods = activeMods.filter(m => armors.some(a => a.name === m.stateName))
expect(armorMods.length).toBe(1)
expect(armorMods[0]!.stateName).toBe(armor.name)
expect(armorMods[0]!.stats['skill_armor_percent']).toBe(100 + armor.id)
}
}
})
it('preserves single-state invariant across 100 randomized rapid casting bursts', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
const armors = ['frozenarmor', 'shiverarmor', 'chillingarmor']
let lastCast = ''
for (let i = 0; i < 100; i++) {
const pick = armors[Math.floor(Math.random() * armors.length)]!
bus.applyState({
stateNameOrId: pick,
slvl: 10 + (i % 20),
})
lastCast = pick
const group1Entries = bus.getActiveEntries().filter(e => e.group === 1)
expect(group1Entries.length).toBe(1)
expect(group1Entries[0]!.stateName).toBe(lastCast)
}
})
it('enforces cross-level eviction: lower-slvl different armor evicts higher-slvl armor', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
// Start with Chilling Armor slvl 50
bus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 50 })
expect(bus.hasState('chillingarmor')).toBe(true)
expect(bus.getState('chillingarmor')?.slvl).toBe(50)
// Cast Frozen Armor slvl 1 -> must EVIC Chilling Armor
bus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 1 })
expect(bus.hasState('chillingarmor')).toBe(false)
expect(bus.hasState('frozenarmor')).toBe(true)
expect(bus.getState('frozenarmor')?.slvl).toBe(1)
// Cast Shiver Armor slvl 1 -> must EVICT Frozen Armor
bus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 1 })
expect(bus.hasState('frozenarmor')).toBe(false)
expect(bus.hasState('shiverarmor')).toBe(true)
expect(bus.getState('shiverarmor')?.slvl).toBe(1)
})
it('enforces same-armor level rules: higher slvl overwrites; lower slvl is rejected', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
// Apply Frozen Armor slvl 20 with 3000 frames duration
bus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20, durationFrames: 3000 })
expect(bus.getState('frozenarmor')?.slvl).toBe(20)
// Attempt to cast Frozen Armor slvl 5 while slvl 20 is active -> rejected
const lowerRes = bus.applyState({
stateNameOrId: 'frozenarmor',
sourceSkillId: 40,
slvl: 5,
durationFrames: 3000,
})
expect(lowerRes.applied).toBe(false)
expect(bus.getState('frozenarmor')?.slvl).toBe(20)
// Cast Frozen Armor slvl 30 -> accepted and upgraded
const higherRes = bus.applyState({
stateNameOrId: 'frozenarmor',
sourceSkillId: 40,
slvl: 30,
durationFrames: 6000,
})
expect(higherRes.applied).toBe(true)
expect(bus.getState('frozenarmor')?.slvl).toBe(30)
})
it('expires cold armor cleanly on clock tick and clears group 1 state', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
bus.applyState({
stateNameOrId: 'frozenarmor',
sourceSkillId: 40,
slvl: 1,
durationFrames: 3000,
})
expect(bus.hasState('frozenarmor')).toBe(true)
// Advance frame clock past 3000
const expired = bus.tick(3001)
expect(expired).toContain('frozenarmor')
expect(bus.hasState('frozenarmor')).toBe(false)
expect(bus.getActiveEntries().filter(e => e.group === 1).length).toBe(0)
})
})
// =========================================================================
// Dimension 4: Reactive Trigger Adversarial Edge Cases
// =========================================================================
describe('Dimension 4: Reactive Trigger Adversarial Edge Cases', () => {
describe('Frozen Armor (Skill 40) Edge Cases', () => {
it('freezes attacker when damagedinmelee with physical damage > 0', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 100 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(outcome.attackerStatesApplied).toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(true)
// Frozen armor deals 0 damage to attacker
expect(outcome.attackerColdDamage256).toBe(0)
expect(outcome.reflectedPhys256).toBe(0)
})
it('does NOT freeze attacker if physical damage is 0 (e.g. 100% absorbed/miss/blocked)', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 0,
attackerStateBus: attackerBus,
})
expect(outcome.attackerStatesApplied).not.toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(false)
})
it('does NOT freeze attacker if melee damage is 100% elemental with 0 physical', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 0,
incomingElem256: 500 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(outcome.attackerStatesApplied).not.toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(false)
})
it('does NOT trigger Frozen Armor on hitbymissile', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 200 * FIXED_ONE,
returnFire: true,
attackerStateBus: attackerBus,
})
expect(outcome.attackerStatesApplied).not.toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(false)
})
})
describe('Shiver Armor (Skill 50) Edge Cases', () => {
it('proactively triggers on attackedinmelee BEFORE hit resolution, even if damage is 0', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 20 })
// Attacker begins melee swing (attackedinmelee) with 0 incoming damage
const outcome = defenderBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: 0,
attackerStateBus: attackerBus,
})
expect(outcome.attackerStatesApplied).toContain('cold')
expect(attackerBus.hasState('cold')).toBe(true)
expect(outcome.attackerColdDamage256).toBeGreaterThan(0)
})
it('does NOT trigger Shiver Armor on hitbymissile or damagedbymissile', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 20 })
const out1 = defenderBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 100 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(out1.attackerStatesApplied).not.toContain('cold')
expect(out1.attackerColdDamage256).toBe(0)
const out2 = defenderBus.triggerReactiveEvents({
event: 'damagedbymissile',
incomingPhys256: 100 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(out2.attackerStatesApplied).not.toContain('cold')
expect(out2.attackerColdDamage256).toBe(0)
})
})
describe('Chilling Armor (Skill 60) Edge Cases', () => {
it('reflects chillingarmorbolt when struck by missile with returnFire: true', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 100 * FIXED_ONE,
returnFire: true,
attackerStateBus: attackerBus,
})
expect(outcome.spawnedMissiles).toContain('chillingarmorbolt')
})
it('does NOT reflect chillingarmorbolt if incoming missile has returnFire: false', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 100 * FIXED_ONE,
returnFire: false,
attackerStateBus: attackerBus,
})
expect(outcome.spawnedMissiles).not.toContain('chillingarmorbolt')
})
it('does NOT reflect chillingarmorbolt if returnFire is 0', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
const outcome = defenderBus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 100 * FIXED_ONE,
returnFire: 0,
})
expect(outcome.spawnedMissiles).not.toContain('chillingarmorbolt')
})
it('does NOT trigger Chilling Armor on damagedinmelee or attackedinmelee', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
const out1 = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 100 * FIXED_ONE,
})
expect(out1.spawnedMissiles).not.toContain('chillingarmorbolt')
const out2 = defenderBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: 100 * FIXED_ONE,
})
expect(out2.spawnedMissiles).not.toContain('chillingarmorbolt')
})
})
})
// =========================================================================
// Dimension 5: Full Invariant & System Integrity Checks
// =========================================================================
describe('Dimension 5: Full Invariant & System Integrity Checks', () => {
it('enforces BATCH1_SKILLS length strictly 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
})
it('verifies constant mana costs across slvl 1..99 for all three armors', () => {
for (let slvl = 1; slvl <= 99; slvl++) {
expect(getSkillManaCost(40, slvl)).toBe(7)
expect(getSkillManaCost(50, slvl)).toBe(11)
expect(getSkillManaCost(60, slvl)).toBe(17)
}
})
it('verifies zero cooldown ticks across all three armors', () => {
expect(getSkillCooldownTicks(40)).toBe(0)
expect(getSkillCooldownTicks(50)).toBe(0)
expect(getSkillCooldownTicks(60)).toBe(0)
})
it('verifies calculateSkillDamage routing consistency', () => {
for (let slvl = 1; slvl <= 30; slvl++) {
// Frozen Armor: always 0
expect(calculateSkillDamage(40, slvl)).toEqual({ min: 0, max: 0 })
// Shiver Armor: matches calculateShiverArmorDamage
const expectedShiver = calculateShiverArmorDamage(slvl)
expect(calculateSkillDamage(50, slvl)).toEqual(expectedShiver)
// Chilling Armor: matches calculateChillingArmorDamage
const expectedChilling = calculateChillingArmorDamage(slvl)
expect(calculateSkillDamage(60, slvl)).toEqual(expectedChilling)
}
})
it('verifies overlay and missile registrations', () => {
expect(OVERLAY_METAS['frozenarmor']).toBeDefined()
expect(OVERLAY_METAS['shiverarmor']).toBeDefined()
expect(OVERLAY_METAS['chillingarmor']).toBeDefined()
expect(OVERLAY_METAS['frozenarmor_hit']).toBeDefined()
expect(OVERLAY_METAS['shiverarmor_hit']).toBeDefined()
expect(OVERLAY_METAS['chillingarmor_hit']).toBeDefined()
expect(MISSILE_METAS['chillingarmorbolt']).toBeDefined()
expect(getSkillCastOverlay(40)).toBe('ice_cast_1')
expect(getSkillCastOverlay(50)).toBe('ice_cast_2')
expect(getSkillCastOverlay(60)).toBe('ice_cast_3')
})
})
})

View File

@ -0,0 +1,683 @@
/**
* Adversarial Test Coverage & Boundary Stress Suite — Milestone 1: Three Sorceress Cold Armors
* Skills: #040 Frozen Armor, #050 Shiver Armor, #060 Chilling Armor
*
* Playbook: test_coverage_audit (Omega Teamwork)
* Mode: Whitebox Adversarial Verification
*
* Focus Areas:
* 1. 7-Dimensional Full-Spectrum Coverage Audit:
* - D1: 1.13c Data Table Parity (Skills.txt, Missiles.txt, States.txt, Overlay.txt)
* - D2: Baked Visual Assets & Zero-Runtime MPQ Invariance
* - D3: Kinematics & Physics Simulation
* - D4: Collision, Damage Filtering & Obstacle Handling
* - D5: Synergy & Mastery Calculations (1.13c blvl hard points rule)
* - D6: Spellcasting Lifecycle & Resource Management (Mana, Cooldown, Group 1 Mutual Eviction)
* - D7: Regression & WorldArena Invariants (BATCH1_SKILLS length 38)
*
* 2. Deep Adversarial Probes:
* - Probe A: Infinite Reflection Loop Protection (Chilling Armor vs Chilling Armor, returnFire=false)
* - Probe B: Mid-Duration Armor Switching & Lingering Effect Eviction (Defense % clearing, Reactive handler replacement)
* - Probe C: Fixed-Point 256-Precision Bitrate Cold Damage & HP Arithmetic (HitShift 7 parity, 256th integer exactness)
*/
import { describe, expect, it } from 'vitest'
import fs from 'node:fs'
import path from 'node:path'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList, FIXED_ONE } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
import { WorldArena } from '../../../src/game/engine/world-arena.ts'
import {
calculateFrozenArmorDefense,
calculateFrozenArmorDuration,
calculateFrozenArmorFreezeLength,
calculateShiverArmorDefense,
calculateShiverArmorDuration,
calculateShiverArmorDamage,
calculateShiverArmorChillLength,
calculateChillingArmorDefense,
calculateChillingArmorDuration,
calculateChillingArmorDamage,
calculateChillingArmorChillLength,
calculateColdArmorDuration,
calculateSkillDamage,
getSkillManaCost,
getSkillCooldownTicks,
getSkillCastOverlay,
BATCH1_SKILLS,
} from '../../../src/game/skills.ts'
import {
FROZEN_ARMOR_OVERLAY_META,
FROZEN_ARMOR_HIT_OVERLAY_META,
OVERLAY_METAS,
} from '../../../src/render/overlays-meta.ts'
import {
ICEBOLT_META,
MISSILE_METAS,
} from '../../../src/render/missiles-meta.ts'
describe('Adversarial Test Coverage & Boundary Stress Suite — Milestone 1 Cold Armors (Skills 40, 50, 60)', () => {
const skillsTxtPath = path.resolve(__dirname, '../../../samples/fixtures/data/global/excel/Skills.txt')
const missilesTxtPath = path.resolve(__dirname, '../../../samples/fixtures/data/global/excel/Missiles.txt')
const overlayTxtPath = path.resolve(__dirname, '../../../samples/fixtures/data/global/excel/Overlay.txt')
const statesTxtPath = path.resolve(__dirname, '../../../samples/fixtures/data/global/excel/States.txt')
// =========================================================================
// Dimension 1: 1.13c Data Table Parity & Cross-Table Relational Integrity
// =========================================================================
describe('Dimension 1: Data Table Parity & Cross-Table Relational Integrity', () => {
it('verifies State Group 1 definition in States.txt for all 3 cold armors via registry', async () => {
const registry = await getSharedDataRegistry()
const coldStates = ['frozenarmor', 'shiverarmor', 'chillingarmor']
for (const st of coldStates) {
const stateRec = registry.getStateByName(st)
expect(stateRec, `State ${st} must exist in registry`).toBeDefined()
expect(stateRec!.group, `State ${st} must belong to State Group 1`).toBe(1)
}
})
it('verifies Missiles.txt row 264 (chillingarmorbolt) vs row 59 (icebolt) ReturnFire contract', () => {
const content = fs.readFileSync(missilesTxtPath, 'latin1')
const lines = content.split('\n')
const headers = lines[0]!.split('\t').map(h => h.trim())
const missileIdx = headers.indexOf('Missile')
const idIdx = headers.indexOf('Id')
const returnFireIdx = headers.indexOf('ReturnFire')
const hitShiftIdx = headers.indexOf('HitShift')
const velIdx = headers.indexOf('Vel')
// icebolt (ID 59) allows return fire
const iceboltRow = lines.slice(1).map(l => l.split('\t')).find(r => r[idIdx]?.trim() === '59')
expect(iceboltRow).toBeDefined()
expect(iceboltRow![missileIdx]?.trim()).toBe('icebolt')
expect(Number(iceboltRow![returnFireIdx])).toBe(1)
// chillingarmorbolt (ID 264) forbids return fire to prevent infinite loops
const boltRow = lines.slice(1).map(l => l.split('\t')).find(r => r[idIdx]?.trim() === '264')
expect(boltRow).toBeDefined()
expect(boltRow![missileIdx]?.trim()).toBe('chillingarmorbolt')
const rf = boltRow![returnFireIdx]?.trim()
expect(rf === '' || rf === '0' || rf === undefined).toBe(true)
expect(Number(boltRow![velIdx])).toBe(18) // Faster velocity than base icebolt (12)
expect(Number(boltRow![hitShiftIdx])).toBe(8)
})
it('verifies HitShift contracts across Skills 40, 50, and 60', () => {
const content = fs.readFileSync(skillsTxtPath, 'latin1')
const lines = content.split('\n')
const headers = lines[0]!.split('\t').map(h => h.trim())
const idIdx = headers.indexOf('Id')
const hitShiftIdx = headers.indexOf('HitShift')
const getRow = (id: string) => lines.slice(1).map(l => l.split('\t')).find(r => r[idIdx]?.trim() === id)
// Skill 40 (Frozen Armor): HitShift = 8
expect(Number(getRow('40')![hitShiftIdx])).toBe(8)
// Skill 50 (Shiver Armor): HitShift = 7 -> divide by 2
expect(Number(getRow('50')![hitShiftIdx])).toBe(7)
// Skill 60 (Chilling Armor): HitShift = 7 -> divide by 2
expect(Number(getRow('60')![hitShiftIdx])).toBe(7)
})
})
// =========================================================================
// Dimension 2: Baked Visual Assets & Zero-Runtime MPQ Invariance
// =========================================================================
describe('Dimension 2: Baked Visual Assets & Zero-Runtime MPQ Invariance', () => {
it('verifies pre-baked overlay files have valid PNG magic bytes and JSON sprite atlases', () => {
const publicDir = path.resolve(__dirname, '../../../public')
const assetFiles = [
'overlays/FrozenArmor.png',
'overlays/FrozenArmor.json',
'overlays/AuraResistColdCast.png',
'overlays/AuraResistColdCast.json',
'missiles/icebolt.png',
'missiles/icebolt.json',
]
for (const rel of assetFiles) {
const full = path.join(publicDir, rel)
expect(fs.existsSync(full), `Asset file ${rel} must exist`).toBe(true)
const stat = fs.statSync(full)
expect(stat.size, `Asset file ${rel} must not be empty`).toBeGreaterThan(100)
if (rel.endsWith('.png')) {
const buf = fs.readFileSync(full)
// PNG magic bytes: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A
expect(buf[0]).toBe(0x89)
expect(buf[1]).toBe(0x50) // P
expect(buf[2]).toBe(0x4E) // N
expect(buf[3]).toBe(0x47) // G
} else if (rel.endsWith('.json')) {
const content = JSON.parse(fs.readFileSync(full, 'utf8'))
expect(content.frames || content.framesPerDirection || content.directions).toBeTruthy()
}
}
})
it('verifies typed metadata registration in OVERLAY_METAS and MISSILE_METAS', () => {
// Overlays
expect(OVERLAY_METAS['frozenarmor']).toBe(FROZEN_ARMOR_OVERLAY_META)
expect(OVERLAY_METAS['shiverarmor']).toBe(FROZEN_ARMOR_OVERLAY_META)
expect(OVERLAY_METAS['chillingarmor']).toBe(FROZEN_ARMOR_OVERLAY_META)
expect(OVERLAY_METAS['frozenarmor_hit']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META)
expect(OVERLAY_METAS['shiverarmor_hit']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META)
expect(OVERLAY_METAS['chillingarmor_hit']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META)
// Missiles
expect(MISSILE_METAS['chillingarmorbolt']).toBe(ICEBOLT_META)
// Cast overlays
expect(getSkillCastOverlay(40)).toBe('ice_cast_1')
expect(getSkillCastOverlay(50)).toBe('ice_cast_2')
expect(getSkillCastOverlay(60)).toBe('ice_cast_3')
})
})
// =========================================================================
// Dimension 3 & 4: Kinematics, Physics & Damage Filtering Stress
// =========================================================================
describe('Dimension 3 & 4: Kinematics, Physics & Damage Filtering Stress', () => {
it('Frozen Armor: strictly requires physical damage > 0 to freeze', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
// Case 1: 0 phys damage, 1000 fire damage -> NO freeze
let out = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 0,
incomingElem256: 1000 * FIXED_ONE,
elemType: 'fire',
attackerStateBus: attackerBus,
})
expect(out.attackerStatesApplied).not.toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(false)
// Case 2: 1 unit of physical damage (1/256) -> DOES freeze
out = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 1, // 1/256th of 1 point of damage
incomingElem256: 0,
attackerStateBus: attackerBus,
})
expect(out.attackerStatesApplied).toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(true)
})
it('Shiver Armor: triggers proactively on attack declaration regardless of attack success', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 10 })
// Attacker initiates swing that deals 0 damage (missed / dodged / blocked)
const out = defenderBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: 0,
attackerStateBus: attackerBus,
})
expect(out.attackerStatesApplied).toContain('cold')
expect(attackerBus.hasState('cold')).toBe(true)
expect(out.attackerColdDamage256).toBeGreaterThan(0)
})
it('Chilling Armor: ignores melee damage and melee attacks completely', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
// 1. Struck in melee with massive damage
const outMelee = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 10000 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(outMelee.spawnedMissiles.length).toBe(0)
// 2. Attacked in melee
const outAttack = defenderBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: 10000 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(outAttack.spawnedMissiles.length).toBe(0)
})
})
// =========================================================================
// Dimension 5: Synergy & 1.13c Blvl Invariant
// =========================================================================
describe('Dimension 5: Synergy & 1.13c Blvl Invariant', () => {
it('strictly enforces hard points rule: +allskills and charges give zero synergy bonus', async () => {
const registry = await getSharedDataRegistry()
const skillRec = registry.getSkillById(50)! // Shiver Armor
// Hard points only (20 in Frozen Armor, 20 in Chilling Armor)
const statsHard = new UnitStatList()
statsHard.setBaseSkillLevel(50, 20)
statsHard.setBaseSkillLevel(40, 20)
statsHard.setBaseSkillLevel(60, 20)
const resHard = skillRec ? skillRec : null
expect(resHard).toBeDefined()
const dmgHard = calculateShiverArmorDamage(20, { frozenArmor: 20, chillingArmor: 20 })
expect(dmgHard.min).toBe(276)
expect(dmgHard.max).toBe(328.9)
// Soft skills (+20 all skills, +33 charged skill levels)
const statsSoft = new UnitStatList()
statsSoft.setBaseSkillLevel(50, 20)
statsSoft.setBonusSkillLevel(40, 20)
statsSoft.setBonusSkillLevel(60, 20)
statsSoft.setChargedSkillLevel(40, 33)
statsSoft.setChargedSkillLevel(60, 33)
// In 1.13c, soft skills must NOT contribute to synergies
const dmgSoft = calculateShiverArmorDamage(20, {
frozenArmor: statsSoft.getBaseSkillLevel(40),
chillingArmor: statsSoft.getBaseSkillLevel(60),
})
// Without hard points, base damage only: 60 - 71.5
expect(dmgSoft.min).toBe(60)
expect(dmgSoft.max).toBe(71.5)
})
it('validates mathematical epsilon parity (+1e-9) prevents IEEE-754 floor truncation', () => {
// 50.5 * 3.8 = 191.89999999999998 in standard JS IEEE-754 floating point arithmetic!
const rawProduct = 50.5 * 3.8
expect(rawProduct).toBeLessThan(191.9)
// Without epsilon, Math.floor(rawProduct * 10) / 10 would erroneously yield 191.8
expect(Math.floor(rawProduct * 10) / 10).toBe(191.8)
// With authentic epsilon guard (+ 1e-9), calculateChillingArmorDamage produces authentic 191.9
const dmg = calculateChillingArmorDamage(20, { frozenArmor: 20, shiverArmor: 20 })
expect(dmg.max).toBe(191.9)
})
})
// =========================================================================
// Dimension 6 & 7: Lifecycle, Resource Management & BATCH1 Invariants
// =========================================================================
describe('Dimension 6 & 7: Lifecycle, Resource Management & BATCH1 Invariants', () => {
it('enforces constant mana costs and zero cooldowns for all 3 armors', () => {
expect(getSkillManaCost(40, 1)).toBe(7)
expect(getSkillManaCost(40, 99)).toBe(7)
expect(getSkillCooldownTicks(40)).toBe(0)
expect(getSkillManaCost(50, 1)).toBe(11)
expect(getSkillManaCost(50, 99)).toBe(11)
expect(getSkillCooldownTicks(50)).toBe(0)
expect(getSkillManaCost(60, 1)).toBe(17)
expect(getSkillManaCost(60, 99)).toBe(17)
expect(getSkillCooldownTicks(60)).toBe(0)
})
it('enforces BATCH1_SKILLS length invariant strictly 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
})
})
// =========================================================================
// Deep Adversarial Probe A: Infinite Reflection Loop Protection
// =========================================================================
describe('Deep Adversarial Probe A: Infinite Reflection Loop Protection', () => {
it('terminates Chilling Armor retaliation loop after exactly 1 cycle (depth <= 1)', async () => {
const registry = await getSharedDataRegistry()
// Entity A (Player Sorceress) with Chilling Armor
const statsA = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const busA = new StateBus(statsA, registry)
busA.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
// Entity B (Enemy Sorceress / Monster with Chilling Armor)
const statsB = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const busB = new StateBus(statsB, registry)
busB.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
// Step 1: An initial arrow (returnFire: true) hits Entity B
const initialHit = busB.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 100 * FIXED_ONE,
returnFire: true,
attackerStateBus: busA,
})
expect(initialHit.spawnedMissiles).toContain('chillingarmorbolt')
expect(initialHit.spawnedMissiles.length).toBe(1)
// Step 2: The spawned chillingarmorbolt travels and hits Entity A.
// Lookup chillingarmorbolt in data registry:
const boltRec = registry.getMissileByName('chillingarmorbolt')!
expect(boltRec).toBeDefined()
expect(boltRec.returnFire).toBe(false) // Must be false per 1.13c Missiles.txt!
// Step 3: chillingarmorbolt impacts Entity A with returnFire: boltRec.returnFire (false)
const retaliatoryHit = busA.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 150 * FIXED_ONE,
returnFire: boltRec.returnFire,
attackerStateBus: busB,
})
// Must NOT spawn another missile — loop terminates immediately!
expect(retaliatoryHit.spawnedMissiles.length).toBe(0)
})
it('simulates a 100-cycle ping-pong loop test verifying zero missile explosion cascades', async () => {
const registry = await getSharedDataRegistry()
const statsA = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE })
const statsB = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE })
const busA = new StateBus(statsA, registry)
const busB = new StateBus(statsB, registry)
busA.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
busB.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
let totalMissilesSpawned = 0
const boltRec = registry.getMissileByName('chillingarmorbolt')!
// Simulate 100 consecutive incoming missiles with returnFire: false
for (let i = 0; i < 100; i++) {
const outA = busA.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 50 * FIXED_ONE,
returnFire: boltRec.returnFire, // false
attackerStateBus: busB,
})
totalMissilesSpawned += outA.spawnedMissiles.length
const outB = busB.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 50 * FIXED_ONE,
returnFire: boltRec.returnFire, // false
attackerStateBus: busA,
})
totalMissilesSpawned += outB.spawnedMissiles.length
}
// Zero missiles must be spawned from non-returnFire impacts
expect(totalMissilesSpawned).toBe(0)
})
it('handles concurrent barrage of 100 missiles with mixed returnFire flags deterministically', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 10000 * FIXED_ONE, hitpoints: 10000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
bus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
let retTrueCount = 0
let retFalseCount = 0
let missilesProduced = 0
// 100 mixed missiles: even indices have returnFire: true, odd have false
for (let i = 0; i < 100; i++) {
const rf = i % 2 === 0
if (rf) retTrueCount++
else retFalseCount++
const out = bus.triggerReactiveEvents({
event: 'hitbymissile',
incomingPhys256: 10 * FIXED_ONE,
returnFire: rf,
})
missilesProduced += out.spawnedMissiles.length
}
expect(retTrueCount).toBe(50)
expect(retFalseCount).toBe(50)
expect(missilesProduced).toBe(50) // Exactly 50 retaliations for 50 returnFire: true missiles
})
})
// =========================================================================
// Deep Adversarial Probe B: Mid-Duration Armor Switching & Lingering Effects
// =========================================================================
describe('Deep Adversarial Probe B: Mid-Duration Armor Switching & Lingering Effects', () => {
it('instantly removes previous armor defense modifiers upon switching armors', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { armorclass: 100, maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
// Step 1: Base defense is 100
expect(stats.getAccruedStat('armorclass')).toBe(100)
// Step 2: Cast Frozen Armor (slvl 20) -> +125% defense
bus.applyState({
stateNameOrId: 'frozenarmor',
sourceSkillId: 40,
slvl: 20,
stats: { skill_armor_percent: calculateFrozenArmorDefense(20) }, // 125
})
expect(bus.hasState('frozenarmor')).toBe(true)
expect(stats.getModifierBonus('skill_armor_percent')).toBe(125)
// 100 * (100 + 125) / 100 = 225
expect(stats.getAccruedStat('armorclass')).toBe(225)
// Step 3: Mid-duration switch to Shiver Armor (slvl 1) -> +45% defense
// Must evict Frozen Armor completely: defense must become 145, NOT 100 + 125 + 45 = 270!
bus.applyState({
stateNameOrId: 'shiverarmor',
sourceSkillId: 50,
slvl: 1,
stats: { skill_armor_percent: calculateShiverArmorDefense(1) }, // 45
})
expect(bus.hasState('frozenarmor')).toBe(false)
expect(bus.hasState('shiverarmor')).toBe(true)
expect(stats.getModifierBonus('skill_armor_percent')).toBe(45)
expect(stats.getAccruedStat('armorclass')).toBe(145)
// Step 4: Mid-duration switch to Chilling Armor (slvl 10) -> +90% defense
bus.applyState({
stateNameOrId: 'chillingarmor',
sourceSkillId: 60,
slvl: 10,
stats: { skill_armor_percent: calculateChillingArmorDefense(10) }, // 90
})
expect(bus.hasState('shiverarmor')).toBe(false)
expect(bus.hasState('chillingarmor')).toBe(true)
expect(stats.getModifierBonus('skill_armor_percent')).toBe(90)
expect(stats.getAccruedStat('armorclass')).toBe(190)
// Step 5: Remove armor -> defense resets to baseline 100
bus.removeState('chillingarmor')
expect(bus.hasState('chillingarmor')).toBe(false)
expect(stats.getModifierBonus('skill_armor_percent')).toBe(0)
expect(stats.getAccruedStat('armorclass')).toBe(100)
})
it('ensures no lingering reactive event triggers from previous armors after switching', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500 * FIXED_ONE, hitpoints: 500 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
// Start with Frozen Armor
defenderBus.applyState({ stateNameOrId: 'frozenarmor', sourceSkillId: 40, slvl: 20 })
// Immediately switch to Chilling Armor (Skill 60)
defenderBus.applyState({ stateNameOrId: 'chillingarmor', sourceSkillId: 60, slvl: 20 })
// Attacker strikes with melee physical damage -> Chilling Armor does NOT react to melee!
// If Frozen Armor lingered, attacker would be frozen solid.
const out = defenderBus.triggerReactiveEvents({
event: 'damagedinmelee',
incomingPhys256: 200 * FIXED_ONE,
attackerStateBus: attackerBus,
})
expect(out.attackerStatesApplied).not.toContain('freeze')
expect(attackerBus.hasState('freeze')).toBe(false)
expect(out.spawnedMissiles.length).toBe(0)
})
it('stress tests 100 rapid armor switches in tight loop verifying zero orphaned state entries', async () => {
const registry = await getSharedDataRegistry()
const stats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE })
const bus = new StateBus(stats, registry)
const armors = [
{ name: 'frozenarmor', id: 40, def: 125 },
{ name: 'shiverarmor', id: 50, def: 159 },
{ name: 'chillingarmor', id: 60, def: 140 },
]
for (let i = 0; i < 100; i++) {
const armor = armors[i % 3]!
bus.applyState({
stateNameOrId: armor.name,
sourceSkillId: armor.id,
slvl: 20,
stats: { skill_armor_percent: armor.def },
})
// Invariant: at any point, exactly 1 Group 1 armor state can exist in entries
const group1Entries = bus.getActiveEntries().filter(e => e.group === 1 || ['frozenarmor', 'shiverarmor', 'chillingarmor'].includes(e.stateName))
expect(group1Entries.length).toBe(1)
expect(group1Entries[0]!.stateName).toBe(armor.name)
expect(stats.getModifierBonus('skill_armor_percent')).toBe(armor.def)
}
})
})
// =========================================================================
// Deep Adversarial Probe C: Fixed-Point 256-Precision Bitrate Cold Damage
// =========================================================================
describe('Deep Adversarial Probe C: Fixed-Point 256-Precision Bitrate Cold Damage', () => {
it('verifies exact integer 256-bitrate scaling across all 60 skill levels and synergies for Shiver Armor', () => {
// EMin: 12, EMax: 16, HitShift: 7
for (let slvl = 1; slvl <= 60; slvl++) {
for (const synPoints of [0, 10, 20, 40]) {
const dmg = calculateShiverArmorDamage(slvl, synPoints)
expect(dmg.min).toBeGreaterThan(0)
expect(dmg.max).toBeGreaterThanOrEqual(dmg.min)
expect(Number.isFinite(dmg.min)).toBe(true)
expect(Number.isFinite(dmg.max)).toBe(true)
// In 256-bitrate integer arithmetic:
const min256 = Math.round(dmg.min * FIXED_ONE)
const max256 = Math.round(dmg.max * FIXED_ONE)
expect(Number.isInteger(min256)).toBe(true)
expect(Number.isInteger(max256)).toBe(true)
}
}
})
it('verifies exact HitShift 7 bitrate conversion for base Shiver Armor (Skill 50)', () => {
// Level 1: EMin=12, EMax=16. HitShift 7 -> 12 * 128 = 1536 (6.0), 16 * 128 = 2048 (8.0)
const dmg1 = calculateShiverArmorDamage(1, 0)
expect(dmg1.min * 256).toBe(12 << 7)
expect(dmg1.max * 256).toBe(16 << 7)
// Level 10: rawMin = 52, rawMax = 65 -> 52 << 7 = 6656 (26.0), 65 << 7 = 8320 (32.5)
const dmg10 = calculateShiverArmorDamage(10, 0)
expect(dmg10.min * 256).toBe(52 << 7)
expect(dmg10.max * 256).toBe(65 << 7)
// Level 20: rawMin = 120, rawMax = 143 -> 120 << 7 = 15360 (60.0), 143 << 7 = 18304 (71.5)
const dmg20 = calculateShiverArmorDamage(20, 0)
expect(dmg20.min * 256).toBe(120 << 7)
expect(dmg20.max * 256).toBe(143 << 7)
})
it('verifies exact HitShift 7 bitrate conversion for base Chilling Armor (Skill 60)', () => {
// Level 1: EMin=8, EMax=12. HitShift 7 -> 8 * 128 = 1024 (4.0), 12 * 128 = 1536 (6.0)
const dmg1 = calculateChillingArmorDamage(1, 0)
expect(dmg1.min * 256).toBe(8 << 7)
expect(dmg1.max * 256).toBe(12 << 7)
// Level 10: rawMin = 30, rawMax = 43 -> 30 << 7 = 3840 (15.0), 43 << 7 = 5504 (21.5)
const dmg10 = calculateChillingArmorDamage(10, 0)
expect(dmg10.min * 256).toBe(30 << 7)
expect(dmg10.max * 256).toBe(43 << 7)
// Level 20: rawMin = 78, rawMax = 101 -> 78 << 7 = 9984 (39.0), 101 << 7 = 12928 (50.5)
const dmg20 = calculateChillingArmorDamage(20, 0)
expect(dmg20.min * 256).toBe(78 << 7)
expect(dmg20.max * 256).toBe(101 << 7)
})
it('simulates 100 consecutive Shiver Armor hits verifying zero floating point HP drift', async () => {
const registry = await getSharedDataRegistry()
const defenderStats = new UnitStatList(registry, { maxhp: 10000 * FIXED_ONE, hitpoints: 10000 * FIXED_ONE })
const attackerStats = new UnitStatList(registry, { maxhp: 500000 * FIXED_ONE, hitpoints: 500000 * FIXED_ONE })
const defenderBus = new StateBus(defenderStats, registry)
const attackerBus = new StateBus(attackerStats, registry)
defenderBus.applyState({ stateNameOrId: 'shiverarmor', sourceSkillId: 50, slvl: 20 })
let totalDamageDeducted256 = 0
const initialHp256 = attackerStats.getHp256()
for (let i = 0; i < 100; i++) {
const out = defenderBus.triggerReactiveEvents({
event: 'attackedinmelee',
incomingPhys256: 100 * FIXED_ONE,
attackerStateBus: attackerBus,
})
const dmg256 = out.attackerColdDamage256
expect(Number.isInteger(dmg256)).toBe(true)
attackerStats.setHp256(attackerStats.getHp256() - dmg256)
totalDamageDeducted256 += dmg256
}
// Assert HP arithmetic is exact to the 256th bit
expect(attackerStats.getHp256()).toBe(initialHp256 - totalDamageDeducted256)
})
})
// =========================================================================
// Fuzzing & Degenerate Input Boundary Testing
// =========================================================================
describe('Fuzzing & Degenerate Input Boundary Testing', () => {
it('gracefully handles boundary and degenerate slvl values without crashes or NaN', () => {
const degenerateSlvls = [-10, 0, 0.5, 1, 99, 500, NaN, Infinity, -Infinity]
for (const slvl of degenerateSlvls) {
expect(() => calculateFrozenArmorDefense(slvl)).not.toThrow()
expect(() => calculateFrozenArmorDuration(slvl)).not.toThrow()
expect(() => calculateFrozenArmorFreezeLength(slvl)).not.toThrow()
expect(() => calculateShiverArmorDefense(slvl)).not.toThrow()
expect(() => calculateShiverArmorDuration(slvl)).not.toThrow()
expect(() => calculateShiverArmorDamage(slvl)).not.toThrow()
expect(() => calculateShiverArmorChillLength(slvl)).not.toThrow()
expect(() => calculateChillingArmorDefense(slvl)).not.toThrow()
expect(() => calculateChillingArmorDuration(slvl)).not.toThrow()
expect(() => calculateChillingArmorDamage(slvl)).not.toThrow()
expect(() => calculateChillingArmorChillLength(slvl)).not.toThrow()
const dmgS = calculateShiverArmorDamage(slvl)
expect(Number.isFinite(dmgS.min)).toBe(true)
expect(Number.isFinite(dmgS.max)).toBe(true)
const dmgC = calculateChillingArmorDamage(slvl)
expect(Number.isFinite(dmgC.min)).toBe(true)
expect(Number.isFinite(dmgC.max)).toBe(true)
}
})
it('gracefully handles degenerate synergy inputs', () => {
const degenerateSyns = [-50, 0, 1000, NaN, undefined, null, {} as any]
for (const syn of degenerateSyns) {
expect(() => calculateShiverArmorDamage(20, syn)).not.toThrow()
expect(() => calculateChillingArmorDamage(20, syn)).not.toThrow()
expect(() => calculateFrozenArmorDuration(20, syn)).not.toThrow()
expect(() => calculateFrozenArmorFreezeLength(20, syn)).not.toThrow()
}
})
})
})

View File

@ -0,0 +1,336 @@
import { describe, expect, it } from 'vitest'
import {
calculateFrozenArmorDefense,
calculateFrozenArmorDuration,
calculateFrozenArmorFreezeLength,
calculateShiverArmorDefense,
calculateShiverArmorDuration,
calculateShiverArmorDamage,
calculateShiverArmorChillLength,
calculateChillingArmorDefense,
calculateChillingArmorDuration,
calculateChillingArmorDamage,
calculateChillingArmorChillLength,
calculateColdArmorDuration,
BATCH1_SKILLS,
} from '../../../src/game/skills.ts'
import {
createWorld,
tickMonsters,
updateMonsters,
hitPlayerWithMissile,
type Monster,
type CombatTerrain,
type CombatOptions,
} from '../../../src/game/combat.ts'
import {
executeSUnitDmg,
type CombatUnitContext,
type SUnitDmgPacket,
} from '../../../src/game/engine/combat-pipeline.ts'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList, FIXED_ONE } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
const OPEN: CombatTerrain = { overlap: () => 0 }
const DEFAULT_OPTIONS: CombatOptions = {
playerSpeed: 100,
playerReach: 40,
playerCooldownTicks: 25,
playerDamage: 10,
playerManaPerAttack: 0,
respawnTicks: 100,
}
describe('Cold Armors Remediation Suite (Reviewer 2 Findings)', () => {
describe('Finding 1: Number Overload Contract & Multiplier Options', () => {
it('interprets number synergy as allocated skill points for Shiver Armor', () => {
// Base at slvl 20 is min 60, max 71.5
// 0 points -> 1.0x -> { min: 60, max: 71.5 }
expect(calculateShiverArmorDamage(20, 0)).toEqual({ min: 60, max: 71.5 })
// 20 points -> 1 + (20 * 9)/100 = 2.8x -> 60*2.8=168, 71.5*2.8=200.2
expect(calculateShiverArmorDamage(20, 20)).toEqual({ min: 168.0, max: 200.2 })
// 40 points -> 1 + (40 * 9)/100 = 4.6x -> 60*4.6=276, 71.5*4.6=328.9
expect(calculateShiverArmorDamage(20, 40)).toEqual({ min: 276.0, max: 328.9 })
// Parity between object synergy { frozenArmor: 20, chillingArmor: 20 } and number 40
const objDmg = calculateShiverArmorDamage(20, { frozenArmor: 20, chillingArmor: 20 })
const numDmg = calculateShiverArmorDamage(20, 40)
expect(numDmg).toEqual(objDmg)
})
it('interprets number synergy as allocated skill points for Chilling Armor', () => {
// Base at slvl 20 is min 39, max 50.5
// 0 points -> 1.0x -> { min: 39, max: 50.5 }
expect(calculateChillingArmorDamage(20, 0)).toEqual({ min: 39, max: 50.5 })
// 20 points -> 1 + (20 * 7)/100 = 2.4x -> 39*2.4=93.6, 50.5*2.4=121.2
expect(calculateChillingArmorDamage(20, 20)).toEqual({ min: 93.6, max: 121.2 })
// 40 points -> 1 + (40 * 7)/100 = 3.8x -> 39*3.8=148.2, 50.5*3.8=191.9
expect(calculateChillingArmorDamage(20, 40)).toEqual({ min: 148.2, max: 191.9 })
// Parity between object synergy { frozenArmor: 20, shiverArmor: 20 } and number 40
const objDmg = calculateChillingArmorDamage(20, { frozenArmor: 20, shiverArmor: 20 })
const numDmg = calculateChillingArmorDamage(20, 40)
expect(numDmg).toEqual(objDmg)
})
it('supports options.synMultiplier to explicitly override multiplier', () => {
// Override multiplier directly to 4.6x on Shiver Armor
expect(calculateShiverArmorDamage(20, undefined, { synMultiplier: 4.6 })).toEqual({
min: 276.0,
max: 328.9,
})
// Override multiplier directly to 3.8x on Chilling Armor
expect(calculateChillingArmorDamage(20, undefined, { synMultiplier: 3.8 })).toEqual({
min: 148.2,
max: 191.9,
})
})
it('clamps negative synergy points to 0 without degrading below base', () => {
expect(calculateShiverArmorDamage(20, -10)).toEqual(calculateShiverArmorDamage(20, 0))
expect(calculateChillingArmorDamage(20, -10)).toEqual(calculateChillingArmorDamage(20, 0))
})
})
describe('Finding 4: Negative Synergy Clamping in Duration & Length Calculators', () => {
it('clamps negative synergy in duration calculators', () => {
expect(calculateFrozenArmorDuration(10, -5)).toBe(calculateFrozenArmorDuration(10, 0))
expect(calculateShiverArmorDuration(10, -5)).toBe(calculateShiverArmorDuration(10, 0))
expect(calculateChillingArmorDuration(10, -5)).toBe(calculateChillingArmorDuration(10, 0))
expect(calculateColdArmorDuration(10, 1200, 120, 10, -5)).toBe(calculateColdArmorDuration(10, 1200, 120, 10, 0))
})
it('clamps negative synergy in freeze and chill length calculators', () => {
expect(calculateFrozenArmorFreezeLength(10, -5)).toBe(calculateFrozenArmorFreezeLength(10, 0))
expect(calculateShiverArmorChillLength(10, -5)).toBe(calculateShiverArmorChillLength(10, 0))
expect(calculateChillingArmorChillLength(10, -5)).toBe(calculateChillingArmorChillLength(10, 0))
})
})
describe('Finding 2: Active Combat Hooks in combat.ts', () => {
function makeMonster(overrides?: Partial<Monster>): Monster {
return {
index: 0,
x: 10,
y: 10,
hp: 100,
cooldown: 0,
state: 'idle',
facing: 0,
hitFlash: 0,
corpseTicks: 0,
stats: {
id: 'fallen',
name: 'Fallen',
hp: 100,
damage: 15,
cooldownTicks: 25,
reach: 40, // melee
aggroRadius: 200,
speed: 100,
xp: 10,
},
...overrides,
}
}
it('verifies updateMonsters is alias of tickMonsters', () => {
expect(updateMonsters).toBe(tickMonsters)
})
it('proactively chills and damages monster when Shiver Armor player is attacked in melee', () => {
const world = createWorld(10, 10, 500, 500)
world.player.coldArmorType = 'shiverarmor'
world.player.coldArmorSlvl = 20
const monster = makeMonster({ x: 10, y: 30 }) // distance = 20 <= reach 40
world.monsters = [monster]
// tickMonsters: monster initiates attack on player
tickMonsters(world, DEFAULT_OPTIONS, OPEN, world.players)
// Monster should be chilled and have taken cold damage
expect(monster.chillTicks).toBeGreaterThan(0)
expect(monster.hp).toBeLessThan(100)
})
it('freezes monster reactively when hit lands on Frozen Armor player with physical damage', () => {
const world = createWorld(10, 10, 500, 500)
world.player.coldArmorType = 'frozenarmor'
world.player.coldArmorSlvl = 20
const monster = makeMonster({ x: 10, y: 30 }) // within reach 40
world.monsters = [monster]
tickMonsters(world, DEFAULT_OPTIONS, OPEN, world.players)
expect(monster.frozenTicks).toBeGreaterThan(0)
})
it('retaliates with chillingarmorbolt when Chilling Armor player is hit by missile', () => {
const world = createWorld(10, 10, 500, 500)
world.player.coldArmorType = 'chillingarmor'
world.player.coldArmorSlvl = 20
const attacker = makeMonster({ index: 0, x: 100, y: 10 })
world.monsters = [attacker]
const ret = hitPlayerWithMissile(
world,
{
damage: 15,
attackerIndex: 0,
returnFire: true,
},
world.player,
)
expect(ret.reflected).toBe(true)
expect(ret.retaliatoryMissile).toBe('chillingarmorbolt')
expect(world.events.some(e => e.kind === 'missileHit' && e.missileType === 'chillingarmorbolt')).toBe(true)
expect(attacker.chillTicks).toBeGreaterThan(0)
})
it('does NOT retaliate if missile has returnFire = false', () => {
const world = createWorld(10, 10, 500, 500)
world.player.coldArmorType = 'chillingarmor'
world.player.coldArmorSlvl = 20
const attacker = makeMonster({ index: 0, x: 100, y: 10 })
world.monsters = [attacker]
const ret = hitPlayerWithMissile(
world,
{
damage: 15,
attackerIndex: 0,
returnFire: false,
},
world.player,
)
expect(ret.reflected).toBe(false)
expect(ret.retaliatoryMissile).toBeUndefined()
})
})
describe('Finding 3: Reactive Combat Pipeline Wiring in combat-pipeline.ts', () => {
async function makeCombatant(id: string, name: string): Promise<CombatUnitContext> {
const registry = await getSharedDataRegistry()
const statList = new UnitStatList(registry, {
maxhp: 1000 * FIXED_ONE,
hitpoints: 1000 * FIXED_ONE,
tohit: 1000,
armorclass: 100,
level: 20,
})
const stateBus = new StateBus(statList, registry)
return {
id,
name,
statList,
stateBus,
weaponMinPhys: 20,
weaponMaxPhys: 40,
}
}
it('dispatches attackedinmelee proactively and damages attacker with Shiver Armor', async () => {
const attacker = await makeCombatant('att1', 'Attacking Monster')
const defender = await makeCombatant('def1', 'Sorceress')
// Apply Shiver Armor state 88 to defender
defender.stateBus.applyState({
stateNameOrId: 'shiverarmor',
sourceSkillId: 50,
slvl: 20,
})
const packet: SUnitDmgPacket = {
skillId: 0,
attackKind: 'melee',
physDamagePct: 0,
flatPhysMin256: 20 * FIXED_ONE,
flatPhysMax256: 40 * FIXED_ONE,
}
const outcome = executeSUnitDmg(attacker, defender, packet)
// Attacker should receive chill state and cold damage
expect(outcome.attackerColdDamage256).toBeDefined()
expect(outcome.attackerColdDamage256).toBeGreaterThan(0)
expect(outcome.attackerStatesApplied).toBeDefined()
expect(outcome.attackerStatesApplied).toContain('cold')
})
it('dispatches damagedinmelee post-damage and freezes attacker with Frozen Armor', async () => {
const attacker = await makeCombatant('att2', 'Attacking Monster')
const defender = await makeCombatant('def2', 'Sorceress')
// Apply Frozen Armor state 10 to defender
defender.stateBus.applyState({
stateNameOrId: 'frozenarmor',
sourceSkillId: 40,
slvl: 20,
})
const packet: SUnitDmgPacket = {
skillId: 0,
attackKind: 'melee',
flatPhysMin256: 20 * FIXED_ONE,
flatPhysMax256: 40 * FIXED_ONE,
autoHit: true,
}
const outcome = executeSUnitDmg(attacker, defender, packet)
// Attacker should be frozen
expect(outcome.attackerStatesApplied).toBeDefined()
expect(outcome.attackerStatesApplied).toContain('freeze')
expect(attacker.stateBus.hasState('freeze')).toBe(true)
})
it('dispatches hitbymissile and spawns retaliatory chillingarmorbolt with Chilling Armor', async () => {
const attacker = await makeCombatant('att3', 'Archer Monster')
const defender = await makeCombatant('def3', 'Sorceress')
// Apply Chilling Armor state 20 to defender
defender.stateBus.applyState({
stateNameOrId: 'chillingarmor',
sourceSkillId: 60,
slvl: 20,
})
const packet: SUnitDmgPacket = {
skillId: 0,
attackKind: 'missile',
flatPhysMin256: 20 * FIXED_ONE,
flatPhysMax256: 40 * FIXED_ONE,
returnFire: true,
autoHit: true,
}
const outcome = executeSUnitDmg(attacker, defender, packet)
// Spawns retaliatory chillingarmorbolt
expect(outcome.spawnedMissiles).toBeDefined()
expect(outcome.spawnedMissiles?.length).toBe(1)
expect(outcome.spawnedMissiles?.[0]).toBe('chillingarmorbolt')
})
})
describe('Invariant Integrity', () => {
it('strictly maintains BATCH1_SKILLS length of 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
expect(BATCH1_SKILLS).not.toHaveProperty('40')
expect(BATCH1_SKILLS).not.toHaveProperty('50')
expect(BATCH1_SKILLS).not.toHaveProperty('60')
})
})
})