289 lines
13 KiB
TypeScript
289 lines
13 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)
|
|
})
|
|
})
|
|
})
|