test(skills): persist M4 fire and M5 cross-tree adversarial stress test suites on main

This commit is contained in:
troytt 2026-09-24 04:28:37 +00:00
parent 2e061b3257
commit 51acb0effa
4 changed files with 3747 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,922 @@
/**
* Adversarial Empirical Stress Testing Suite for Milestone 4:
* Sorceress Fire Combat Mechanics, Weapon Scaling, and Double-Dipping
*
* Target Systems:
* - Skill 52: Enchant (weapon fire damage buff, AR bonus, duration)
* - Skill 61: Fire Mastery (passive fire damage percentage scaling)
* - SUnitDmg Combat Pipeline (executeSUnitDmg, computeEffectiveResistance)
* - Combat Sandbox Simulation (CombatWorld, tickPlayer, tickCombat, tickCombatMulti)
*
* Key Verification Dimensions:
* 1. Ranged Weapon Penalty: strictly 33% of Enchant fire damage bonus on ranged weapons (bow/crossbow/throwing)
* vs 100% on melee weapons.
* 2. Sorceress Melee Double-Dip:
* - Cast time: Enchant damage scaled by Fire Mastery (+163% at slvl 20 -> 2.63x).
* - Melee strike: Sorceress applies Fire Mastery (+163% -> 2.63x) again upon hit: base * (1 + 1.63)^2 = 473.8 - 619.1!
* - Ranged strike: Sorceress attacks at range -> second Fire Mastery is NOT applied.
* 3. Non-Sorceress Buff Recipient (Allies / Pets):
* - Allies receive single-multiplied buff damage on melee (no double-dip).
* - Allies receive 33% on ranged.
* 4. Fire Immunity Preservation:
* - Target Fire Resist = 100%, 110%, 150%: strictly 0 fire damage dealt.
* - Confirms Fire Mastery has zero resistance pierce and cannot break immunities.
* - Target Fire Resist = 50% and 0%: verified exact proportional mitigation.
* 5. Resistance Format Interoperability & Lifecycle Boundary Edge Cases.
*/
import { describe, expect, it } from 'vitest'
import {
calculateEnchantDamage,
calculateEnchantAttackRating,
calculateEnchantDuration,
calculateFireMasteryBonus,
calculateEnchantAttackDamage,
} from '../../../src/game/skills.ts'
import {
createWorld,
addPlayer,
tickCombat,
applyEnchantBuff,
type CombatPlayer,
type CombatWorld,
type Monster,
type CombatOptions,
type CombatTerrain,
} from '../../../src/game/combat.ts'
import {
executeSUnitDmg,
computeEffectiveResistance,
type CombatUnitContext,
type SUnitDmgPacket,
} from '../../../src/game/engine/combat-pipeline.ts'
import { UnitStatList, FIXED_ONE } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
/** Helper to create a fully initialized CombatUnitContext */
function createCombatUnit(
id: string,
options?: {
hp?: number
isSorceress?: boolean
fireMasterySlvl?: number
fireResist?: number
hasEnchantState?: boolean
},
): CombatUnitContext {
const statList = new UnitStatList()
const hp = options?.hp ?? 1000
statList.setBaseStat('hp', hp * FIXED_ONE)
statList.setBaseStat('maxhp', hp * FIXED_ONE)
statList.setBaseStat('hitpoints', hp * FIXED_ONE)
statList.setBaseStat('level', 80)
statList.setBaseStat('tohit', 10000) // guarantee 95% or autohit
statList.setBaseStat('armorclass', 100)
if (options?.fireMasterySlvl && options.fireMasterySlvl > 0) {
statList.setBaseSkillLevel(61, options.fireMasterySlvl)
}
if (options?.fireResist !== undefined) {
statList.setBaseStat('fireresist', options.fireResist)
}
const stateBus = new StateBus(statList)
if (options?.hasEnchantState) {
stateBus.applyState({ stateNameOrId: 'enchant', durationFrames: 15000 })
}
return {
id,
name: id,
statList,
stateBus,
weaponMinPhys: 10,
weaponMaxPhys: 10,
}
}
/** Flat mock terrain */
const mockTerrain: CombatTerrain = {
overlap: () => 0,
}
/** Standard combat options for deterministic tests */
const baseOptions: CombatOptions = {
playerSpeed: 100,
playerReach: 50,
playerCooldownTicks: 10,
playerDamage: 10, // Base physical damage = 10
playerManaPerAttack: 0,
respawnTicks: 100,
}
describe('Milestone 4 Adversarial Empirical Stress Test Suite (Combat, Scaling, Double-Dip)', () => {
// ==========================================================================
// Suite 1: Ranged Weapon Penalty (33% on Ranged vs 100% on Melee)
// ==========================================================================
describe('Suite 1: Ranged Weapon Penalty (33% Ranged vs 100% Melee)', () => {
it('verifies calculateEnchantAttackDamage strictly scales ranged attacks by 33% across slvl 1..40', () => {
for (const slvl of [1, 5, 10, 15, 20, 25, 30, 35, 40]) {
const baseDmg = calculateEnchantDamage(slvl)
// Melee non-sorceress attack (100% buff damage)
const meleeRes = calculateEnchantAttackDamage({
enchantDamage: baseDmg,
isRanged: false,
isSorceress: false,
})
expect(meleeRes.min).toBe(baseDmg.min)
expect(meleeRes.max).toBe(baseDmg.max)
// Ranged non-sorceress attack (strictly 33% of buff damage)
const rangedRes = calculateEnchantAttackDamage({
enchantDamage: baseDmg,
isRanged: true,
isSorceress: false,
})
const expectedRangedMin = Math.round(baseDmg.min * 0.33 * 10) / 10
const expectedRangedMax = Math.round(baseDmg.max * 0.33 * 10) / 10
expect(rangedRes.min).toBe(expectedRangedMin)
expect(rangedRes.max).toBe(expectedRangedMax)
expect(rangedRes.average).toBe(Math.round(((expectedRangedMin + expectedRangedMax) / 2) * 10) / 10)
// Verify ratio is within rounding precision of 33%
const ratio = rangedRes.average / meleeRes.average
expect(ratio).toBeGreaterThan(0.32)
expect(ratio).toBeLessThan(0.34)
}
})
it('verifies combat pipeline executeSUnitDmg applies 33% penalty to missile fire damage while physical damage is untouched', () => {
const attacker = createCombatUnit('attacker_unit', { hasEnchantState: true })
const defender = createCombatUnit('defender_unit', { fireResist: 0 })
// Base Enchant fire damage in 256ths: 100.0 fire damage = 25600
const enchantFire256 = 100 * FIXED_ONE
// 1. Melee attack: full 100% fire damage
const meleePacket: SUnitDmgPacket = {
skillId: 52,
attackKind: 'melee',
isEnchant: true,
elemType: 'fire',
elemMin256: enchantFire256,
elemMax256: enchantFire256,
flatPhysMin256: 0,
flatPhysMax256: 0,
autoHit: true,
}
const meleeOutcome = executeSUnitDmg(attacker, defender, meleePacket)
expect(meleeOutcome.hit).toBe(true)
expect(meleeOutcome.physDamage256).toBe(10 * FIXED_ONE)
expect(meleeOutcome.elemDamage256).toBe(enchantFire256)
// 2. Missile attack: strictly 33% fire damage, physical damage remains 10 * FIXED_ONE
const rangedPacket: SUnitDmgPacket = {
skillId: 52,
attackKind: 'missile',
isEnchant: true,
elemType: 'fire',
elemMin256: enchantFire256,
elemMax256: enchantFire256,
flatPhysMin256: 0,
flatPhysMax256: 0,
autoHit: true,
}
const rangedOutcome = executeSUnitDmg(attacker, defender, rangedPacket)
expect(rangedOutcome.hit).toBe(true)
// Physical damage is completely unaffected by the 33% elemental ranged penalty!
expect(rangedOutcome.physDamage256).toBe(10 * FIXED_ONE)
// Elemental fire damage is strictly Math.trunc(25600 * 33 / 100) = 8448 (= 33.0 fire damage)
const expectedRangedFire256 = Math.trunc((enchantFire256 * 33) / 100)
expect(rangedOutcome.elemDamage256).toBe(expectedRangedFire256)
expect(rangedOutcome.elemDamage256 / FIXED_ONE).toBe(33)
})
it('verifies CombatWorld sandbox tickPlayer correctly differentiates melee (100%) vs ranged (33%) weapons', () => {
const world: CombatWorld = createWorld(100, 100)
const targetMonster: Monster = {
index: 0,
stats: {
id: 'test_zombie',
name: 'Zombie',
hp: 2000,
damage: 0,
cooldownTicks: 25,
reach: 20,
aggroRadius: 0,
speed: 0,
xp: 50,
},
x: 120,
y: 100,
hp: 2000,
cooldown: 0,
state: 'idle',
facing: 0,
hitFlash: 0,
corpseTicks: 0,
}
world.monsters.push(targetMonster)
// 1. Melee attack with Enchant slvl 20 (base 68.5 - 89.5, average 79)
const baseDmg20 = calculateEnchantDamage(20)
world.player.enchantTicks = 5000
world.player.enchantDamage = { min: baseDmg20.min, max: baseDmg20.max }
world.player.isRanged = false
world.player.isSorceress = false // non-sorc for pure 100% vs 33% check
const prevHp = targetMonster.hp
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const meleeHitEvent = world.events.find(e => e.kind === 'monsterHit')
expect(meleeHitEvent).toBeDefined()
// total = baseOptions.playerDamage (10) + Math.round(79) = 89
const meleeTotalDamage = meleeHitEvent!.amount!
expect(meleeTotalDamage).toBe(10 + Math.round((baseDmg20.min + baseDmg20.max) / 2))
expect(targetMonster.hp).toBe(prevHp - meleeTotalDamage)
// Reset cooldown and switch to ranged weapon (bow/throwing)
world.player.cooldown = 0
world.player.isRanged = true
const beforeRangedHp = targetMonster.hp
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const rangedHitEvent = world.events.find(e => e.kind === 'monsterHit')
expect(rangedHitEvent).toBeDefined()
const rangedTotalDamage = rangedHitEvent!.amount!
const expectedRangedFire = Math.round(calculateEnchantAttackDamage({
enchantDamage: baseDmg20,
isRanged: true,
isSorceress: false,
}).average)
expect(rangedTotalDamage).toBe(10 + expectedRangedFire)
expect(targetMonster.hp).toBe(beforeRangedHp - rangedTotalDamage)
// Confirm Enchant fire component on ranged is strictly 33% of melee fire component
const meleeFireComponent = meleeTotalDamage - 10
const rangedFireComponent = rangedTotalDamage - 10
expect(rangedFireComponent / meleeFireComponent).toBeCloseTo(0.33, 1)
})
it('verifies dynamic weapon swapping mid-buff alternates between 100% melee double-dip and 33% ranged penalty', () => {
const world: CombatWorld = createWorld(100, 100)
const dummy: Monster = {
index: 0,
stats: { id: 'd', name: 'D', hp: 10000, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 10 },
x: 120, y: 100, hp: 10000, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
world.monsters.push(dummy)
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
world.player.isSorceress = true
world.player.fireMasterySlvl = 20
// Strike 1: Melee -> deals 10 physical + 547 fire = 557
world.player.isRanged = false
world.player.cooldown = 0
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
expect(world.events.find(e => e.kind === 'monsterHit')?.amount).toBe(557)
// Strike 2: Swap to bow (isRanged: true) -> deals 10 physical + 69 fire = 79
world.events = []
world.player.isRanged = true
world.player.cooldown = 0
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
expect(world.events.find(e => e.kind === 'monsterHit')?.amount).toBe(79)
// Strike 3: Swap back to melee weapon -> deals 557 again
world.events = []
world.player.isRanged = false
world.player.cooldown = 0
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
expect(world.events.find(e => e.kind === 'monsterHit')?.amount).toBe(557)
})
})
// ==========================================================================
// Suite 2: Sorceress Melee Double-Dip Mechanics
// ==========================================================================
describe('Suite 2: Sorceress Melee Double-Dip Mechanics', () => {
it('verifies exact mathematical double-dip formula at slvl 20: base * (1 + 1.63)^2 = 473.8 - 619.1 fire damage', () => {
const slvl = 20
const fmSlvl = 20
const fmBonus = calculateFireMasteryBonus(fmSlvl) // +163%
expect(fmBonus).toBe(163)
const fmMultiplier = 1.0 + fmBonus / 100 // 2.63
// Base Enchant slvl 20 damage (no FM): 68.5 - 89.5
const baseDmg = calculateEnchantDamage(slvl)
expect(baseDmg.min).toBe(68.5)
expect(baseDmg.max).toBe(89.5)
// 1. Cast phase: Enchant damage is amplified by Fire Mastery upon casting
const castDmg = calculateEnchantDamage(slvl, undefined, fmSlvl)
expect(castDmg.min).toBe(180.2) // Math.round(68.5 * 2.63 * 10) / 10 = 180.2
expect(castDmg.max).toBe(235.4) // Math.round(89.5 * 2.63 * 10) / 10 = 235.4
// 2. Melee strike phase: Sorceress hits in melee, applying Fire Mastery (+163%) AGAIN
const meleeStrike = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: fmSlvl,
})
// Exact double-dip bounds:
// min = Math.round(180.2 * 2.63 * 10) / 10 = 473.9
// max = Math.round(235.4 * 2.63 * 10) / 10 = 619.1
expect(meleeStrike.min).toBe(473.9)
expect(meleeStrike.max).toBe(619.1)
expect(meleeStrike.average).toBe(546.5)
// Raw unrounded base * (1 + 1.63)^2:
// 68.5 * 2.63^2 = 473.80775
// 89.5 * 2.63^2 = 619.06275
expect(meleeStrike.min).toBeCloseTo(baseDmg.min * fmMultiplier * fmMultiplier, 0)
expect(meleeStrike.max).toBeCloseTo(baseDmg.max * fmMultiplier * fmMultiplier, 0)
})
it('verifies Sorceress attacking at range strictly does NOT receive the second Fire Mastery application', () => {
const slvl = 20
const fmSlvl = 20
const castDmg = calculateEnchantDamage(slvl, undefined, fmSlvl) // 180.2 - 235.4
const rangedStrike = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: true,
isSorceress: true,
fireMasterySlvl: fmSlvl,
})
// Strictly 33% of cast damage:
// min = Math.round(180.2 * 0.33 * 10) / 10 = 59.5
// max = Math.round(235.4 * 0.33 * 10) / 10 = 77.7
expect(rangedStrike.min).toBe(59.5)
expect(rangedStrike.max).toBe(77.7)
expect(rangedStrike.average).toBe(68.6)
// Prove that if second FM had been erroneously applied, it would have been:
// 473.9 * 0.33 = 156.4, which is 2.63x higher!
expect(rangedStrike.min).toBeLessThan(100)
expect(rangedStrike.max).toBeLessThan(100)
})
it('verifies double-dip scaling across full Fire Mastery progression (slvl 1, 10, 20, 30, 40)', () => {
for (const fmSlvl of [1, 10, 20, 30, 40]) {
const fmBonus = calculateFireMasteryBonus(fmSlvl)
const fmMul = 1.0 + fmBonus / 100
const expectedDoubleMultiplier = fmMul * fmMul
const baseDmg = calculateEnchantDamage(20) // base 68.5 - 89.5
const castDmg = calculateEnchantDamage(20, undefined, fmSlvl)
const meleeHit = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: fmSlvl,
})
// Verify that melee damage scales as (1 + fmBonus/100)^2
const actualRatioMin = meleeHit.min / baseDmg.min
const actualRatioMax = meleeHit.max / baseDmg.max
expect(actualRatioMin).toBeCloseTo(expectedDoubleMultiplier, 1)
expect(actualRatioMax).toBeCloseTo(expectedDoubleMultiplier, 1)
}
})
it('verifies interaction of Warmth synergy (+180% = 2.8x) and Fire Mastery double-dip ((2.63)^2 = 6.9169x)', () => {
// Warmth 20 hard points (+180%), Fire Mastery 20 (+163%)
const warmthPts = 20
const fmSlvl = 20
const baseDmg = calculateEnchantDamage(20) // 68.5 - 89.5
const buffedCastDmg = calculateEnchantDamage(20, { warmth: warmthPts }, fmSlvl)
// Warmth multiplier = 1 + 20*0.09 = 2.8
// FM multiplier = 1 + 1.63 = 2.63
// Total cast multiplier = 2.8 * 2.63 = 7.364
// Min: 68.5 * 7.364 = 504.434 -> 504.4
// Max: 89.5 * 7.364 = 659.078 -> 659.1
expect(buffedCastDmg.min).toBe(504.4)
expect(buffedCastDmg.max).toBe(659.1)
// Melee double dip: applies FM 2.63 again!
// Total multiplier = 2.8 * (2.63)^2 = 19.36732x!
const meleeHit = calculateEnchantAttackDamage({
enchantDamage: buffedCastDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: fmSlvl,
})
// Min: 504.4 * 2.63 = 1326.572 -> 1326.6
// Max: 659.1 * 2.63 = 1733.433 -> 1733.4
expect(meleeHit.min).toBe(1326.6)
expect(meleeHit.max).toBe(1733.4)
expect(meleeHit.average).toBe(1530.0)
// Ranged hit: 33% penalty and NO second FM
const rangedHit = calculateEnchantAttackDamage({
enchantDamage: buffedCastDmg,
isRanged: true,
isSorceress: true,
fireMasterySlvl: fmSlvl,
})
expect(rangedHit.min).toBe(166.5) // 504.4 * 0.33 = 166.452 -> 166.5
expect(rangedHit.max).toBe(217.5) // 659.1 * 0.33 = 217.503 -> 217.5
expect(rangedHit.average).toBe(192.0)
})
it('verifies extreme skill level scaling (slvl 60 Enchant + slvl 60 Fire Mastery) maintains numerical stability', () => {
const slvl = 60
const fmBonus = calculateFireMasteryBonus(60) // 30 + 59 * 7 = 443% -> 5.43x
expect(fmBonus).toBe(443)
const baseDmg = calculateEnchantDamage(60)
expect(baseDmg.min).toBeGreaterThan(0)
expect(baseDmg.max).toBeGreaterThan(baseDmg.min)
expect(Number.isFinite(baseDmg.min)).toBe(true)
const castDmg = calculateEnchantDamage(60, undefined, 60)
expect(castDmg.min).toBeCloseTo(baseDmg.min * 5.43, 0)
const meleeDmg = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 60,
})
// Double dip = base * (5.43)^2
expect(meleeDmg.min).toBeCloseTo(baseDmg.min * 5.43 * 5.43, 0)
expect(Number.isFinite(meleeDmg.average)).toBe(true)
})
})
// ==========================================================================
// Suite 3: Non-Sorceress Buff Recipient (Allies / Pets)
// ==========================================================================
describe('Suite 3: Non-Sorceress Buff Recipient (Allies / Pets)', () => {
it('verifies non-Sorceress melee ally receives single-multiplied buff damage without second Fire Mastery', () => {
// Sorceress with slvl 20 Enchant and slvl 20 Fire Mastery casts on an ally
const castDmg = calculateEnchantDamage(20, undefined, 20) // 180.2 - 235.4
// Ally (isSorceress: false, no Fire Mastery) strikes in melee
const allyMelee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: false,
})
// Damage must match the single-multiplied cast buff exactly:
expect(allyMelee.min).toBe(180.2)
expect(allyMelee.max).toBe(235.4)
expect(allyMelee.average).toBe(207.8)
// Contrast with Sorceress attacking:
const sorcMelee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
})
expect(sorcMelee.average).toBe(546.5)
expect(sorcMelee.average).toBeCloseTo(allyMelee.average * 2.63, 0)
})
it('verifies non-Sorceress ranged ally receives strictly 33% of single-multiplied buff damage', () => {
const castDmg = calculateEnchantDamage(20, undefined, 20) // 180.2 - 235.4
// Rogue mercenary or bow Amazon strikes at range
const allyRanged = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: true,
isSorceress: false,
})
expect(allyRanged.min).toBe(59.5) // 180.2 * 0.33 = 59.466 -> 59.5
expect(allyRanged.max).toBe(77.7) // 235.4 * 0.33 = 77.682 -> 77.7
expect(allyRanged.average).toBe(68.6)
})
it('verifies Party Sorceress 2 with lower Fire Mastery applies her own Fire Mastery upon melee strike', () => {
// Sorc 1 (slvl 20 Enchant, slvl 20 FM) casts on Sorc 2 (who has slvl 10 FM = +93% / 1.93x)
const castDmg = calculateEnchantDamage(20, undefined, 20) // 180.2 - 235.4
// Sorc 2 attacks in melee with her own FM slvl 10:
const sorc2Melee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 10,
})
// Min: 180.2 * 1.93 = 347.786 -> 347.8
// Max: 235.4 * 1.93 = 454.322 -> 454.3
expect(sorc2Melee.min).toBe(347.8)
expect(sorc2Melee.max).toBe(454.3)
expect(sorc2Melee.average).toBe(401.1)
// When Sorc 2 attacks at range, her FM is not applied, strictly 33% of cast damage:
const sorc2Ranged = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: true,
isSorceress: true,
fireMasterySlvl: 10,
})
expect(sorc2Ranged.min).toBe(59.5)
expect(sorc2Ranged.max).toBe(77.7)
})
it('verifies CombatWorld multi-player sandbox: Sorceress vs Barbarian ally dealing Enchant damage', () => {
const world: CombatWorld = createWorld(100, 100) // Player 0: Sorceress
const barbarianAlly = addPlayer(world, 105, 100) // Player 1: Barbarian
const castDmg = calculateEnchantDamage(20, undefined, 20) // 180.2 - 235.4
// Apply buff to both
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
world.player.isSorceress = true
world.player.fireMasterySlvl = 20
world.player.isRanged = false
applyEnchantBuff(barbarianAlly, 20, { casterFireMasterySlvl: 20 })
barbarianAlly.isSorceress = false
barbarianAlly.fireMasterySlvl = 0
barbarianAlly.isRanged = false
// Spawn 2 target dummies
const dummySorc: Monster = {
index: 0,
stats: { id: 'dummy_s', name: 'Dummy Sorc', hp: 2000, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 10 },
x: 120, y: 100, hp: 2000, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
const dummyBarb: Monster = {
index: 1,
stats: { id: 'dummy_b', name: 'Dummy Barb', hp: 2000, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 10 },
x: 105, y: 120, hp: 2000, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
world.monsters.push(dummySorc, dummyBarb)
// Tick player 0 (Sorceress melee attack)
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const sorcEvent = world.events.find(e => e.kind === 'monsterHit')
expect(sorcEvent).toBeDefined()
// Sorc damage = base (10) + double-dip fire average (546.5 -> 547) = 557
expect(sorcEvent!.amount).toBe(10 + Math.round(546.5))
// Now tick barbarian ally melee attack
world.events = []
// Let barbarian attack
barbarianAlly.cooldown = 0
// We simulate barbarian's tick via executeSUnitDmg directly or checking calculation
const barbHit = calculateEnchantAttackDamage({
enchantDamage: barbarianAlly.enchantDamage!,
isRanged: barbarianAlly.isRanged,
isSorceress: barbarianAlly.isSorceress,
fireMasterySlvl: barbarianAlly.fireMasterySlvl,
})
// Barbarian damage is single-dipped: 207.8 -> 208
expect(barbHit.average).toBe(207.8)
expect(sorcEvent!.amount! - 10).toBeGreaterThan(barbHit.average * 2.5)
})
})
// ==========================================================================
// Suite 4: Fire Immunity Preservation & Resistance Handling
// ==========================================================================
describe('Suite 4: Fire Immunity Preservation & Resistance Handling', () => {
it('strictly preserves fire immunity at 100%, 110%, and 150% Fire Resist (0 fire damage dealt)', () => {
const castDmg = calculateEnchantDamage(20, undefined, 20)
for (const res of [100, 110, 125, 150, 200]) {
// Melee Sorceress with slvl 20 Fire Mastery vs immune monster
const immuneMelee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: res,
})
expect(immuneMelee.min).toBe(0)
expect(immuneMelee.max).toBe(0)
expect(immuneMelee.average).toBe(0)
// Ranged Sorceress vs immune monster
const immuneRanged = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: true,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: res,
})
expect(immuneRanged.min).toBe(0)
expect(immuneRanged.max).toBe(0)
expect(immuneRanged.average).toBe(0)
// Extreme Fire Mastery slvl 40 (+303%) vs immune monster
const extremeFmMelee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 40,
targetFireResist: res,
})
expect(extremeFmMelee.min).toBe(0)
expect(extremeFmMelee.max).toBe(0)
expect(extremeFmMelee.average).toBe(0)
}
})
it('verifies combat pipeline executeSUnitDmg flags immuneToElem = true and elemDamage256 = 0 for res >= 100', () => {
const attacker = createCombatUnit('attacker_fm40', {
fireMasterySlvl: 40, // +303% FM!
hasEnchantState: true,
})
for (const res of [100, 110, 150]) {
const defender = createCombatUnit(`defender_res_${res}`, { fireResist: res })
const packet: SUnitDmgPacket = {
skillId: 52,
attackKind: 'melee',
isEnchant: true,
elemType: 'fire',
elemMin256: 50000,
elemMax256: 50000,
flatPhysMin256: 0,
flatPhysMax256: 0,
autoHit: true,
}
const outcome = executeSUnitDmg(attacker, defender, packet)
expect(outcome.hit).toBe(true)
expect(outcome.immuneToElem).toBe(true)
expect(outcome.elemDamage256).toBe(0)
// Physical damage is applied normally (weapon base 10)
expect(outcome.physDamage256).toBe(10 * FIXED_ONE)
expect(outcome.totalDamage).toBe(10)
}
})
it('verifies proportional mitigation at 50% and 0% Fire Resist without floor clamp violations', () => {
const castDmg = calculateEnchantDamage(20, undefined, 20)
// 0% Fire Resist: 100% damage unmitigated
const res0 = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 0,
})
expect(res0.min).toBe(473.9)
expect(res0.max).toBe(619.1)
expect(res0.average).toBe(546.5)
// 50% Fire Resist: exactly 50% reduction
const res50 = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 50,
})
expect(res50.min).toBe(Math.round(473.9 * 0.5 * 10) / 10) // 237.0
expect(res50.max).toBe(Math.round(619.1 * 0.5 * 10) / 10) // 309.6
expect(res50.average).toBe(Math.round(546.5 * 0.5 * 10) / 10) // 273.3
// 75% Fire Resist: exactly 75% reduction (25% remaining)
const res75 = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 75,
})
expect(res75.min).toBe(118.5)
expect(res75.max).toBe(154.8)
expect(res75.average).toBe(136.7)
// 99% Fire Resist: non-immune boundary, exactly 1% remaining
const res99 = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 99,
})
expect(res99.min).toBeGreaterThan(0)
expect(res99.average).toBe(Math.round(546.5 * 0.01 * 10) / 10) // 5.5
})
it('verifies computeEffectiveResistance 1.13c immunity breaking and -100% floor clamp rules', () => {
// 1. Natural immunity without Conviction / Lower Resist cannot be broken by passive pierce
const immuneRes = computeEffectiveResistance({
baseRes: 100,
passiveEnemyPierce: 50, // e.g. facets / gear
})
expect(immuneRes.isImmune).toBe(true)
expect(immuneRes.effectiveRes).toBe(100)
// 2. Negative resistance cannot go below authentic 1.13c -100% cap
const extremeNegRes = computeEffectiveResistance({
baseRes: -80,
convictionPierce: 50,
})
// -80 - 50 = -130, clamped to -100!
expect(extremeNegRes.effectiveRes).toBe(-100)
})
it('verifies CombatWorld sandbox tickCombat against immune monster (res=100) deals only physical damage', () => {
const world: CombatWorld = createWorld(100, 100)
const immuneMonster: Monster = {
index: 0,
stats: { id: 'fire_immune_pitlord', name: 'Pit Lord', hp: 500, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 50 },
x: 120, y: 100, hp: 500, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
;(immuneMonster as any).fireResist = 100 // Set fire resist to 100%
world.monsters.push(immuneMonster)
// Buff player with massive Enchant + Fire Mastery
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
world.player.isSorceress = true
world.player.fireMasterySlvl = 20
world.player.isRanged = false
// Player attacks
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const hitEvent = world.events.find(e => e.kind === 'monsterHit')
expect(hitEvent).toBeDefined()
// Because monster is fire immune, only baseOptions.playerDamage (10) is dealt!
expect(hitEvent!.amount).toBe(10)
expect(immuneMonster.hp).toBe(490)
})
it('ADVERSARIAL GAP CHECK: verifies whether tickPlayer handles monster.stats.resistances?.fire', () => {
const world: CombatWorld = createWorld(100, 100)
const monsterWithResistancesBag: Monster = {
index: 0,
stats: {
id: 'typed_res_monster',
name: 'Resistant Monster',
hp: 500,
damage: 0,
cooldownTicks: 25,
reach: 20,
aggroRadius: 0,
speed: 0,
xp: 50,
resistances: { fire: 100 },
resists: { fire: 100 },
},
x: 120, y: 100, hp: 500, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
world.monsters.push(monsterWithResistancesBag)
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
world.player.isSorceress = true
world.player.fireMasterySlvl = 20
world.player.isRanged = false
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const hitEvent = world.events.find(e => e.kind === 'monsterHit')
expect(hitEvent).toBeDefined()
// In combat.ts line 895: targetFireResist: (target as any).fireResist ?? (target.stats as any).fireResist
// Because target.stats.resistances?.fire is NOT inspected, targetFireResist resolves to undefined!
// This results in full unmitigated fire damage (557 instead of 10):
const actualDmg = hitEvent!.amount!
expect(actualDmg).toBe(557) // EMPIRICAL PROOF OF GAP: resistances bag ignored by combat.ts:895
})
it('proves empirically that Fire Mastery provides 0% enemy fire resistance pierce (unlike Cold Mastery)', () => {
// Monster with 50% fire resist
const castDmg = calculateEnchantDamage(20, undefined, 20)
// With FM slvl 20 (+163% damage bonus)
const withFm = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 50,
})
// Without FM (e.g. ally)
const withoutFm = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: false,
targetFireResist: 50,
})
// Unresisted baselines
const unresistedWithFm = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 0,
})
const unresistedWithoutFm = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: false,
targetFireResist: 0,
})
// The damage reduction ratio for both is EXACTLY (100 - 50)/100 = 0.5!
// This proves Fire Mastery does not pierce enemy resistance by even 1%!
expect(withFm.average / unresistedWithFm.average).toBeCloseTo(0.5, 2)
expect(withoutFm.average / unresistedWithoutFm.average).toBeCloseTo(0.5, 2)
})
})
// ==========================================================================
// Suite 5: Adversarial Boundary & Lifecycle Stress Tests
// ==========================================================================
describe('Suite 5: Adversarial Boundary & Lifecycle Stress Tests', () => {
it('verifies buff expiration countdown: damage drops to 0 fire bonus after enchantTicks reaches 0', () => {
const world: CombatWorld = createWorld(100, 100)
const monster: Monster = {
index: 0,
stats: { id: 'dummy', name: 'Dummy', hp: 2000, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 50 },
x: 120, y: 100, hp: 2000, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
world.monsters.push(monster)
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
world.player.isSorceress = true
world.player.fireMasterySlvl = 20
// Set ticks to exactly 2 ticks left
world.player.enchantTicks = 2
// Tick 1: buff still active (enchantTicks decreases to 1)
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
expect(world.events.find(e => e.kind === 'monsterHit')?.amount).toBe(10 + Math.round(546.5))
expect(world.player.enchantTicks).toBe(1)
// Tick 2: buff expires (enchantTicks decrements to 0, enchantDamage cleared)
world.player.cooldown = 0
world.events = []
tickCombat(world, { movement: { x: 0, y: 0 }, attack: false }, baseOptions, mockTerrain, [0, 100])
expect(world.player.enchantTicks).toBe(0)
expect(world.player.enchantDamage).toBeUndefined()
// Tick 3: player attacks with expired buff -> strictly physical damage only (10)
world.player.cooldown = 0
world.events = []
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
expect(world.events.find(e => e.kind === 'monsterHit')?.amount).toBe(10)
})
it('verifies Attack Rating bonus scaling across slvl 1..40 and non-positive inputs', () => {
expect(calculateEnchantAttackRating(1)).toBe(20)
expect(calculateEnchantAttackRating(10)).toBe(101)
expect(calculateEnchantAttackRating(20)).toBe(191)
expect(calculateEnchantAttackRating(30)).toBe(281)
expect(calculateEnchantAttackRating(40)).toBe(371)
expect(calculateEnchantAttackRating(0)).toBe(0)
expect(calculateEnchantAttackRating(-5)).toBe(0)
expect(calculateEnchantAttackRating(NaN)).toBe(0)
})
it('verifies Enchant duration scaling across slvl 1..40 and non-positive inputs', () => {
expect(calculateEnchantDuration(1)).toBe(3600) // 144s
expect(calculateEnchantDuration(10)).toBe(9000) // 360s
expect(calculateEnchantDuration(20)).toBe(15000) // 600s
expect(calculateEnchantDuration(30)).toBe(21000) // 840s
expect(calculateEnchantDuration(40)).toBe(27000) // 1080s
expect(calculateEnchantDuration(0)).toBe(0)
expect(calculateEnchantDuration(-1)).toBe(0)
expect(calculateEnchantDuration(NaN)).toBe(0)
})
it('verifies whiff event when attacking while no monster is within playerReach', () => {
const world: CombatWorld = createWorld(100, 100)
const distantMonster: Monster = {
index: 0,
stats: { id: 'far_monster', name: 'Far', hp: 100, damage: 0, cooldownTicks: 25, reach: 20, aggroRadius: 0, speed: 0, xp: 50 },
x: 300, y: 300, hp: 100, cooldown: 0, state: 'idle', facing: 0, hitFlash: 0, corpseTicks: 0,
}
world.monsters.push(distantMonster)
applyEnchantBuff(world.player, 20, { casterFireMasterySlvl: 20 })
tickCombat(world, { movement: { x: 0, y: 0 }, attack: true }, baseOptions, mockTerrain, [0, 100])
const whiffEvent = world.events.find(e => e.kind === 'playerHit' && e.text === 'whiff')
expect(whiffEvent).toBeDefined()
expect(whiffEvent!.amount).toBe(0)
expect(distantMonster.hp).toBe(100) // unaffected
})
})
})

View File

@ -0,0 +1,527 @@
/**
* Adversarial Empirical Stress Testing Suite for Milestone 4:
* Sorceress Fire Tree — Enchant (Skill 52) & Fire Mastery (Skill 61) Math & Mechanics
*
* 1.13c Ground Truth Sources:
* - Skills.txt (row 52 Enchant, row 61 Fire Mastery, row 37 Warmth)
* - D2Common.dll (HitShift 7 256-fixed-point, 5-band damage progression, ln12 duration)
* - D2Game.dll (Enchant buff attachment, ranged 33% penalty, Sorceress melee double-dip)
*
* Test Dimensions:
* 1. Monotonicity sweeps across slvl 1..99 for Enchant damage, AR bonus %, duration, and Fire Mastery %
* 2. Synergy scaling: Warmth synergy (+9%/lvl) linear scaling without overflow/underflow at syn 0, 20, 50, 99
* 3. Fire Mastery cast multiplier: (1 + calculateFireMasteryBonus(fmSlvl) / 100) base buff scaling & melee double-dip
* 4. Discrete values verification: exact 1.13c values at slvl 1, 10, 20
* 5. Extreme inputs: slvl 0, negative, and NaN inputs safely clamped
* 6. Differential testing against independent brute-force oracle (1000+ randomized iterations)
*/
import { describe, it, expect } from 'vitest'
import {
calculateEnchantDamage,
calculateEnchantAttackRating,
calculateEnchantDuration,
calculateFireMasteryBonus,
calculateEnchantAttackDamage,
} from '../../../src/game/skills.ts'
// ============================================================================
// Independent Brute-Force Oracles for Differential Testing
// ============================================================================
/** Independent Oracle for raw 5-band minimum fire damage */
function oracleRawMin(lvl: number): number {
const l = Number.isFinite(lvl) ? Math.max(1, Math.floor(lvl)) : 1
let min = 16
for (let i = 2; i <= l; i++) {
if (i <= 8) min += 3
else if (i <= 16) min += 7
else if (i <= 22) min += 11
else if (i <= 28) min += 15
else min += 19
}
return min
}
/** Independent Oracle for raw 5-band maximum fire damage */
function oracleRawMax(lvl: number): number {
const l = Number.isFinite(lvl) ? Math.max(1, Math.floor(lvl)) : 1
let max = 20
for (let i = 2; i <= l; i++) {
if (i <= 8) max += 5
else if (i <= 16) max += 9
else if (i <= 22) max += 13
else if (i <= 28) max += 17
else max += 21
}
return max
}
/** Independent Oracle for Attack Rating bonus % */
function oracleEnchantAR(slvl: number): number {
if (!Number.isFinite(slvl) || slvl <= 0) return 0
const lvl = Math.floor(slvl)
return 20 + (lvl - 1) * 9
}
/** Independent Oracle for Duration in frames */
function oracleEnchantDuration(slvl: number): number {
if (!Number.isFinite(slvl) || slvl <= 0) return 0
const lvl = Math.floor(slvl)
return 3600 + (lvl - 1) * 600
}
/** Independent Oracle for Fire Mastery bonus % */
function oracleFireMasteryBonus(slvl: number): number {
if (!Number.isFinite(slvl) || slvl <= 0) return 0
const lvl = Math.floor(slvl)
return 30 + (lvl - 1) * 7
}
/** Independent Oracle for complete Enchant damage */
function oracleEnchantDamage(slvl: number, warmth: number = 0, fireMastery: number = 0) {
const rawMin = oracleRawMin(slvl)
const rawMax = oracleRawMax(slvl)
const wPts = Number.isFinite(warmth) ? Math.max(0, warmth) : 0
const fmPts = Number.isFinite(fireMastery) ? Math.max(0, fireMastery) : 0
const warmthMul = 1.0 + (wPts * 9) / 100
const fmMul = 1.0 + oracleFireMasteryBonus(fmPts) / 100
const totalMul = warmthMul * fmMul
const min256 = Math.floor(rawMin * 128 * totalMul)
const max256 = Math.floor(rawMax * 128 * totalMul)
const min = Math.round((rawMin * 0.5 * totalMul) * 10) / 10
const max = Math.round((rawMax * 0.5 * totalMul) * 10) / 10
return { min, max, min256, max256 }
}
describe('Milestone 4 Math Adversarial Stress Suite (Enchant & Fire Mastery)', () => {
// ==========================================================================
// Suite 1: Monotonicity Sweeps (slvl 1..99)
// ==========================================================================
describe('Suite 1: Monotonicity Sweeps across slvl 1..99', () => {
it('Enchant fire damage strictly increases monotonically across slvl 1..99', () => {
let prev = calculateEnchantDamage(1)
expect(prev.min).toBe(8.0)
expect(prev.max).toBe(10.0)
expect(prev.min256).toBe(2048)
expect(prev.max256).toBe(2560)
for (let slvl = 2; slvl <= 99; slvl++) {
const curr = calculateEnchantDamage(slvl)
// Strict monotonicity
expect(curr.min).toBeGreaterThan(prev.min)
expect(curr.max).toBeGreaterThan(prev.max)
expect(curr.min256).toBeGreaterThan(prev.min256)
expect(curr.max256).toBeGreaterThan(prev.max256)
// Invariant: min <= max
expect(curr.min).toBeLessThanOrEqual(curr.max)
expect(curr.min256).toBeLessThanOrEqual(curr.max256)
prev = curr
}
})
it('Enchant Attack Rating bonus % strictly increases monotonically across slvl 1..99', () => {
let prev = calculateEnchantAttackRating(1)
expect(prev).toBe(20)
for (let slvl = 2; slvl <= 99; slvl++) {
const curr = calculateEnchantAttackRating(slvl)
// Strict linear increase of exactly +9% per level
expect(curr).toBe(prev + 9)
expect(curr).toBe(20 + (slvl - 1) * 9)
prev = curr
}
expect(calculateEnchantAttackRating(99)).toBe(902)
})
it('Enchant duration in frames strictly increases monotonically across slvl 1..99', () => {
let prev = calculateEnchantDuration(1)
expect(prev).toBe(3600) // 144 seconds
for (let slvl = 2; slvl <= 99; slvl++) {
const curr = calculateEnchantDuration(slvl)
// Strict linear increase of exactly +600 frames (24 seconds) per level
expect(curr).toBe(prev + 600)
expect(curr).toBe(3600 + (slvl - 1) * 600)
prev = curr
}
expect(calculateEnchantDuration(99)).toBe(62400) // 2496s = 41.6 minutes
})
it('Fire Mastery fire damage bonus % strictly increases monotonically across slvl 1..99', () => {
let prev = calculateFireMasteryBonus(1)
expect(prev).toBe(30) // +30%
for (let slvl = 2; slvl <= 99; slvl++) {
const curr = calculateFireMasteryBonus(slvl)
// Strict linear increase of exactly +7% per level
expect(curr).toBe(prev + 7)
expect(curr).toBe(30 + (slvl - 1) * 7)
prev = curr
}
expect(calculateFireMasteryBonus(99)).toBe(716) // +716%
})
})
// ==========================================================================
// Suite 2: Synergy Scaling & Linear Progression (Warmth +9%/lvl)
// ==========================================================================
describe('Suite 2: Synergy Scaling & Precision (Warmth +9%/lvl)', () => {
it('Warmth synergy scales linearly at slvl 1, 10, 20, 50, 99 with syn 0, 20, 50, 99', () => {
const testLevels = [1, 10, 20, 50, 99]
const testSynergies = [0, 20, 50, 99]
for (const slvl of testLevels) {
const base = calculateEnchantDamage(slvl, 0)
for (const syn of testSynergies) {
const withSyn = calculateEnchantDamage(slvl, syn)
const expectedMultiplier = 1.0 + (syn * 9) / 100
// Check finiteness and positivity
expect(Number.isFinite(withSyn.min)).toBe(true)
expect(Number.isFinite(withSyn.max)).toBe(true)
expect(Number.isFinite(withSyn.min256)).toBe(true)
expect(Number.isFinite(withSyn.max256)).toBe(true)
expect(withSyn.min).toBeGreaterThan(0)
expect(withSyn.max).toBeGreaterThanOrEqual(withSyn.min)
// Exact 256-fixed-point integer parity
const rawMin = oracleRawMin(slvl)
const rawMax = oracleRawMax(slvl)
expect(withSyn.min256).toBe(Math.floor(rawMin * 128 * expectedMultiplier))
expect(withSyn.max256).toBe(Math.floor(rawMax * 128 * expectedMultiplier))
// Floating point 0.5 step parity rounded to 1 decimal
expect(withSyn.min).toBe(Math.round((rawMin * 0.5 * expectedMultiplier) * 10) / 10)
expect(withSyn.max).toBe(Math.round((rawMax * 0.5 * expectedMultiplier) * 10) / 10)
}
}
})
it('verifies exact step increments per Warmth hard point across syn 0..20', () => {
const slvl = 20
const rawMin = 137
const rawMax = 179
let prev = calculateEnchantDamage(slvl, 0)
for (let w = 1; w <= 20; w++) {
const curr = calculateEnchantDamage(slvl, w)
// Strict monotonicity per point of synergy
expect(curr.min256).toBeGreaterThan(prev.min256)
expect(curr.max256).toBeGreaterThan(prev.max256)
// Multiplier check: 1 + 0.09 * w
const mul = 1 + 0.09 * w
expect(curr.min256).toBe(Math.floor(rawMin * 128 * mul))
expect(curr.max256).toBe(Math.floor(rawMax * 128 * mul))
prev = curr
}
// At Warmth 20: +180% synergy bonus (2.8x base)
const maxWarmth = calculateEnchantDamage(20, 20)
expect(maxWarmth.min256).toBe(Math.floor(137 * 128 * 2.8)) // 49100
expect(maxWarmth.max256).toBe(Math.floor(179 * 128 * 2.8)) // 64153
})
it('handles negative or invalid synergy values by safe clamping to 0', () => {
const base = calculateEnchantDamage(20, 0)
const negSyn = calculateEnchantDamage(20, -10)
expect(negSyn).toEqual(base)
const nanSyn = calculateEnchantDamage(20, NaN)
expect(nanSyn).toEqual(base)
})
})
// ==========================================================================
// Suite 3: Fire Mastery Cast Multiplier & Interaction
// ==========================================================================
describe('Suite 3: Fire Mastery Cast Multiplier & Combat Integration', () => {
it('scales Enchant base buff damage by (1 + calculateFireMasteryBonus(fmSlvl) / 100) at cast time', () => {
const slvls = [1, 10, 20, 50, 99]
const fmLvls = [0, 1, 10, 20, 50, 99]
for (const slvl of slvls) {
const base = calculateEnchantDamage(slvl)
for (const fmSlvl of fmLvls) {
const fmBonus = calculateFireMasteryBonus(fmSlvl)
const fmMul = 1.0 + fmBonus / 100
const castDmg = calculateEnchantDamage(slvl, 0, fmSlvl)
const expectedMin = Math.round(base.min * fmMul * 10) / 10
const expectedMax = Math.round(base.max * fmMul * 10) / 10
expect(castDmg.min).toBe(expectedMin)
expect(castDmg.max).toBe(expectedMax)
}
}
})
it('supports synergies object argument { warmth, fireMastery } and positional arguments equivalently', () => {
const slvl = 20
const warmth = 20
const fmSlvl = 20
const viaObj = calculateEnchantDamage(slvl, { warmth, fireMastery: fmSlvl })
const viaPos = calculateEnchantDamage(slvl, warmth, fmSlvl)
expect(viaObj).toEqual(viaPos)
expect(viaObj.min).toBe(504.4)
expect(viaObj.max).toBe(659.1)
})
it('verifies compound scaling: Warmth synergy combined with Fire Mastery multiplier', () => {
// slvl 20 Enchant: rawMin=137, rawMax=179
// Warmth 20 (+180% -> 2.80x)
// Fire Mastery 20 (+163% -> 2.63x)
// Total multiplier = 2.80 * 2.63 = 7.364x
const buffed = calculateEnchantDamage(20, { warmth: 20, fireMastery: 20 })
const expectedMin256 = Math.floor(137 * 128 * 2.8 * 2.63)
const expectedMax256 = Math.floor(179 * 128 * 2.8 * 2.63)
expect(buffed.min256).toBe(expectedMin256)
expect(buffed.max256).toBe(expectedMax256)
const expectedMin = Math.round((137 * 0.5 * 2.8 * 2.63) * 10) / 10
const expectedMax = Math.round((179 * 0.5 * 2.8 * 2.63) * 10) / 10
expect(buffed.min).toBe(expectedMin)
expect(buffed.max).toBe(expectedMax)
})
it('verifies Sorceress melee double-dip vs ranged 33% penalty via calculateEnchantAttackDamage', () => {
const castDmg = calculateEnchantDamage(20, 0, 20) // min: 180.2, max: 235.4
const fmBonus = calculateFireMasteryBonus(20) // 163% -> 2.63x
// 1. Sorceress Melee: Double-Dip multiplies fire damage again!
const meleeSorc = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
})
const expectedMeleeMin = Math.round(castDmg.min * (1 + fmBonus / 100) * 10) / 10 // 473.9
const expectedMeleeMax = Math.round(castDmg.max * (1 + fmBonus / 100) * 10) / 10 // 619.1
expect(meleeSorc.min).toBe(expectedMeleeMin)
expect(meleeSorc.max).toBe(expectedMeleeMax)
// 2. Ranged attack: 33% penalty, zero second Fire Mastery multiplier
const rangedSorc = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: true,
isSorceress: true,
fireMasterySlvl: 20,
})
const expectedRangedMin = Math.round(castDmg.min * 0.33 * 10) / 10 // 59.5
const expectedRangedMax = Math.round(castDmg.max * 0.33 * 10) / 10 // 77.7
expect(rangedSorc.min).toBe(expectedRangedMin)
expect(rangedSorc.max).toBe(expectedRangedMax)
// 3. Non-Sorceress ally in melee: 100% buff damage, no secondary multiplier
const allyMelee = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: false,
})
expect(allyMelee.min).toBe(castDmg.min)
expect(allyMelee.max).toBe(castDmg.max)
// 4. Fire immunity barrier: enemies with >= 100% resistance take 0 damage
const immune = calculateEnchantAttackDamage({
enchantDamage: castDmg,
isRanged: false,
isSorceress: true,
fireMasterySlvl: 20,
targetFireResist: 100,
})
expect(immune.min).toBe(0)
expect(immune.max).toBe(0)
expect(immune.average).toBe(0)
})
})
// ==========================================================================
// Suite 4: Exact 1.13c Discrete Values Verification (slvl 1, 10, 20)
// ==========================================================================
describe('Suite 4: Exact 1.13c Discrete Values Verification (slvl 1, 10, 20)', () => {
it('verifies exact discrete values at slvl 1', () => {
// slvl 1: 8.0-10.0 fire dmg, +20% AR, 3600 frames duration (144s), +30% Fire Mastery
const dmg = calculateEnchantDamage(1)
expect(dmg.min).toBe(8.0)
expect(dmg.max).toBe(10.0)
expect(dmg.min256).toBe(2048)
expect(dmg.max256).toBe(2560)
expect(calculateEnchantAttackRating(1)).toBe(20)
expect(calculateEnchantDuration(1)).toBe(3600)
expect(calculateFireMasteryBonus(1)).toBe(30)
})
it('verifies exact discrete values at slvl 10', () => {
// slvl 10: 25.5-36.5 fire dmg, +101% AR, 9000 frames duration (360s), +93% Fire Mastery
const dmg = calculateEnchantDamage(10)
expect(dmg.min).toBe(25.5)
expect(dmg.max).toBe(36.5)
expect(dmg.min256).toBe(6528)
expect(dmg.max256).toBe(9344)
expect(calculateEnchantAttackRating(10)).toBe(101)
expect(calculateEnchantDuration(10)).toBe(9000)
expect(calculateFireMasteryBonus(10)).toBe(93)
})
it('verifies exact discrete values at slvl 20', () => {
// slvl 20: 68.5-89.5 fire dmg, +191% AR, 15000 frames duration (600s), +163% Fire Mastery
const dmg = calculateEnchantDamage(20)
expect(dmg.min).toBe(68.5)
expect(dmg.max).toBe(89.5)
expect(dmg.min256).toBe(17536)
expect(dmg.max256).toBe(22912)
expect(calculateEnchantAttackRating(20)).toBe(191)
expect(calculateEnchantDuration(20)).toBe(15000)
expect(calculateFireMasteryBonus(20)).toBe(163)
})
it('verifies 5-band breakpoint transition step deltas across slvl 2..99', () => {
for (let lvl = 2; lvl <= 99; lvl++) {
const prev = calculateEnchantDamage(lvl - 1)
const curr = calculateEnchantDamage(lvl)
const dMin256 = curr.min256 - prev.min256
const dMax256 = curr.max256 - prev.max256
let expDMin = 3 * 128
let expDMax = 5 * 128
if (lvl >= 9 && lvl <= 16) {
expDMin = 7 * 128
expDMax = 9 * 128
} else if (lvl >= 17 && lvl <= 22) {
expDMin = 11 * 128
expDMax = 13 * 128
} else if (lvl >= 23 && lvl <= 28) {
expDMin = 15 * 128
expDMax = 17 * 128
} else if (lvl >= 29) {
expDMin = 19 * 128
expDMax = 21 * 128
}
expect(dMin256).toBe(expDMin)
expect(dMax256).toBe(expDMax)
}
})
})
// ==========================================================================
// Suite 5: Extreme Inputs & Boundary Values Clamping
// ==========================================================================
describe('Suite 5: Extreme Inputs & Boundary Values Clamping', () => {
it('safely clamps slvl 0 to valid baselines', () => {
// Enchant damage clamps slvl <= 0 to slvl 1
const dmg0 = calculateEnchantDamage(0)
const dmg1 = calculateEnchantDamage(1)
expect(dmg0).toEqual(dmg1)
// AR, Duration, and Fire Mastery clamp slvl <= 0 to 0
expect(calculateEnchantAttackRating(0)).toBe(0)
expect(calculateEnchantDuration(0)).toBe(0)
expect(calculateFireMasteryBonus(0)).toBe(0)
})
it('safely clamps negative slvl inputs (-1, -10, -99)', () => {
const dmgNeg = calculateEnchantDamage(-10)
const dmg1 = calculateEnchantDamage(1)
expect(dmgNeg).toEqual(dmg1)
expect(calculateEnchantAttackRating(-5)).toBe(0)
expect(calculateEnchantDuration(-50)).toBe(0)
expect(calculateFireMasteryBonus(-99)).toBe(0)
})
it('safely handles NaN and non-finite inputs', () => {
const dmgNaN = calculateEnchantDamage(NaN)
const dmg1 = calculateEnchantDamage(1)
expect(dmgNaN).toEqual(dmg1)
expect(calculateEnchantAttackRating(NaN)).toBe(0)
expect(calculateEnchantDuration(NaN)).toBe(0)
expect(calculateFireMasteryBonus(NaN)).toBe(0)
expect(calculateEnchantAttackRating(Infinity)).toBe(0)
expect(calculateEnchantDuration(-Infinity)).toBe(0)
expect(calculateFireMasteryBonus(Infinity)).toBe(0)
})
it('correctly truncates / floors floating-point skill levels', () => {
expect(calculateEnchantAttackRating(10.9)).toBe(calculateEnchantAttackRating(10))
expect(calculateEnchantDuration(15.7)).toBe(calculateEnchantDuration(15))
expect(calculateFireMasteryBonus(20.3)).toBe(calculateFireMasteryBonus(20))
expect(calculateEnchantDamage(20.8)).toEqual(calculateEnchantDamage(20))
})
})
// ==========================================================================
// Suite 6: Differential Testing against Brute-Force Oracle
// ==========================================================================
describe('Suite 6: Differential Testing against Brute-Force Oracle (1000+ Iterations)', () => {
it('matches independent brute-force oracle across 1000 randomized (slvl, warmth, fireMastery) inputs', () => {
// Deterministic pseudorandom generator (LCG)
let seed = 42
function rand() {
seed = (seed * 1664525 + 1013904223) >>> 0
return seed / 4294967296
}
const ITERATIONS = 1000
for (let i = 0; i < ITERATIONS; i++) {
// Biased input generation: mix of boundary values and uniform randoms
let slvl: number
let warmth: number
let fireMastery: number
if (i < 50) {
// Boundary testing phase
slvl = (i % 30) + 1
warmth = i % 25
fireMastery = i % 25
} else if (i < 100) {
// High level and extreme synergy phase
slvl = 80 + Math.floor(rand() * 20)
warmth = Math.floor(rand() * 100)
fireMastery = Math.floor(rand() * 100)
} else {
// General distribution 1..99
slvl = Math.floor(rand() * 99) + 1
warmth = Math.floor(rand() * 50)
fireMastery = Math.floor(rand() * 50)
}
const actual = calculateEnchantDamage(slvl, warmth, fireMastery)
const expected = oracleEnchantDamage(slvl, warmth, fireMastery)
// Strict differential checks against oracle
expect(actual.min).toBe(expected.min)
expect(actual.max).toBe(expected.max)
expect(actual.min256).toBe(expected.min256)
expect(actual.max256).toBe(expected.max256)
// AR and Duration oracle differential checks
expect(calculateEnchantAttackRating(slvl)).toBe(oracleEnchantAR(slvl))
expect(calculateEnchantDuration(slvl)).toBe(oracleEnchantDuration(slvl))
expect(calculateFireMasteryBonus(fireMastery)).toBe(oracleFireMasteryBonus(fireMastery))
}
})
})
})