diablo2-web/tests/skills/bar/adv-bar-challenger-iter2-pr...

451 lines
20 KiB
TypeScript

/**
* Independent Challenger Iteration 2 Adversarial Stress & Edge Case Probes
*
* Testing the 8 focal points:
* 1. Warcry Party Dissemination (radius bounds, distance <= 30 vs > 30, multiple allies)
* 2. Battle Orders Stamina Scaling (+35% base, +3%/lvl)
* 3. Battle Command Radius (20 subtiles across slvl 1..50)
* 4. War Cry Stun (stunDurationFrames scaling, stun state applied, damage dealt)
* 5. Battle Cry Debuffs (defense reduction, damage reduction, duration scaling slvl 1..30)
* 6. Boss & Champion Mental Curse Immunities (Howl, Grim Ward, Taunt across ranks 'boss', 'champion', 'unique', isBoss, isChampion, is_boss stat)
* 7. Find Item & Find Potion Horking Probability (corpse consumption on failure, 'hork_failed' note, diminishing returns curve)
*/
import { describe, it, expect } from 'vitest'
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
import { UnitStatList } from '../../../src/game/engine/stat-list.ts'
import { StateBus } from '../../../src/game/engine/state-bus.ts'
import { evaluateSkill113c, executeSkill113c, registerSkillModule } from '../../../src/game/skills/registry.ts'
import { skillModule as findPotionModule } from '../../../src/game/skills/impl/bar/skill-131-find-potion.ts'
import { skillModule as findItemModule } from '../../../src/game/skills/impl/bar/skill-142-find-item.ts'
import { skillModule as grimWardModule } from '../../../src/game/skills/impl/bar/skill-150-grim-ward.ts'
import type { CombatUnitContext } from '../../../src/game/engine/combat-pipeline.ts'
describe('Challenger Iteration 2 — Independent Adversarial Edge-Case Probes', () => {
// --------------------------------------------------------------------------
// 1. Warcry Party Dissemination (Shout, Battle Orders, Battle Command)
// --------------------------------------------------------------------------
describe('1. Warcry Party Dissemination Range & Filtering', () => {
it('disseminates Shout (138), Battle Orders (149), and Battle Command (155) to allies within 30 units and excludes allies outside 30 units', async () => {
const registry = await getSharedDataRegistry()
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
// Ally 1: close (dist = 10 <= 30)
const ally1Stats = new UnitStatList(registry)
const ally1Bus = new StateBus(ally1Stats, registry)
const ally1: CombatUnitContext = { id: 'ally1', name: 'MercNear', statList: ally1Stats, stateBus: ally1Bus }
// Ally 2: far (dist = 35 > 30)
const ally2Stats = new UnitStatList(registry)
const ally2Bus = new StateBus(ally2Stats, registry)
const ally2: CombatUnitContext = { id: 'ally2', name: 'MercFar', statList: ally2Stats, stateBus: ally2Bus }
// Ally 3: exact boundary (dist = 30)
const ally3Stats = new UnitStatList(registry)
const ally3Bus = new StateBus(ally3Stats, registry)
const ally3: CombatUnitContext = { id: 'ally3', name: 'MercBound', statList: ally3Stats, stateBus: ally3Bus }
const targetPositions = new Map([
['hero', { x: 0, y: 0 }],
['ally1', { x: 6, y: 8 }], // hypot = 10 <= 30 -> receives
['ally2', { x: 28, y: 21 }], // hypot = 35 > 30 -> does NOT receive
['ally3', { x: 18, y: 24 }], // hypot = 30 <= 30 -> receives
])
for (const skillId of [138, 149, 155]) {
const curHeroBus = new StateBus(heroStats, registry)
const curAlly1Bus = new StateBus(ally1Stats, registry)
const curAlly2Bus = new StateBus(ally2Stats, registry)
const curAlly3Bus = new StateBus(ally3Stats, registry)
const curHero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: curHeroBus }
const curAlly1: CombatUnitContext = { id: 'ally1', name: 'MercNear', statList: ally1Stats, stateBus: curAlly1Bus }
const curAlly2: CombatUnitContext = { id: 'ally2', name: 'MercFar', statList: ally2Stats, stateBus: curAlly2Bus }
const curAlly3: CombatUnitContext = { id: 'ally3', name: 'MercBound', statList: ally3Stats, stateBus: curAlly3Bus }
const skillRec = registry.getSkillById(skillId)!
const evalRes = evaluateSkill113c({ registry, skill: skillRec, slvl: 5, blvl: 5, statList: heroStats })
const stateExpected = skillId === 138 ? 'shout' : skillId === 149 ? 'battleorders' : 'battlecommand'
executeSkill113c({
registry,
skill: skillRec,
evalResult: evalRes,
caster: curHero,
targets: [],
targetPositions,
allies: [curAlly1, curAlly2, curAlly3],
corpses: [],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 1,
targetX: 0,
targetY: 0,
})
expect(curHeroBus.hasState(stateExpected)).toBe(true)
expect(curAlly1Bus.hasState(stateExpected)).toBe(true)
expect(curAlly2Bus.hasState(stateExpected)).toBe(false)
expect(curAlly3Bus.hasState(stateExpected)).toBe(true)
}
})
})
// --------------------------------------------------------------------------
// 2. Battle Orders Stamina Scaling
// --------------------------------------------------------------------------
describe('2. Battle Orders Stamina Scaling Across Levels', () => {
it('verifies item_maxstamina_percent strictly matches item_maxhp_percent and item_maxmana_percent for slvl 1..50', async () => {
const registry = await getSharedDataRegistry()
const boRec = registry.getSkillById(149)!
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
for (const slvl of [1, 2, 5, 10, 20, 30, 45, 50]) {
const curHeroBus = new StateBus(heroStats, registry)
const curHero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: curHeroBus }
const expectedPct = 35 + (slvl - 1) * 3
const evalBO = evaluateSkill113c({ registry, skill: boRec, slvl, blvl: slvl, statList: heroStats })
executeSkill113c({
registry,
skill: boRec,
evalResult: evalBO,
caster: curHero,
targets: [],
targetPositions: new Map(),
corpses: [],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: slvl,
targetX: 0,
targetY: 0,
})
const state = curHeroBus.getState('battleorders')
expect(state).toBeDefined()
expect(state?.stats?.['item_maxhp_percent']).toBe(expectedPct)
expect(state?.stats?.['item_maxmana_percent']).toBe(expectedPct)
expect(state?.stats?.['item_maxstamina_percent']).toBe(expectedPct)
}
})
})
// --------------------------------------------------------------------------
// 3. Battle Command Radius
// --------------------------------------------------------------------------
describe('3. Battle Command Radius Subtiles', () => {
it('verifies radiusSubtiles is authentically 20 subtiles across slvl 1..50 (not 4875)', async () => {
const registry = await getSharedDataRegistry()
const bcRec = registry.getSkillById(155)!
const stats = new UnitStatList(registry)
for (const slvl of [1, 5, 10, 20, 30, 40, 50]) {
const evalBC = evaluateSkill113c({ registry, skill: bcRec, slvl, blvl: slvl, statList: stats })
expect(evalBC.radiusSubtiles).toBe(20)
// Duration should scale per 1.13c: 125 + 250 * (slvl - 1)
expect(evalBC.durationFrames).toBe(125 + 250 * (slvl - 1))
}
})
})
// --------------------------------------------------------------------------
// 4. War Cry Stun & Damage
// --------------------------------------------------------------------------
describe('4. War Cry Radial Physical Damage & Stun Delivery', () => {
it('verifies target takes radial physical spell damage AND is stunned for 25 + 5 * (slvl - 1) frames via executeSUnitDmg', async () => {
const registry = await getSharedDataRegistry()
const wcRec = registry.getSkillById(154)!
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
for (const slvl of [1, 5, 10, 20]) {
const dummyStats = new UnitStatList(registry)
const dummyBus = new StateBus(dummyStats, registry)
const dummy: CombatUnitContext = { id: `dummy-${slvl}`, name: 'Target', statList: dummyStats, stateBus: dummyBus }
const evalWC = evaluateSkill113c({ registry, skill: wcRec, slvl, blvl: slvl, statList: heroStats })
const expectedStunFrames = 25 + 5 * (slvl - 1)
const outcome = executeSkill113c({
registry,
skill: wcRec,
evalResult: evalWC,
caster: hero,
targets: [dummy],
targetPositions: new Map(),
corpses: [],
missileEngine: { spawnMissile: () => ({ name: 'warcry' }) } as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 1,
targetX: 0,
targetY: 0,
})
expect(outcome.totalDamageDealt).toBeGreaterThan(0)
expect(outcome.statesApplied).toContain('stun')
expect(dummyBus.hasState('stun')).toBe(true)
const stunState = dummyBus.getState('stun')
expect(stunState).toBeDefined()
expect(stunState?.durationFrames).toBe(expectedStunFrames)
}
})
})
// --------------------------------------------------------------------------
// 5. Battle Cry Debuff Formulas
// --------------------------------------------------------------------------
describe('5. Battle Cry Debuff Formulas Across Levels', () => {
it('verifies defense reduction -(50 + 2 * (slvl - 1))%, damage reduction -(25 + 1 * (slvl - 1))%, and duration 300 + 60 * (slvl - 1)', async () => {
const registry = await getSharedDataRegistry()
const bcRec = registry.getSkillById(146)!
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
for (const slvl of [1, 2, 5, 10, 20, 30]) {
const dummyStats = new UnitStatList(registry)
const dummyBus = new StateBus(dummyStats, registry)
const dummy: CombatUnitContext = { id: `dummy-${slvl}`, name: 'Monster', statList: dummyStats, stateBus: dummyBus }
const evalBC = evaluateSkill113c({ registry, skill: bcRec, slvl, blvl: slvl, statList: heroStats })
executeSkill113c({
registry,
skill: bcRec,
evalResult: evalBC,
caster: hero,
targets: [dummy],
targetPositions: new Map(),
corpses: [],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 1,
targetX: 0,
targetY: 0,
})
const state = dummyBus.getState('battlecry')
expect(state).toBeDefined()
const expectedDef = -(50 + 2 * (slvl - 1))
const expectedDmg = -(25 + 1 * (slvl - 1))
const expectedDuration = 300 + 60 * (slvl - 1)
expect(state?.stats?.['item_armor_percent']).toBe(expectedDef)
expect(state?.stats?.['item_mindamage_percent']).toBe(expectedDmg)
expect(state?.durationFrames).toBe(expectedDuration)
}
})
})
// --------------------------------------------------------------------------
// 6. Boss & Champion Mental Curse Immunities
// --------------------------------------------------------------------------
describe('6. Boss & Champion Mental Curse Immunities (Howl, Grim Ward, Taunt)', () => {
it('verifies Act Bosses, Champions, and SuperUniques are completely immune to Howl (#130), Grim Ward (#150), and Taunt (#137)', async () => {
const registry = await getSharedDataRegistry()
registerSkillModule(grimWardModule)
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
// Define diverse immune targets:
const actBossStats = new UnitStatList(registry)
const actBossBus = new StateBus(actBossStats, registry)
const actBoss: CombatUnitContext = { id: 'diablo', name: 'Diablo', statList: actBossStats, stateBus: actBossBus, isBoss: true, rank: 'boss' }
const championStats = new UnitStatList(registry)
const championBus = new StateBus(championStats, registry)
const champion: CombatUnitContext = { id: 'champ', name: 'Champion Fallen', statList: championStats, stateBus: championBus, isChampion: true, rank: 'champion' }
const superUniqueStats = new UnitStatList(registry)
const superUniqueBus = new StateBus(superUniqueStats, registry)
const superUnique: CombatUnitContext = { id: 'rakanishu', name: 'Rakanishu', statList: superUniqueStats, stateBus: superUniqueBus, rank: 'unique' }
// Normal non-immune target:
const normalStats = new UnitStatList(registry)
const normalBus = new StateBus(normalStats, registry)
const normalTarget: CombatUnitContext = { id: 'normal', name: 'Fallen', statList: normalStats, stateBus: normalBus, rank: 'normal' }
const allTargets = [actBoss, champion, superUnique, normalTarget]
// 6.1 Test Howl (130)
const howlRec = registry.getSkillById(130)!
const evalHowl = evaluateSkill113c({ registry, skill: howlRec, slvl: 5, blvl: 5, statList: heroStats })
executeSkill113c({
registry,
skill: howlRec,
evalResult: evalHowl,
caster: hero,
targets: allTargets,
targetPositions: new Map(),
corpses: [],
missileEngine: { spawnMissile: () => ({ name: 'howl' }) } as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 1,
targetX: 0,
targetY: 0,
})
expect(normalBus.hasState('terror')).toBe(true)
expect(actBossBus.hasState('terror')).toBe(false)
expect(championBus.hasState('terror')).toBe(false)
expect(superUniqueBus.hasState('terror')).toBe(false)
// 6.2 Test Grim Ward (150)
const normalBus2 = new StateBus(normalStats, registry)
const normalTarget2: CombatUnitContext = { ...normalTarget, stateBus: normalBus2 }
const gwRec = registry.getSkillById(150)!
const evalGW = evaluateSkill113c({ registry, skill: gwRec, slvl: 5, blvl: 5, statList: heroStats })
const corpse = { id: 'corpse-1', consumed: false, shattered: false, isBoss: false, baseMaxHp: 500, x: 0, y: 0 }
executeSkill113c({
registry,
skill: gwRec,
evalResult: evalGW,
caster: hero,
targets: [actBoss, champion, superUnique, normalTarget2],
targetPositions: new Map(),
corpses: [corpse as any],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 2,
targetX: 0,
targetY: 0,
})
expect(normalBus2.hasState('terror')).toBe(true)
expect(actBossBus.hasState('terror')).toBe(false)
expect(championBus.hasState('terror')).toBe(false)
expect(superUniqueBus.hasState('terror')).toBe(false)
// 6.3 Test Taunt (137)
const normalBus3 = new StateBus(normalStats, registry)
const normalTarget3: CombatUnitContext = { ...normalTarget, stateBus: normalBus3 }
const tauntRec = registry.getSkillById(137)!
const evalTaunt = evaluateSkill113c({ registry, skill: tauntRec, slvl: 5, blvl: 5, statList: heroStats })
executeSkill113c({
registry,
skill: tauntRec,
evalResult: evalTaunt,
caster: hero,
targets: [actBoss, champion, superUnique, normalTarget3],
targetPositions: new Map(),
corpses: [],
missileEngine: { spawnMissile: () => ({ name: 'taunt' }) } as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: 3,
targetX: 0,
targetY: 0,
})
expect(normalBus3.hasState('taunt')).toBe(true)
expect(actBossBus.hasState('taunt')).toBe(false)
expect(championBus.hasState('taunt')).toBe(false)
expect(superUniqueBus.hasState('taunt')).toBe(false)
})
})
// --------------------------------------------------------------------------
// 7. Find Item & Find Potion Horking Probability
// --------------------------------------------------------------------------
describe('7. Find Item & Find Potion Horking Probability and Corpse Consumption', () => {
it('verifies failed rolls consume the corpse and emit note "hork_failed", while successful rolls emit "item_found" or "potion_found"', async () => {
const registry = await getSharedDataRegistry()
registerSkillModule(findPotionModule)
registerSkillModule(findItemModule)
const heroStats = new UnitStatList(registry)
const heroBus = new StateBus(heroStats, registry)
const hero: CombatUnitContext = { id: 'hero', name: 'Barbarian', statList: heroStats, stateBus: heroBus }
// Find Potion (131)
const fpRec = registry.getSkillById(131)!
const evalFP = evaluateSkill113c({ registry, skill: fpRec, slvl: 1, blvl: 1, statList: heroStats })
// Find Item (142)
const fiRec = registry.getSkillById(142)!
const evalFI = evaluateSkill113c({ registry, skill: fiRec, slvl: 1, blvl: 1, statList: heroStats })
// Run 50 trials of each skill and verify invariant:
// 1. corpse.consumed is ALWAYS true
// 2. corpsesConsumed === 1
// 3. executed === true
// 4. exactly one of 'hork_failed' or 'potion_found'/'item_found' is in notes
let fpSuccess = 0
let fpFailed = 0
for (let i = 0; i < 50; i++) {
const corpse = { id: `corpse-fp-${i}`, consumed: false, shattered: false, isBoss: false, baseMaxHp: 500, x: 0, y: 0 }
const outcome = executeSkill113c({
registry,
skill: fpRec,
evalResult: evalFP,
caster: hero,
targets: [],
targetPositions: new Map(),
corpses: [corpse as any],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: i,
targetX: 0,
targetY: 0,
})
expect(corpse.consumed).toBe(true)
expect(outcome.executed).toBe(true)
expect(outcome.corpsesConsumed).toBe(1)
if (outcome.notes.includes('potion_found')) {
fpSuccess++
expect(outcome.notes.includes('hork_failed')).toBe(false)
} else {
fpFailed++
expect(outcome.notes.includes('hork_failed')).toBe(true)
}
}
expect(fpSuccess).toBeGreaterThan(0)
expect(fpFailed).toBeGreaterThan(0)
let fiSuccess = 0
let fiFailed = 0
for (let i = 0; i < 50; i++) {
const corpse = { id: `corpse-fi-${i}`, consumed: false, shattered: false, isBoss: false, baseMaxHp: 500, x: 0, y: 0 }
const outcome = executeSkill113c({
registry,
skill: fiRec,
evalResult: evalFI,
caster: hero,
targets: [],
targetPositions: new Map(),
corpses: [corpse as any],
missileEngine: {} as any,
auraScanner: {} as any,
summonManager: {} as any,
currentTick: i,
targetX: 0,
targetY: 0,
})
expect(corpse.consumed).toBe(true)
expect(outcome.executed).toBe(true)
expect(outcome.corpsesConsumed).toBe(1)
if (outcome.notes.includes('item_found')) {
fiSuccess++
expect(outcome.notes.includes('hork_failed')).toBe(false)
} else {
fiFailed++
expect(outcome.notes.includes('hork_failed')).toBe(true)
}
}
expect(fiSuccess).toBeGreaterThan(0)
expect(fiFailed).toBeGreaterThan(0)
})
})
})