509 lines
22 KiB
TypeScript
509 lines
22 KiB
TypeScript
/**
|
|
* Diablo II: Lord of Destruction v1.13c — Assassin Milestone M29 Class Gate & End-to-End Suite
|
|
*
|
|
* Comprehensive cross-tree integration and architectural verification across all 30 Assassin skills:
|
|
*
|
|
* Skill Trees Covered:
|
|
* 1. Traps (10 skills):
|
|
* - Fire Blast (251), Shock Web (256), Blade Sentinel (257), Charged Bolt Sentry (261),
|
|
* Wake of Fire (262), Blade Fury (266), Lightning Sentry (271), Wake of Inferno (272),
|
|
* Death Sentry (276), Blade Shield (277).
|
|
* 2. Martial Arts (10 skills):
|
|
* - Tiger Strike (254), Dragon Talon (255), Fists of Fire (259), Dragon Claw (260),
|
|
* Cobra Strike (265), Claws of Thunder (269), Dragon Tail (270), Blades of Ice (274),
|
|
* Dragon Flight (275), Phoenix Strike (280).
|
|
* 3. Shadow Disciplines (10 skills):
|
|
* - Claw Mastery (252), Psychic Hammer (253), Burst of Speed (258), Weapon Block (263),
|
|
* Cloak of Shadows (264), Fade (267), Shadow Warrior (268), Mind Blast (273),
|
|
* Venom (278), Shadow Master (279).
|
|
*
|
|
* Core Verification Scenarios:
|
|
* Scenario 1: Traps Tree Synergy Network & 5-Band Progression (Traps)
|
|
* Scenario 2: Martial Arts Charge-Up & Finisher Discharge Pipeline
|
|
* Scenario 3: Shadow Disciplines Buff Mechanics & Venom Bitrate Clamp
|
|
* Scenario 4: Minion & Summon Caps (5 Traps + 1 Shadow = 6 Max)
|
|
* Scenario 5: NextDelay Collision Avoidance Across Trees
|
|
* Scenario 6: Cross-Tree Hybrid Build Integration (Trapper / Kicksin / Shadow)
|
|
* Scenario 7: Crowd Control & Status Effects (Mind Blast Stun/Convert, Psychic Hammer KB, CoS Blind)
|
|
* Scenario 8: Complete 30-Skill Assassin Tree Data Registry & Architectural Invariants
|
|
*
|
|
* ZERO MOCKS: vi.mock / vi.spyOn strictly forbidden.
|
|
* ZERO CIRCULAR ORACLES: Precalculated independently from 1.13c ground truth constants.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import { BATCH1_SKILLS } from '../../../src/game/skills.ts'
|
|
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
|
|
|
|
// Traps imports
|
|
import {
|
|
ASSASSIN_TRAP_SKILL_IDS,
|
|
BLADE_SRC_DAM,
|
|
MAX_ACTIVE_TRAPS,
|
|
NEXT_DELAY_FRAMES,
|
|
NextDelayTracker,
|
|
TrapManager,
|
|
evaluateBladeFuryStats,
|
|
evaluateBladeSentinelStats,
|
|
evaluateDeathSentryStats,
|
|
evaluateFireBlastStats,
|
|
evaluateLightningSentryStats,
|
|
evaluateWakeOfFireStats,
|
|
} from '../../../src/game/skills/assassin-traps.ts'
|
|
|
|
// Martial Arts imports
|
|
import {
|
|
ASSASSIN_MARTIAL_SKILL_IDS,
|
|
ChargeOrbManager,
|
|
canCastDragonClaw,
|
|
evaluateCobraStrikeStats,
|
|
evaluateDragonClawStats,
|
|
evaluateDragonFlightStats,
|
|
evaluateDragonTailStats,
|
|
evaluateDragonTalonStats,
|
|
evaluatePhoenixStrikeStats,
|
|
evaluateTigerStrikeStats,
|
|
resolveFinisherDischarge,
|
|
} from '../../../src/game/skills/assassin-martial-arts.ts'
|
|
|
|
// Shadow Disciplines imports
|
|
import {
|
|
ASSASSIN_SHADOW_SKILL_IDS,
|
|
ShadowBuffTracker,
|
|
ShadowMinionManager,
|
|
computeBurstOfSpeedDuration,
|
|
computeBurstOfSpeedFRW,
|
|
computeBurstOfSpeedIAS,
|
|
computeClawMasteryAR,
|
|
computeClawMasteryCriticalStrike,
|
|
computeClawMasteryED,
|
|
computeCloakOfShadowsDefenseBonus,
|
|
computeCloakOfShadowsDefenseReduction,
|
|
computeCloakOfShadowsDuration,
|
|
computeFadeCurseReduction,
|
|
computeFadeDuration,
|
|
computeFadePhysicalDamageReduction,
|
|
computeFadeResistances,
|
|
computeMindBlastConversionChance,
|
|
computeMindBlastDamage,
|
|
computeMindBlastStunDuration,
|
|
computePsychicHammerDamage,
|
|
computePsychicHammerKnockbackChance,
|
|
computeShadowMasterStats,
|
|
computeShadowWarriorStats,
|
|
computeVenomDuration,
|
|
computeVenomPoisonDamage,
|
|
computeWeaponBlockChance,
|
|
resolveMindBlastTarget,
|
|
resolveVenomPoisonClamp,
|
|
resolveWeaponBlock,
|
|
} from '../../../src/game/skills/assassin-shadow.ts'
|
|
|
|
describe('Milestone M29 — Assassin Class Gate & Comprehensive E2E Integration Suite', () => {
|
|
// =========================================================================
|
|
// Scenario 1: Traps Tree Synergy Network & 5-Band Progression (Traps)
|
|
// =========================================================================
|
|
describe('Scenario 1: Traps Tree Synergy Network & 5-Band Progression (Traps)', () => {
|
|
it('verifies Lightning Sentry 5-band damage progression and 3-way synergy (+360%)', () => {
|
|
// Base Lightning Sentry slvl 20 (0 synergies):
|
|
// Min: 1 (constant)
|
|
// Max: compute5BandScaling(20, 20, 10, 16, 24, 34, 44) = 20 + 7*10 + 8*16 + 4*24 = 314
|
|
const baseLS = evaluateLightningSentryStats(20, {})
|
|
expect(baseLS.minDamage).toBe(1)
|
|
expect(baseLS.maxDamage).toBe(314)
|
|
expect(baseLS.shots).toBe(10)
|
|
|
|
// Fully synergized Lightning Sentry (20 Shock Web, 20 Charged Bolt Sentry, 20 Death Sentry):
|
|
// In 1.13c: +12% per blvl of Shock Web, CBS, Death Sentry (total 3 * 20 * 12% = +720%)
|
|
const synLS = evaluateLightningSentryStats(20, {
|
|
shockWeb: 20,
|
|
chargedBoltSentry: 20,
|
|
deathSentry: 20,
|
|
})
|
|
expect(synLS.minDamage).toBe(1)
|
|
expect(synLS.maxDamage).toBe(Math.floor(314 * (1 + 720 / 100))) // 2574
|
|
})
|
|
|
|
it('verifies Death Sentry corpse explosion radius, shot count, and lightning synergy', () => {
|
|
// Base Death Sentry slvl 20, 0 Fire Blast blvl, 0 LS blvl
|
|
const baseDS = evaluateDeathSentryStats(20, {})
|
|
// Radius: 10 + (20 - 1) = 29 subtiles (9.66 yards)
|
|
expect(baseDS.corpseExplosionRadiusSubtiles).toBe(29)
|
|
// Shot count: 5 + floor(0 / 3) = 5
|
|
expect(baseDS.shots).toBe(5)
|
|
// Corpse explosion damage range: 40% to 80% of corpse max HP
|
|
expect(baseDS.corpseExplosionMinPct).toBe(40)
|
|
expect(baseDS.corpseExplosionMaxPct).toBe(80)
|
|
|
|
// Death Sentry with 15 Fire Blast blvl -> shot count = 5 + floor(15/3) = 10 shots!
|
|
const synDS = evaluateDeathSentryStats(20, {
|
|
fireBlast: 15,
|
|
lightningSentry: 20,
|
|
})
|
|
expect(synDS.shots).toBe(10)
|
|
// Lightning damage: base max is 50 + 7*11 + 8*15 + 4*19 = 323, boosted by +240% (20 * 12%) -> floor(323 * 3.4) = 1098
|
|
expect(synDS.maxLtngDamage).toBe(Math.floor(323 * 3.4))
|
|
})
|
|
|
|
it('verifies Blade weapon damage scaling (Blade Sentinel 3/8, Blade Fury 3/4)', () => {
|
|
const bSentinel = evaluateBladeSentinelStats(20, 128)
|
|
// 128 base weapon damage * 48 / 128 = 48 scaled weapon damage
|
|
expect(bSentinel.scaledWeaponPhys).toBe(48)
|
|
expect(BLADE_SRC_DAM.BLADE_SENTINEL).toBe(48) // 48/128 = 3/8
|
|
|
|
const bFury = evaluateBladeFuryStats(20, 128)
|
|
// 128 base weapon damage * 96 / 128 = 96 scaled weapon damage
|
|
expect(bFury.scaledWeaponPhys).toBe(96)
|
|
expect(BLADE_SRC_DAM.BLADE_FURY).toBe(96) // 96/128 = 3/4
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 2: Martial Arts Charge-Up & Finisher Discharge Pipeline
|
|
// =========================================================================
|
|
describe('Scenario 2: Martial Arts Charge-Up & Finisher Discharge Pipeline', () => {
|
|
it('manages multi-skill charge accumulation and resolves full discharge on finisher contact', () => {
|
|
const orbMgr = new ChargeOrbManager()
|
|
|
|
// Charge up Tiger Strike to tier 3
|
|
orbMgr.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 100)
|
|
orbMgr.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 150)
|
|
orbMgr.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 200)
|
|
|
|
// Charge up Cobra Strike to tier 2
|
|
orbMgr.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 20, 250)
|
|
orbMgr.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 20, 300)
|
|
|
|
expect(orbMgr.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 300)).toBe(3)
|
|
expect(orbMgr.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE, 300)).toBe(2)
|
|
|
|
// Finisher impacts: resolveFinisherDischarge releases all active charges
|
|
const activeCharges = orbMgr.dischargeAll(300)
|
|
expect(activeCharges.length).toBe(2)
|
|
|
|
const payloads = resolveFinisherDischarge(activeCharges)
|
|
expect(payloads.length).toBe(2)
|
|
|
|
const tigerPayload = payloads.find(p => p.skillId === ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE)
|
|
expect(tigerPayload?.enhancedDamagePct).toBe(1440) // Tier 3 slvl 20: +1440% ED
|
|
|
|
const cobraPayload = payloads.find(p => p.skillId === ASSASSIN_MARTIAL_SKILL_IDS.COBRA_STRIKE)
|
|
expect(cobraPayload?.lifeStealPct).toBe(135) // Tier 2 slvl 20
|
|
expect(cobraPayload?.manaStealPct).toBe(135) // Tier 2 slvl 20
|
|
|
|
// OrbManager is now completely clear
|
|
expect(orbMgr.getActiveCharges(300).length).toBe(0)
|
|
})
|
|
|
|
it('verifies Phoenix Strike strictly discharges the targeted single-tier payload', () => {
|
|
const pStats = evaluatePhoenixStrikeStats(20)
|
|
|
|
// Tier 1: Meteor
|
|
expect(pStats.meteorMinDamage).toBeGreaterThan(0)
|
|
expect(pStats.meteorMaxDamage).toBeGreaterThan(pStats.meteorMinDamage)
|
|
|
|
// Tier 2: Chain Lightning
|
|
expect(pStats.chainLightningMaxDamage).toBeGreaterThan(0)
|
|
expect(pStats.chainLightningTargets).toBeGreaterThan(0)
|
|
|
|
// Tier 3: Chaos Ice Orb
|
|
expect(pStats.chaosIceMaxDamage).toBeGreaterThan(0)
|
|
expect(pStats.freezeDurationFrames).toBeGreaterThan(0)
|
|
|
|
// Discharge resolution for Tier 1
|
|
const p1 = resolveFinisherDischarge([{
|
|
skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE,
|
|
count: 1,
|
|
slvl: 20,
|
|
expiresAtFrame: 1000,
|
|
}])[0]
|
|
expect(p1?.phoenixPayload?.tier).toBe(1)
|
|
expect(p1?.phoenixPayload?.type).toBe('meteor')
|
|
|
|
// Discharge resolution for Tier 2
|
|
const p2 = resolveFinisherDischarge([{
|
|
skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE,
|
|
count: 2,
|
|
slvl: 20,
|
|
expiresAtFrame: 1000,
|
|
}])[0]
|
|
expect(p2?.phoenixPayload?.tier).toBe(2)
|
|
expect(p2?.phoenixPayload?.type).toBe('chain_lightning')
|
|
|
|
// Discharge resolution for Tier 3
|
|
const p3 = resolveFinisherDischarge([{
|
|
skillId: ASSASSIN_MARTIAL_SKILL_IDS.PHOENIX_STRIKE,
|
|
count: 3,
|
|
slvl: 20,
|
|
expiresAtFrame: 1000,
|
|
}])[0]
|
|
expect(p3?.phoenixPayload?.tier).toBe(3)
|
|
expect(p3?.phoenixPayload?.type).toBe('chaos_ice')
|
|
})
|
|
|
|
it('verifies Dragon Talon multi-kicks and Dragon Claw dual-claw strike mechanics', () => {
|
|
// Dragon Talon kick count: 1 + floor(slvl / 6)
|
|
expect(evaluateDragonTalonStats(1).kickCount).toBe(1)
|
|
expect(evaluateDragonTalonStats(6).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)
|
|
|
|
// Dragon Claw requirement and dual strikes
|
|
expect(canCastDragonClaw(true, false)).toBe(false)
|
|
expect(canCastDragonClaw(true, true)).toBe(true)
|
|
|
|
const dcStats = evaluateDragonClawStats(20, 20)
|
|
expect(dcStats.strikesCount).toBe(2)
|
|
// Base ED: 50 + 5 * 19 = 145% + Claw Mastery synergy (20 * 4% = +80%) = 225%
|
|
expect(dcStats.enhancedDamagePct).toBe(225)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 3: Shadow Disciplines Buff Mechanics & Venom Bitrate Clamp
|
|
// =========================================================================
|
|
describe('Scenario 3: Shadow Disciplines Buff Mechanics & Venom Bitrate Clamp', () => {
|
|
it('enforces strict mutual exclusivity between Burst of Speed and Fade', () => {
|
|
const tracker = new ShadowBuffTracker()
|
|
|
|
tracker.applyBurstOfSpeed(20)
|
|
expect(tracker.getBurstOfSpeed()?.params['iasPct']).toBe(53)
|
|
expect(tracker.getFade()).toBeNull()
|
|
|
|
// Activating Fade cancels BoS immediately
|
|
const fadeRes = tracker.applyFade(20)
|
|
expect(fadeRes.cancelledBoS).toBe(true)
|
|
expect(tracker.getBurstOfSpeed()).toBeNull()
|
|
expect(tracker.getFade()?.params['allResistPct']).toBe(65)
|
|
expect(tracker.getFade()?.params['damageResistPct']).toBe(20)
|
|
})
|
|
|
|
it('clamps combined poison damage to 10 frames under active Venom buff', () => {
|
|
// Venom slvl 20 deals 170..178 over 10 frames
|
|
// Adding weapon poison: 50 damage over 50 frames (1 dmg/frame)
|
|
const clamped = resolveVenomPoisonClamp(20, [{ damage: 50, durationFrames: 50 }])
|
|
expect(clamped.finalDurationFrames).toBe(10)
|
|
// 170 (venom base min) + 10 (weapon 1 dmg/frame * 10 frames) = 180 total
|
|
expect(clamped.totalDamageDealt).toBe(180)
|
|
})
|
|
|
|
it('enforces Cloak of Shadows recast lockout and defense modifications', () => {
|
|
const tracker = new ShadowBuffTracker()
|
|
|
|
const c1 = tracker.applyCloakOfShadows(20)
|
|
expect(c1.success).toBe(true)
|
|
expect(tracker.getCloakOfShadows()?.params['monsterDefReductionPct']).toBe(72)
|
|
expect(tracker.getCloakOfShadows()?.params['assassinDefBonusPct']).toBe(67)
|
|
|
|
// Recast while active must fail
|
|
const c2 = tracker.applyCloakOfShadows(20)
|
|
expect(c2.success).toBe(false)
|
|
expect(c2.reason).toContain('recast locked')
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 4: Minion & Summon Caps (5 Traps + 1 Shadow = 6 Max)
|
|
// =========================================================================
|
|
describe('Scenario 4: Minion & Summon Caps (5 Traps + 1 Shadow = 6 Max)', () => {
|
|
it('enforces global 5-trap FIFO eviction cap on PetType assassintrap', () => {
|
|
const trapMgr = new TrapManager()
|
|
const ownerId = 'assassin_player'
|
|
|
|
// Summon 5 traps
|
|
for (let i = 1; i <= 5; i++) {
|
|
const t = trapMgr.registerTrap({
|
|
ownerId,
|
|
skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY,
|
|
summonMon: 'lightningsentry',
|
|
x: i * 10,
|
|
y: i * 10,
|
|
slvl: 20,
|
|
shots: 10,
|
|
currentFrame: i * 100,
|
|
})
|
|
expect(t.evictedTrap).toBeUndefined()
|
|
}
|
|
expect(trapMgr.getActiveTrapCount(ownerId)).toBe(5)
|
|
|
|
// 6th trap causes FIFO eviction of trap 1
|
|
const sixth = trapMgr.registerTrap({
|
|
ownerId,
|
|
skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY,
|
|
summonMon: 'deathsentry',
|
|
x: 60,
|
|
y: 60,
|
|
slvl: 20,
|
|
shots: 5,
|
|
currentFrame: 600,
|
|
})
|
|
expect(sixth.evictedTrap?.id).toBe('trap_1')
|
|
expect(trapMgr.getActiveTrapCount(ownerId)).toBe(5)
|
|
})
|
|
|
|
it('enforces PetType shadowwarrior cap = 1 and coexists with 5 active traps', () => {
|
|
const trapMgr = new TrapManager()
|
|
const shadowMgr = new ShadowMinionManager()
|
|
const ownerId = 'assassin_player'
|
|
|
|
// Summon 5 traps
|
|
for (let i = 1; i <= 5; i++) {
|
|
trapMgr.registerTrap({
|
|
ownerId,
|
|
skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY,
|
|
summonMon: 'lightningsentry',
|
|
x: i * 10,
|
|
y: i * 10,
|
|
slvl: 20,
|
|
shots: 10,
|
|
currentFrame: 100,
|
|
})
|
|
}
|
|
expect(trapMgr.getActiveTrapCount(ownerId)).toBe(5)
|
|
|
|
// Summon Shadow Warrior
|
|
const s1 = shadowMgr.summonShadow('warrior', 20, 200)
|
|
expect(s1.despawnedPrevious).toBe(false)
|
|
expect(shadowMgr.getActiveShadow()?.type).toBe('warrior')
|
|
|
|
// Summon Shadow Master -> replaces Shadow Warrior
|
|
const s2 = shadowMgr.summonShadow('master', 20, 250)
|
|
expect(s2.despawnedPrevious).toBe(true)
|
|
expect(shadowMgr.getActiveShadow()?.type).toBe('master')
|
|
|
|
// Total pets active: 5 traps + 1 shadow = 6 pets simultaneously
|
|
expect(trapMgr.getActiveTrapCount(ownerId)).toBe(5)
|
|
expect(shadowMgr.getActiveShadow()).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 5: NextDelay Collision Avoidance Across Trees
|
|
// =========================================================================
|
|
describe('Scenario 5: NextDelay Collision Avoidance Across Trees', () => {
|
|
it('enforces NextDelay hit protection (Wake of Fire 4f, Shock Web 25f, Dragon Flight 4f)', () => {
|
|
const nd = new NextDelayTracker()
|
|
const targetId = 'target_boss'
|
|
const channelWoF = 'wof_channel'
|
|
const channelShockWeb = 'shockweb_channel'
|
|
|
|
// Wake of Fire (4 frames NextDelay)
|
|
expect(nd.canTakeDamage(targetId, channelWoF, 100)).toBe(true)
|
|
nd.recordHit(targetId, channelWoF, 100, NEXT_DELAY_FRAMES.WAKE_OF_FIRE)
|
|
|
|
// Hits on frames 101, 102, 103 are rejected
|
|
expect(nd.canTakeDamage(targetId, channelWoF, 101)).toBe(false)
|
|
expect(nd.canTakeDamage(targetId, channelWoF, 102)).toBe(false)
|
|
expect(nd.canTakeDamage(targetId, channelWoF, 103)).toBe(false)
|
|
// Hit on frame 104 is accepted!
|
|
expect(nd.canTakeDamage(targetId, channelWoF, 104)).toBe(true)
|
|
|
|
// Shock Web (25 frames NextDelay)
|
|
nd.recordHit(targetId, channelShockWeb, 200, NEXT_DELAY_FRAMES.SHOCK_WEB)
|
|
expect(nd.canTakeDamage(targetId, channelShockWeb, 224)).toBe(false)
|
|
expect(nd.canTakeDamage(targetId, channelShockWeb, 225)).toBe(true)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 6: Cross-Tree Hybrid Build Integration (Trapper / Kicksin / Shadow)
|
|
// =========================================================================
|
|
describe('Scenario 6: Cross-Tree Hybrid Build Integration (Trapper / Kicksin / Shadow)', () => {
|
|
it('simulates a complete hybrid combat loop without state pollution', () => {
|
|
const buffs = new ShadowBuffTracker()
|
|
const traps = new TrapManager()
|
|
const shadows = new ShadowMinionManager()
|
|
const orbs = new ChargeOrbManager()
|
|
const ownerId = 'hybrid_assassin'
|
|
|
|
// 1. Buff Burst of Speed
|
|
buffs.applyBurstOfSpeed(20)
|
|
expect(buffs.getBurstOfSpeed()?.params['frwPct']).toBe(61)
|
|
|
|
// 2. Summon Shadow Master
|
|
shadows.summonShadow('master', 20, 100)
|
|
expect(shadows.getActiveShadow()?.stats.allResistPct).toBe(76)
|
|
|
|
// 3. Lay 4 Lightning Sentries + 1 Death Sentry
|
|
traps.registerTrap({ ownerId, skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, summonMon: 'ls', x: 0, y: 0, slvl: 20, shots: 10, currentFrame: 150 })
|
|
traps.registerTrap({ ownerId, skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, summonMon: 'ls', x: 2, y: 0, slvl: 20, shots: 10, currentFrame: 160 })
|
|
traps.registerTrap({ ownerId, skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, summonMon: 'ls', x: -2, y: 0, slvl: 20, shots: 10, currentFrame: 170 })
|
|
traps.registerTrap({ ownerId, skillId: ASSASSIN_TRAP_SKILL_IDS.LIGHTNING_SENTRY, summonMon: 'ls', x: 0, y: 2, slvl: 20, shots: 10, currentFrame: 180 })
|
|
traps.registerTrap({ ownerId, skillId: ASSASSIN_TRAP_SKILL_IDS.DEATH_SENTRY, summonMon: 'ds', x: 0, y: -2, slvl: 20, shots: 5, currentFrame: 190 })
|
|
expect(traps.getActiveTrapCount(ownerId)).toBe(5)
|
|
|
|
// 4. Engage in melee: build Tiger Strike charges
|
|
orbs.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 200)
|
|
orbs.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 220)
|
|
orbs.addCharge(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 20, 240)
|
|
expect(orbs.getChargeLevel(ASSASSIN_MARTIAL_SKILL_IDS.TIGER_STRIKE, 240)).toBe(3)
|
|
|
|
// 5. Release via Dragon Talon multi-kick finisher
|
|
const talon = evaluateDragonTalonStats(20)
|
|
expect(talon.kickCount).toBe(4) // 1 + floor(20/6) = 4 kicks
|
|
const discharge = resolveFinisherDischarge(orbs.dischargeAll(250))
|
|
expect(discharge[0]?.enhancedDamagePct).toBe(1440)
|
|
expect(orbs.getActiveCharges(250).length).toBe(0)
|
|
|
|
// Everything coexists in pristine state
|
|
expect(traps.getActiveTrapCount(ownerId)).toBe(5)
|
|
expect(shadows.getActiveShadow()).not.toBeNull()
|
|
expect(buffs.getBurstOfSpeed()).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 7: Crowd Control & Status Effects (Mind Blast, Psychic Hammer, CoS)
|
|
// =========================================================================
|
|
describe('Scenario 7: Crowd Control & Status Effects (Mind Blast, Psychic Hammer, CoS)', () => {
|
|
it('verifies Mind Blast 4-yard AOE stun and normal monster conversion', () => {
|
|
// Normal monster hit by Mind Blast
|
|
const resNorm = resolveMindBlastTarget(20, { roll: 10 })
|
|
expect(resNorm.stunApplied).toBe(true)
|
|
expect(resNorm.stunDurationFrames).toBe(145) // 50 + 5*(20-1)
|
|
expect(resNorm.converted).toBe(true)
|
|
|
|
// Unique/Boss immune to conversion
|
|
const resBoss = resolveMindBlastTarget(20, { isBoss: true, roll: 0 })
|
|
expect(resBoss.stunApplied).toBe(true)
|
|
expect(resBoss.stunDurationFrames).toBe(145)
|
|
expect(resBoss.converted).toBe(false)
|
|
})
|
|
|
|
it('verifies Psychic Hammer knockback and 50/50 magic/phys damage split', () => {
|
|
const dmg = computePsychicHammerDamage(20)
|
|
expect(dmg.minPhys).toBe(56)
|
|
expect(dmg.minMagic).toBe(56)
|
|
expect(dmg.maxPhys).toBe(79)
|
|
expect(dmg.maxMagic).toBe(79)
|
|
|
|
expect(computePsychicHammerKnockbackChance(20, 'normal')).toBe(100)
|
|
expect(computePsychicHammerKnockbackChance(20, 'unique')).toBe(92)
|
|
expect(computePsychicHammerKnockbackChance(20, 'boss')).toBe(87)
|
|
})
|
|
})
|
|
|
|
// =========================================================================
|
|
// Scenario 8: Complete 30-Skill Assassin Tree Data Registry & Invariants
|
|
// =========================================================================
|
|
describe('Scenario 8: Complete 30-Skill Assassin Tree Data Registry & Architectural Invariants', () => {
|
|
it('verifies all 30 Assassin skills (IDs 251..280) exist in 1.13c Data Registry', async () => {
|
|
const registry = await getSharedDataRegistry()
|
|
// Assassin skill IDs are 251..280 inclusive
|
|
for (let id = 251; id <= 280; id++) {
|
|
const skill = registry.getSkillById(id)
|
|
expect(skill, `Skill ${id} must exist in registry`).toBeDefined()
|
|
expect(skill?.charClass).toBe('ass')
|
|
expect(skill?.name).toBeDefined()
|
|
expect(skill?.name.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
|
|
it('enforces BATCH1_SKILLS length invariant = strictly 38', () => {
|
|
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
|
})
|
|
})
|
|
})
|