451 lines
20 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|