684 lines
31 KiB
TypeScript
684 lines
31 KiB
TypeScript
/**
|
|
* 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()
|
|
}
|
|
})
|
|
})
|
|
})
|