diablo2-web/tests/skills/ass/adv-ass-martial-stress.test.ts

843 lines
36 KiB
TypeScript

/**
* Adversarial Stress & Ground Truth Parity Test Suite — Assassin Martial Arts (10 skills)
*
* Validates 1.13c Diablo II mechanics:
* 1. Charge orb progression (1..3 tiers, 375-frame duration, refresh on hit, timeout expiry).
* 2. Multi-skill charge coexistence (simultaneous Tiger, Cobra, and Phoenix charges).
* 3. Finisher release mechanics:
* - Simultaneous full discharge of all active charge-ups.
* - Elemental charge-ups release cumulative tiers (FoF, CoT, BoI).
* - Phoenix Strike releases single targeted tier (1: Meteor, 2: Chain Lightning, 3: Chaos Ice).
* 4. Dragon Talon kick count progression: 1 + floor(slvl / 6).
* 5. Dragon Claw dual-claw equipment validation and 2-hit strike.
* 6. Dragon Tail physical-to-fire explosion conversion: 50 + 10 * (slvl - 1)%.
* 7. Dragon Flight teleport kick with 4-frame NextDelay and +100..+575% ED.
*
* Gitea Milestone: M27 (Issue #474)
*/
import { describe, expect, it } from 'vitest'
import {
ASSASSIN_MARTIAL_SKILL_IDS,
MAX_CHARGES_PER_SKILL,
CHARGE_DURATION_FRAMES,
ChargeOrbManager,
resolveFinisherDischarge,
canCastDragonClaw,
evaluateTigerStrikeStats,
evaluateDragonTalonStats,
evaluateFistsOfFireStats,
evaluateDragonClawStats,
evaluateCobraStrikeStats,
evaluateClawsOfThunderStats,
evaluateDragonTailStats,
evaluateBladesOfIceStats,
evaluateDragonFlightStats,
evaluatePhoenixStrikeStats,
} from '../../../src/game/skills/assassin-martial-arts.ts'
describe('Assassin Martial Arts Ground Truth & Adversarial Suite (Milestone M27)', () => {
describe('1. Charge Orb Progression & Expiration Mechanics', () => {
it('accumulates charges from 1 to 3 and caps at 3', () => {
const manager = new ChargeOrbManager()
const skillId = ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE
// Hit 1 at frame 10
const c1 = manager.addCharge(skillId, 20, 10)
expect(c1.previousCount).toBe(0)
expect(c1.currentCount).toBe(1)
expect(manager.getChargeLevel(skillId, 10)).toBe(1)
// Hit 2 at frame 30
const c2 = manager.addCharge(skillId, 20, 30)
expect(c2.previousCount).toBe(1)
expect(c2.currentCount).toBe(2)
expect(manager.getChargeLevel(skillId, 30)).toBe(2)
// Hit 3 at frame 50
const c3 = manager.addCharge(skillId, 20, 50)
expect(c3.previousCount).toBe(2)
expect(c3.currentCount).toBe(3)
expect(manager.getChargeLevel(skillId, 50)).toBe(3)
// Hit 4 at frame 70 -> stays at 3, but refreshes duration
const c4 = manager.addCharge(skillId, 20, 70)
expect(c4.previousCount).toBe(3)
expect(c4.currentCount).toBe(3)
expect(manager.getChargeLevel(skillId, 70)).toBe(3)
})
it('enforces 375-frame (15.0s) duration expiration', () => {
const manager = new ChargeOrbManager()
const skillId = ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE
// Add charge at frame 100 -> expires at 100 + 375 = 475
manager.addCharge(skillId, 20, 100)
// Valid within duration window
expect(manager.getChargeLevel(skillId, 200)).toBe(1)
expect(manager.getChargeLevel(skillId, 474)).toBe(1)
// Expired at frame 475
expect(manager.getChargeLevel(skillId, 475)).toBe(0)
expect(manager.getActiveCharges(475)).toHaveLength(0)
})
it('refreshes expiration when a charge is added before timeout', () => {
const manager = new ChargeOrbManager()
const skillId = ASSASSIN_MARTIAL_SKILL_IDS.CLAWS_OF_THUNDER
// Charge 1 at frame 0 -> expires at 375
manager.addCharge(skillId, 20, 0)
expect(manager.getChargeLevel(skillId, 300)).toBe(1)
// Charge 2 at frame 350 -> expires at 350 + 375 = 725
manager.addCharge(skillId, 20, 350)
expect(manager.getChargeLevel(skillId, 350)).toBe(2)
// Frame 400 (> 375 original expiry) is still active!
expect(manager.getChargeLevel(skillId, 400)).toBe(2)
expect(manager.getChargeLevel(skillId, 724)).toBe(2)
// Frame 725 is expired
expect(manager.getChargeLevel(skillId, 725)).toBe(0)
})
it('supports simultaneous multi-skill charge orb coexistence', () => {
const manager = new ChargeOrbManager()
// Charge Tiger Strike to 3
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 0)
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 10)
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 20)
// Charge Cobra Strike to 2
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 20, 25)
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 20, 35)
// Charge Phoenix Strike to 1
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE, 20, 40)
const active = manager.getActiveCharges(50)
expect(active).toHaveLength(3)
expect(manager.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 50)).toBe(3)
expect(manager.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 50)).toBe(2)
expect(manager.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE, 50)).toBe(1)
})
})
describe('2. Finisher Release & Discharge Resolution Engine', () => {
it('discharges all active charges and clears container on finisher impact', () => {
const manager = new ChargeOrbManager()
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 0)
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 10)
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 20) // 3 charges
manager.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 20, 30) // 1 charge
const discharged = manager.dischargeAll(50)
expect(discharged).toHaveLength(2)
// Manager is now completely empty
expect(manager.getActiveCharges(50)).toHaveLength(0)
expect(manager.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 50)).toBe(0)
expect(manager.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 50)).toBe(0)
})
it('resolves Tiger Strike 3-tier Enhanced Damage bonus (+1440% at slvl 20 Tier 3)', () => {
// Tier 1: 480% ED
const p1 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, count: 1, slvl: 20, expiresAtFrame: 1000 }])
expect(p1[0]?.enhancedDamagePct).toBe(480)
// Tier 2: 960% ED
const p2 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, count: 2, slvl: 20, expiresAtFrame: 1000 }])
expect(p2[0]?.enhancedDamagePct).toBe(960)
// Tier 3: 1440% ED
const p3 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, count: 3, slvl: 20, expiresAtFrame: 1000 }])
expect(p3[0]?.enhancedDamagePct).toBe(1440)
})
it('resolves Cobra Strike Life and Mana steal progression', () => {
// Tier 1: Life Steal only (135% at slvl 20)
const p1 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, count: 1, slvl: 20, expiresAtFrame: 1000 }])
expect(p1[0]?.lifeStealPct).toBe(135)
expect(p1[0]?.manaStealPct).toBe(0)
// Tier 2: Life Steal (135%) + Mana Steal (135%)
const p2 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, count: 2, slvl: 20, expiresAtFrame: 1000 }])
expect(p2[0]?.lifeStealPct).toBe(135)
expect(p2[0]?.manaStealPct).toBe(135)
// Tier 3: 2x Life Steal (270%) + 2x Mana Steal (270%)
const p3 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, count: 3, slvl: 20, expiresAtFrame: 1000 }])
expect(p3[0]?.lifeStealPct).toBe(270)
expect(p3[0]?.manaStealPct).toBe(270)
})
it('resolves Claws of Thunder cumulative elemental tiers (Tier 3 releases Nova + 4 Bolts)', () => {
// Tier 1: direct damage only
const p1 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.CLAWS_OF_THUNDER, count: 1, slvl: 20, expiresAtFrame: 1000 }])
expect(p1[0]?.ltngDamage?.hasNova).toBe(false)
expect(p1[0]?.ltngDamage?.chargedBoltsCount).toBeUndefined()
// Tier 2: direct damage + Nova
const p2 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.CLAWS_OF_THUNDER, count: 2, slvl: 20, expiresAtFrame: 1000 }])
expect(p2[0]?.ltngDamage?.hasNova).toBe(true)
expect(p2[0]?.ltngDamage?.chargedBoltsCount).toBeUndefined()
// Tier 3: direct damage + Nova + 4 Charged Bolts
const p3 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.CLAWS_OF_THUNDER, count: 3, slvl: 20, expiresAtFrame: 1000 }])
expect(p3[0]?.ltngDamage?.hasNova).toBe(true)
expect(p3[0]?.ltngDamage?.chargedBoltsCount).toBe(4)
})
it('resolves Phoenix Strike single-tier payload selection', () => {
// Charge 1: Meteor
const p1 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE, count: 1, slvl: 20, expiresAtFrame: 1000 }])
expect(p1[0]?.phoenixPayload?.tier).toBe(1)
expect(p1[0]?.phoenixPayload?.type).toBe('meteor')
expect(p1[0]?.phoenixPayload?.meteorDamage).toBeDefined()
expect(p1[0]?.phoenixPayload?.chainLightningDamage).toBeUndefined()
expect(p1[0]?.phoenixPayload?.chaosIceDamage).toBeUndefined()
// Charge 2: Chain Lightning
const p2 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE, count: 2, slvl: 20, expiresAtFrame: 1000 }])
expect(p2[0]?.phoenixPayload?.tier).toBe(2)
expect(p2[0]?.phoenixPayload?.type).toBe('chain_lightning')
expect(p2[0]?.phoenixPayload?.chainLightningDamage?.maxTargets).toBe(10)
expect(p2[0]?.phoenixPayload?.meteorDamage).toBeUndefined()
// Charge 3: Chaos Ice Orb
const p3 = resolveFinisherDischarge([{ skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE, count: 3, slvl: 20, expiresAtFrame: 1000 }])
expect(p3[0]?.phoenixPayload?.tier).toBe(3)
expect(p3[0]?.phoenixPayload?.type).toBe('chaos_ice')
expect(p3[0]?.phoenixPayload?.chaosIceDamage?.boltsCount).toBe(16)
expect(p3[0]?.phoenixPayload?.chaosIceDamage?.freezeFrames).toBeGreaterThan(0)
})
})
describe('3. Dragon Talon Multi-Kick Finisher', () => {
it('evaluates kick count scaling: 1 + floor(slvl / 6)', () => {
expect(evaluateDragonTalonStats(1).kickCount).toBe(1)
expect(evaluateDragonTalonStats(5).kickCount).toBe(1)
expect(evaluateDragonTalonStats(6).kickCount).toBe(2)
expect(evaluateDragonTalonStats(11).kickCount).toBe(2)
expect(evaluateDragonTalonStats(12).kickCount).toBe(3)
expect(evaluateDragonTalonStats(18).kickCount).toBe(4)
expect(evaluateDragonTalonStats(24).kickCount).toBe(5)
expect(evaluateDragonTalonStats(30).kickCount).toBe(6)
})
it('evaluates Attack Rating progression: 20 + 35 * (slvl - 1)%', () => {
expect(evaluateDragonTalonStats(1).attackRatingBonusPct).toBe(20)
expect(evaluateDragonTalonStats(10).attackRatingBonusPct).toBe(335)
expect(evaluateDragonTalonStats(20).attackRatingBonusPct).toBe(685)
})
})
describe('4. Dragon Claw Dual-Claw Finisher', () => {
it('enforces dual-claw equipment requirement', () => {
expect(canCastDragonClaw(false, false)).toBe(false)
expect(canCastDragonClaw(true, false)).toBe(false)
expect(canCastDragonClaw(false, true)).toBe(false)
expect(canCastDragonClaw(true, true)).toBe(true)
})
it('evaluates 2-strike count, ED% and Claw Mastery synergy', () => {
const s0 = evaluateDragonClawStats(20, 0)
expect(s0.strikesCount).toBe(2)
// 50 + 5 * 19 = 145% ED
expect(s0.enhancedDamagePct).toBe(145)
expect(s0.attackRatingBonusPct).toBe(515) // 40 + 25 * 19 = 515%
// With Claw Mastery blvl 20: +80% ED (4% per blvl)
const s20 = evaluateDragonClawStats(20, 20)
expect(s20.enhancedDamagePct).toBe(145 + 80)
})
})
describe('5. Dragon Tail Explosive Kick Finisher', () => {
it('evaluates physical-to-fire conversion: 50 + 10 * (slvl - 1)%', () => {
expect(evaluateDragonTailStats(1).fireExplosionPct).toBe(50)
expect(evaluateDragonTailStats(10).fireExplosionPct).toBe(140)
expect(evaluateDragonTailStats(20).fireExplosionPct).toBe(240)
})
it('applies 6 subtiles (4 yards) explosion radius', () => {
expect(evaluateDragonTailStats(20).radiusSubtiles).toBe(6)
})
})
describe('6. Dragon Flight Teleport Kick Finisher', () => {
it('evaluates teleport kick ED progression: 100 + 25 * (slvl - 1)% and NextDelay', () => {
const df1 = evaluateDragonFlightStats(1)
expect(df1.enhancedDamagePct).toBe(100)
expect(df1.attackRatingBonusPct).toBe(60)
expect(df1.nextDelayFrames).toBe(4)
const df20 = evaluateDragonFlightStats(20)
expect(df20.enhancedDamagePct).toBe(575) // 100 + 25 * 19 = 575%
expect(df20.attackRatingBonusPct).toBe(535) // 60 + 25 * 19 = 535%
expect(df20.nextDelayFrames).toBe(4)
expect(df20.manaCost).toBe(15)
})
})
describe('7. Challenger 1 Runtime Execution & Differential Stress Suite (executeSkill113c / evaluateSkill113c)', () => {
it('Dimension 1: Missed charge-up attacks (Tiger, Cobra, FoF, CoT, BoI, Phoenix) do NOT grant or advance charges', async () => {
const { getSharedDataRegistry } = await import('../../../src/game/engine/data-registry.ts')
const { FIXED_ONE, UnitStatList } = await import('../../../src/game/engine/stat-list.ts')
const { StateBus } = await import('../../../src/game/engine/state-bus.ts')
const { MissileEngine } = await import('../../../src/game/engine/missile-engine.ts')
const { AuraScanner } = await import('../../../src/game/engine/aura-scanner.ts')
const { SummonManager } = await import('../../../src/game/engine/summon-manager.ts')
const { evaluateSkill113c, executeSkill113c } = await import('../../../src/game/skills/registry.ts')
const registry = await getSharedDataRegistry()
const chargeUpSkills = [
{ id: 254, state: 'progressive_damage' },
{ id: 259, state: 'progressive_fire' },
{ id: 265, state: 'progressive_steal' },
{ id: 269, state: 'progressive_lightning' },
{ id: 274, state: 'progressive_cold' },
{ id: 280, state: 'progressive_other' },
]
for (const { id, state } of chargeUpSkills) {
const skill = registry.getSkillById(id)!
const casterStats = new UnitStatList()
casterStats.setBaseStat('level', 80)
casterStats.setBaseStat('tohit', 1) // 5% minimum hit chance -> roll=25 misses!
casterStats.setBaseSkillLevel(id, 20)
const casterBus = new StateBus(casterStats, registry)
const caster = {
id: 'caster_ass',
name: 'Assassin',
statList: casterStats,
stateBus: casterBus,
weaponMinPhys: 50,
weaponMaxPhys: 50,
}
const targetStats = new UnitStatList()
targetStats.setBaseStat('level', 80)
targetStats.setBaseStat('armorclass', 100000)
targetStats.setBaseStat('maxhp', 100000 * FIXED_ONE)
targetStats.setHp256(100000 * FIXED_ONE)
const target = {
id: 'target_mon',
name: 'Monster',
statList: targetStats,
stateBus: new StateBus(targetStats, registry),
}
const evalResult = evaluateSkill113c({ registry, skill, slvl: 20, blvl: 20, statList: casterStats })
const missRes = executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 100,
targetX: 10,
targetY: 10,
})
expect(missRes.totalDamageDealt).toBe(0)
expect(casterBus.getState(state)).toBeUndefined()
// Now give caster high AR so attacks connect: 1 -> 2 -> 3 -> 3 (capped at 3) and refresh 375-frame timer
casterStats.setBaseStat('tohit', 100000)
targetStats.setBaseStat('armorclass', 10)
for (const [stepIdx, tick, expectedCharge] of [
[1, 100, 1],
[2, 200, 2],
[3, 300, 3],
[4, 400, 3],
] as const) {
executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: tick,
targetX: 10,
targetY: 10,
})
const activeSt = casterBus.getState(state)
expect(activeSt, `Skill ${id} step ${stepIdx}`).toBeDefined()
expect(activeSt?.stats?.charge).toBe(expectedCharge)
expect(activeSt?.expireFrame).toBe(tick + 375)
}
// Advance stateBus past 400 + 375 = 775 frames -> charge expires
casterBus.tick(774)
expect(casterBus.getState(state)).toBeDefined()
casterBus.tick(775)
expect(casterBus.getState(state)).toBeUndefined()
}
})
it('Dimension 2: Finishers on miss do NOT discharge charges; on first connecting hit, all charges discharge and reset to 0', async () => {
const { getSharedDataRegistry } = await import('../../../src/game/engine/data-registry.ts')
const { FIXED_ONE, UnitStatList } = await import('../../../src/game/engine/stat-list.ts')
const { StateBus } = await import('../../../src/game/engine/state-bus.ts')
const { MissileEngine } = await import('../../../src/game/engine/missile-engine.ts')
const { AuraScanner } = await import('../../../src/game/engine/aura-scanner.ts')
const { SummonManager } = await import('../../../src/game/engine/summon-manager.ts')
const { evaluateSkill113c, executeSkill113c } = await import('../../../src/game/skills/registry.ts')
const registry = await getSharedDataRegistry()
const finisherIds = [255, 260, 270, 275, 0]
for (const finId of finisherIds) {
const skill = registry.getSkillById(finId)!
const casterStats = new UnitStatList()
casterStats.setBaseStat('level', 80)
casterStats.setBaseStat('tohit', 1) // Force miss first
casterStats.setHp256(500 * FIXED_ONE)
casterStats.setMana256(500 * FIXED_ONE)
if (finId > 0) casterStats.setBaseSkillLevel(finId, 12)
const casterBus = new StateBus(casterStats, registry)
// Pre-load Charge 3 Tiger Strike + Charge 3 Cobra Strike + Charge 2 Phoenix Strike
casterBus.applyState({
stateNameOrId: 'progressive_damage',
sourceUnitId: 'caster',
sourceSkillId: 254,
slvl: 20,
durationFrames: 375,
currentFrame: 10,
stats: { charge: 3, progressive_charges: 3 },
})
casterBus.applyState({
stateNameOrId: 'progressive_steal',
sourceUnitId: 'caster',
sourceSkillId: 265,
slvl: 20,
durationFrames: 375,
currentFrame: 10,
stats: { charge: 3, progressive_charges: 3 },
})
casterBus.applyState({
stateNameOrId: 'progressive_other',
sourceUnitId: 'caster',
sourceSkillId: 280,
slvl: 20,
durationFrames: 375,
currentFrame: 10,
stats: { charge: 2, progressive_charges: 2 },
})
const caster = {
id: 'caster',
name: 'Assassin',
statList: casterStats,
stateBus: casterBus,
isDualClaw: true,
weaponMinPhys: 100,
weaponMaxPhys: 100,
}
const targetStats = new UnitStatList()
targetStats.setBaseStat('level', 80)
targetStats.setBaseStat('armorclass', 100000)
targetStats.setBaseStat('maxhp', 50000 * FIXED_ONE)
targetStats.setHp256(50000 * FIXED_ONE)
const target = {
id: 'target',
name: 'Target',
statList: targetStats,
stateBus: new StateBus(targetStats, registry),
}
const evalResult = evaluateSkill113c({ registry, skill, slvl: 12, blvl: 12, statList: casterStats })
const missOut = executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 20,
targetX: 10,
targetY: 10,
})
// Missed finisher must NOT discharge any charge!
expect(missOut.notes).not.toContain('finisher_discharged')
expect(casterBus.getState('progressive_damage')?.stats?.charge).toBe(3)
expect(casterBus.getState('progressive_steal')?.stats?.charge).toBe(3)
expect(casterBus.getState('progressive_other')?.stats?.charge).toBe(2)
// Now force hit: all charges discharge on first connecting strike and reset to 0
casterStats.setBaseStat('tohit', 100000)
targetStats.setBaseStat('armorclass', 10)
const hpBefore = casterStats.getHp256()
const manaBefore = casterStats.getMana256()
const hitOut = executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 30,
targetX: 10,
targetY: 10,
})
expect(hitOut.notes).toContain('finisher_discharged')
expect(hitOut.totalDamageDealt).toBeGreaterThan(1000) // Base + 1440% Tiger Strike + Phoenix CL
expect(casterStats.getHp256()).toBeGreaterThan(hpBefore) // Cobra Strike life leech applied
expect(casterStats.getMana256()).toBeGreaterThan(manaBefore) // Cobra Strike mana leech applied
expect(casterBus.getState('progressive_damage')).toBeUndefined()
expect(casterBus.getState('progressive_steal')).toBeUndefined()
expect(casterBus.getState('progressive_other')).toBeUndefined()
}
})
it('Dimension 2b: FoF, CoT, and BoI fire cumulative payloads (prgstack=1) while Phoenix Strike fires strictly single payload (prgstack=0)', () => {
// FoF Tier 1, 2, 3 cumulative properties
const fof1 = resolveFinisherDischarge([{ skillId: 259, count: 1, slvl: 20, expiresAtFrame: 500 }])[0]!
const fof2 = resolveFinisherDischarge([{ skillId: 259, count: 2, slvl: 20, expiresAtFrame: 500 }])[0]!
const fof3 = resolveFinisherDischarge([{ skillId: 259, count: 3, slvl: 20, expiresAtFrame: 500 }])[0]!
expect(fof1.fireDamage?.explosionRadiusPx).toBeUndefined()
expect(fof2.fireDamage?.explosionRadiusPx).toBe(64)
expect(fof2.fireDamage?.burningGroundFrames).toBeUndefined()
expect(fof3.fireDamage?.explosionRadiusPx).toBe(64)
expect(fof3.fireDamage?.burningGroundFrames).toBe(60)
// BoI Tier 1, 2, 3 cumulative properties (1.13c Missiles.txt bladesoficecubes: 100 base + 10/lvl = 290 frames at slvl 20)
const boi1 = resolveFinisherDischarge([{ skillId: 274, count: 1, slvl: 20, expiresAtFrame: 500 }])[0]!
const boi2 = resolveFinisherDischarge([{ skillId: 274, count: 2, slvl: 20, expiresAtFrame: 500 }])[0]!
const boi3 = resolveFinisherDischarge([{ skillId: 274, count: 3, slvl: 20, expiresAtFrame: 500 }])[0]!
expect(boi1.coldDamage?.explosionRadiusPx).toBeUndefined()
expect(boi2.coldDamage?.explosionRadiusPx).toBe(64)
expect(boi2.coldDamage?.freezeDurationFrames).toBeUndefined()
expect(boi3.coldDamage?.explosionRadiusPx).toBe(64)
expect(boi3.coldDamage?.freezeDurationFrames).toBe(290)
})
it('Dimension 3: Dragon Claw runtime rejects non-dual-claw caster, executes 2 strikes when dual-claw, and does not double-count Claw Mastery synergy', async () => {
const { getSharedDataRegistry } = await import('../../../src/game/engine/data-registry.ts')
const { FIXED_ONE, UnitStatList } = await import('../../../src/game/engine/stat-list.ts')
const { StateBus } = await import('../../../src/game/engine/state-bus.ts')
const { MissileEngine } = await import('../../../src/game/engine/missile-engine.ts')
const { AuraScanner } = await import('../../../src/game/engine/aura-scanner.ts')
const { SummonManager } = await import('../../../src/game/engine/summon-manager.ts')
const { evaluateSkill113c, executeSkill113c } = await import('../../../src/game/skills/registry.ts')
const registry = await getSharedDataRegistry()
const skill = registry.getSkillById(260)!
const casterStats = new UnitStatList()
casterStats.setBaseStat('level', 80)
casterStats.setBaseStat('tohit', 100000)
casterStats.setBaseSkillLevel(260, 20)
const targetStats = new UnitStatList()
targetStats.setBaseStat('level', 80)
targetStats.setBaseStat('armorclass', 10)
targetStats.setBaseStat('maxhp', 10000 * FIXED_ONE)
targetStats.setHp256(10000 * FIXED_ONE)
const target = {
id: 'target',
name: 'Target',
statList: targetStats,
stateBus: new StateBus(targetStats, registry),
}
const evalResult = evaluateSkill113c({ registry, skill, slvl: 20, blvl: 20, statList: casterStats })
// Rejects when isDualClaw === false
const failRes = executeSkill113c({
registry,
skill,
evalResult,
caster: {
id: 'caster',
name: 'Assassin',
statList: casterStats,
stateBus: new StateBus(casterStats, registry),
isDualClaw: false,
weaponMinPhys: 100,
weaponMaxPhys: 100,
},
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 10,
targetX: 0,
targetY: 0,
})
expect(failRes.executed).toBe(false)
expect(failRes.error).toBe('requires_dual_claws')
expect(failRes.totalDamageDealt).toBe(0)
// Succeeds and lands 2 strikes when isDualClaw === true
const okRes = executeSkill113c({
registry,
skill,
evalResult,
caster: {
id: 'caster',
name: 'Assassin',
statList: casterStats,
stateBus: new StateBus(casterStats, registry),
isDualClaw: true,
weaponMinPhys: 100,
weaponMaxPhys: 100,
},
targets: [target],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 10,
targetX: 0,
targetY: 0,
})
expect(okRes.executed).toBe(true)
// Each strike deals floor(100 * (100 + 145) / 100) = 245 -> 2 strikes = 490
expect(okRes.totalDamageDealt).toBe(490)
// With Claw Mastery (252) blvl = 20 (+80% ED): total physDamagePct must be 145 + 80 = 225% (NOT 305% double-counted!)
casterStats.setBaseSkillLevel(252, 20)
const evalWithSyn = evaluateSkill113c({ registry, skill, slvl: 20, blvl: 20, statList: casterStats })
expect(evalWithSyn.synergyBonusPct).toBe(80)
expect(evalWithSyn.physDamagePct).toBe(225)
})
it('Dimension 4: Dragon Tail deals 0 Fire AoE explosion damage when primary target is 100% Physical Immune', async () => {
const { getSharedDataRegistry } = await import('../../../src/game/engine/data-registry.ts')
const { FIXED_ONE, UnitStatList } = await import('../../../src/game/engine/stat-list.ts')
const { StateBus } = await import('../../../src/game/engine/state-bus.ts')
const { MissileEngine } = await import('../../../src/game/engine/missile-engine.ts')
const { AuraScanner } = await import('../../../src/game/engine/aura-scanner.ts')
const { SummonManager } = await import('../../../src/game/engine/summon-manager.ts')
const { evaluateSkill113c, executeSkill113c } = await import('../../../src/game/skills/registry.ts')
const registry = await getSharedDataRegistry()
const skill = registry.getSkillById(270)!
const casterStats = new UnitStatList()
casterStats.setBaseStat('level', 80)
casterStats.setBaseStat('tohit', 100000)
casterStats.setBaseSkillLevel(270, 20)
const caster = {
id: 'caster',
name: 'Assassin',
statList: casterStats,
stateBus: new StateBus(casterStats, registry),
weaponMinPhys: 100,
weaponMaxPhys: 100,
}
// Primary target: 100% Physical Immune (damageresist = 100)
const immunePrimaryStats = new UnitStatList()
immunePrimaryStats.setBaseStat('level', 80)
immunePrimaryStats.setBaseStat('armorclass', 10)
immunePrimaryStats.setBaseStat('damageresist', 100)
immunePrimaryStats.setBaseStat('maxhp', 10000 * FIXED_ONE)
immunePrimaryStats.setHp256(10000 * FIXED_ONE)
const immunePrimary = {
id: 'primary_immune',
name: 'Ghost',
statList: immunePrimaryStats,
stateBus: new StateBus(immunePrimaryStats, registry),
}
// Secondary target right next to primary target (0% fire resist)
const bystanderStats = new UnitStatList()
bystanderStats.setBaseStat('level', 80)
bystanderStats.setBaseStat('armorclass', 10)
bystanderStats.setBaseStat('maxhp', 10000 * FIXED_ONE)
bystanderStats.setHp256(10000 * FIXED_ONE)
const bystander = {
id: 'bystander',
name: 'Fallen',
statList: bystanderStats,
stateBus: new StateBus(bystanderStats, registry),
}
const evalResult = evaluateSkill113c({ registry, skill, slvl: 20, blvl: 20, statList: casterStats })
const targetPositions = new Map([
['primary_immune', { x: 0, y: 0 }],
['bystander', { x: 16, y: 0 }], // 1 subtile away (< 6 subtiles radius)
])
const immuneRes = executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [immunePrimary, bystander],
targetPositions,
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 10,
targetX: 0,
targetY: 0,
})
// Because primary target took 0 physical damage, 0 fire explosion damage is dealt to bystander!
expect(immuneRes.totalDamageDealt).toBe(0)
expect(bystanderStats.getHp256()).toBe(10000 * FIXED_ONE)
// Now set primary target damageresist to 0: primary takes 100 phys, bystander takes floor(100 * 240 / 100) = 240 fire
immunePrimaryStats.setBaseStat('damageresist', 0)
const normalRes = executeSkill113c({
registry,
skill,
evalResult,
caster,
targets: [immunePrimary, bystander],
targetPositions,
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 20,
targetX: 0,
targetY: 0,
})
expect(normalRes.totalDamageDealt).toBe(100 + 240)
expect(bystanderStats.getHp256()).toBe((10000 - 240) * FIXED_ONE)
})
it('Dimension 5 & 6: Dragon Flight teleports and kicks with +575% ED at slvl 20; single-target melee skills have radiusSubtiles === 0', async () => {
const { getSharedDataRegistry } = await import('../../../src/game/engine/data-registry.ts')
const { FIXED_ONE, UnitStatList } = await import('../../../src/game/engine/stat-list.ts')
const { StateBus } = await import('../../../src/game/engine/state-bus.ts')
const { MissileEngine } = await import('../../../src/game/engine/missile-engine.ts')
const { AuraScanner } = await import('../../../src/game/engine/aura-scanner.ts')
const { SummonManager } = await import('../../../src/game/engine/summon-manager.ts')
const { evaluateSkill113c, executeSkill113c } = await import('../../../src/game/skills/registry.ts')
const registry = await getSharedDataRegistry()
// Dimension 6: Single-target melee skills (Tiger Strike 254, Dragon Talon 255, Dragon Claw 260, Cobra Strike 265) have radius = 0
for (const id of [254, 255, 260, 265]) {
const skill = registry.getSkillById(id)!
const stats = new UnitStatList()
stats.setBaseSkillLevel(id, 20)
const ev = evaluateSkill113c({ registry, skill, slvl: 20, blvl: 20, statList: stats })
expect(ev.radiusSubtiles, `Skill ${id} (${skill.name}) radiusSubtiles must be 0`).toBe(0)
}
// Dimension 5: Dragon Flight (275) teleports to target and deals kick damage with 100 + 25*(slvl-1) = +575% ED at slvl 20
const dfSkill = registry.getSkillById(275)!
const casterStats = new UnitStatList()
casterStats.setBaseStat('level', 80)
casterStats.setBaseStat('tohit', 100000)
casterStats.setBaseSkillLevel(275, 20)
const targetStats = new UnitStatList()
targetStats.setBaseStat('level', 80)
targetStats.setBaseStat('armorclass', 10)
targetStats.setBaseStat('maxhp', 10000 * FIXED_ONE)
targetStats.setHp256(10000 * FIXED_ONE)
const dfEval = evaluateSkill113c({ registry, skill: dfSkill, slvl: 20, blvl: 20, statList: casterStats })
const dfOut = executeSkill113c({
registry,
skill: dfSkill,
evalResult: dfEval,
caster: {
id: 'caster',
name: 'Assassin',
statList: casterStats,
stateBus: new StateBus(casterStats, registry),
weaponMinPhys: 100,
weaponMaxPhys: 100,
},
targets: [{
id: 'target',
name: 'Target',
statList: targetStats,
stateBus: new StateBus(targetStats, registry),
}],
targetPositions: new Map(),
corpses: [],
missileEngine: new MissileEngine(registry),
auraScanner: new AuraScanner(registry),
summonManager: new SummonManager(registry),
currentTick: 10,
targetX: 144,
targetY: 288,
})
expect(dfOut.executed).toBe(true)
expect(dfOut.notes).toContain('teleported:144,288')
expect(dfOut.totalDamageDealt).toBe(675) // 100 * (100 + 575) / 100
})
it('Differential Fuzzing (1000 random states): ChargeOrbManager & resolveFinisherDischarge vs independent 1.13c oracle', () => {
let seed = 0x1337beef
const nextRand = (max: number) => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
return seed % max
}
for (let iter = 0; iter < 1000; iter++) {
const slvl = 1 + nextRand(60) // slvls 1..60
const dt = evaluateDragonTalonStats(slvl)
expect(dt.kickCount).toBe(1 + Math.floor(slvl / 6))
expect(dt.attackRatingBonusPct).toBe(20 + 35 * (slvl - 1))
const dtail = evaluateDragonTailStats(slvl)
expect(dtail.fireExplosionPct).toBe(50 + 10 * (slvl - 1))
expect(dtail.radiusSubtiles).toBe(6)
const df = evaluateDragonFlightStats(slvl)
expect(df.enhancedDamagePct).toBe(100 + 25 * (slvl - 1))
expect(df.nextDelayFrames).toBe(4)
const tier = (1 + nextRand(3)) as 1 | 2 | 3
const tigerDischarge = resolveFinisherDischarge([
{ skillId: 254, count: tier, slvl, expiresAtFrame: 1000 },
])[0]!
const expectedTigerEd = (100 + 20 * (slvl - 1)) * tier
expect(tigerDischarge.enhancedDamagePct).toBe(expectedTigerEd)
const psDischarge = resolveFinisherDischarge([
{ skillId: 280, count: tier, slvl, expiresAtFrame: 1000 },
])[0]!
expect(psDischarge.phoenixPayload?.tier).toBe(tier)
expect(psDischarge.phoenixPayload?.type).toBe(
tier === 1 ? 'meteor' : tier === 2 ? 'chain_lightning' : 'chaos_ice',
)
}
})
})
})