/** * Cohort 4 — Paladin Combat Skills & Offensive Auras (IDs 96..115) * Adversarial Stress & Invariant Verification Suite * * Authored by: teamwork_preview_challenger (challenger_pal_1) * Methodology: Pre-submission solution stress testing (Differential testing, Oracles, Adversarial Edge Cases) * * Probes the 5 Mission-Critical Mechanics: * 1. Blessed Hammer 1.13c Magic Resistance Invariant (Undead/Demons do NOT bypass Magic Resistance; Magic Immunes take 0 damage) * 2. Smite Mechanics (AutoHit vs extreme defense, base shield + Holy Shield flat damage, Leech suppression from items, stun duration formula) * 3. Concentration Synergy to Blessed Hammer (Strictly 50% of listed %ED) * 4. Conviction Invariants (-150% floor, 1/5th efficiency vs elemental immunes >= 100%, elemental only - zero effect on poison/magic) * 5. Sacrifice Self-Damage (8% physical self-damage, caster HP clamped >= 1 HP) */ import { describe, expect, it } from 'vitest' import { getSharedDataRegistry } from '../../../src/baker/mpq-archive-source.ts' import { FIXED_ONE, UnitStatList } from '../../../src/server/engine/stat-list.ts' import { StateBus } from '../../../src/server/engine/state-bus.ts' import { computeEffectiveResistance, computeToHitChance, executeSUnitDmg, type CombatUnitContext, } from '../../../src/server/engine/combat-pipeline.ts' import { calculateSacrificeStats, calculateSmiteStats, calculateBlessedHammerDamage, calculateConcentrationStats, calculateConvictionStats, calculateHolyShieldStats, } from '../../../src/server/skills/skills.ts' describe('Cohort 4 — Paladin Adversarial Stress Suite (Skills 96..115)', () => { // ========================================================================== // 1. Blessed Hammer 1.13c Magic Resistance Invariant // ========================================================================== describe('1. Blessed Hammer 1.13c Magic Resistance Invariant', () => { it('Undead and Demon targets with magic resistance reduce Blessed Hammer damage accordingly', async () => { const registry = await getSharedDataRegistry() const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const paladin: CombatUnitContext = { id: 'paladin', name: 'Paladin', statList: palStats, stateBus: new StateBus(palStats, registry), unitType: 'player', } // Base packet: 1,000 magic damage const baseMagicDmg = 1000 // 1. Undead with 0% magic resistance -> gets +50% undead bonus = 1,500 damage const u0Stats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 0 }) const u0: CombatUnitContext = { id: 'u0', name: 'Undead 0%', statList: u0Stats, stateBus: new StateBus(u0Stats, registry), isUndead: true } const outU0 = executeSUnitDmg(paladin, u0, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseMagicDmg * FIXED_ONE, elemMax256: baseMagicDmg * FIXED_ONE, }) expect(outU0.elemDamage256).toBe(1500 * FIXED_ONE) // 2. Undead with 50% magic resistance -> 1,500 * (100 - 50)% = 750 damage const u50Stats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 50 }) const u50: CombatUnitContext = { id: 'u50', name: 'Undead 50%', statList: u50Stats, stateBus: new StateBus(u50Stats, registry), isUndead: true } const outU50 = executeSUnitDmg(paladin, u50, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseMagicDmg * FIXED_ONE, elemMax256: baseMagicDmg * FIXED_ONE, }) expect(outU50.elemDamage256).toBe(750 * FIXED_ONE) // 3. Demon with 0% magic resistance -> 1,000 damage const d0Stats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 0 }) const d0: CombatUnitContext = { id: 'd0', name: 'Demon 0%', statList: d0Stats, stateBus: new StateBus(d0Stats, registry), isDemon: true } const outD0 = executeSUnitDmg(paladin, d0, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseMagicDmg * FIXED_ONE, elemMax256: baseMagicDmg * FIXED_ONE, }) expect(outD0.elemDamage256).toBe(1000 * FIXED_ONE) // 4. Demon with 75% magic resistance -> 1,000 * 25% = 250 damage const d75Stats = new UnitStatList(registry, { maxhp: 5000 * FIXED_ONE, hitpoints: 5000 * FIXED_ONE, magicresist: 75 }) const d75: CombatUnitContext = { id: 'd75', name: 'Demon 75%', statList: d75Stats, stateBus: new StateBus(d75Stats, registry), isDemon: true } const outD75 = executeSUnitDmg(paladin, d75, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseMagicDmg * FIXED_ONE, elemMax256: baseMagicDmg * FIXED_ONE, }) expect(outD75.elemDamage256).toBe(250 * FIXED_ONE) }) it('CRITICAL 1.13c Invariant: Magic Immune targets (Undead, Demon, Living) take strictly 0 damage', async () => { const registry = await getSharedDataRegistry() const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const paladin: CombatUnitContext = { id: 'paladin', name: 'Paladin', statList: palStats, stateBus: new StateBus(palStats, registry), unitType: 'player', } // Test cases of Magic Immune enemies (baseRes >= 100) const immuneEnemies = [ { name: 'Achmel the Cursed (Undead Magic Immune 100%)', isUndead: true, isDemon: false, magicRes: 100 }, { name: 'Radament Minion (Undead Magic Immune 120%)', isUndead: true, isDemon: false, magicRes: 120 }, { name: 'Baal Minion (Demon Magic Immune 100%)', isUndead: false, isDemon: true, magicRes: 100 }, { name: 'Pit Lord (Demon Magic Immune 110%)', isUndead: false, isDemon: true, magicRes: 110 }, { name: 'Wendingo (Living Magic Immune 100%)', isUndead: false, isDemon: false, magicRes: 100 }, ] for (const enemyDef of immuneEnemies) { const stats = new UnitStatList(registry, { maxhp: 10000 * FIXED_ONE, hitpoints: 10000 * FIXED_ONE, magicresist: enemyDef.magicRes, }) const target: CombatUnitContext = { id: `target_${enemyDef.name}`, name: enemyDef.name, statList: stats, stateBus: new StateBus(stats, registry), isUndead: enemyDef.isUndead, isDemon: enemyDef.isDemon, unitType: 'monster', } const out = executeSUnitDmg(paladin, target, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: 2000 * FIXED_ONE, elemMax256: 2000 * FIXED_ONE, }) expect(out.immuneToElem, `${enemyDef.name} must be flagged immune`).toBe(true) expect(out.elemDamage256, `${enemyDef.name} must take strictly 0 damage`).toBe(0) expect(out.totalDamage, `${enemyDef.name} must take strictly 0 total damage`).toBe(0) expect(stats.getHp256(), `${enemyDef.name} HP must be completely untouched`).toBe(10000 * FIXED_ONE) } }) it('Differential Fuzzing: Fuzz 50 random resistance values [-50..150] across target types', async () => { const registry = await getSharedDataRegistry() const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const paladin: CombatUnitContext = { id: 'paladin', name: 'Paladin', statList: palStats, stateBus: new StateBus(palStats, registry), unitType: 'player', } for (let seed = 1; seed <= 50; seed++) { const rawRes = -50 + (seed * 4) // sweeps -46% to 150% const isUndead = seed % 2 === 0 const isDemon = !isUndead && seed % 3 === 0 const baseDamage = 1000 const targetStats = new UnitStatList(registry, { maxhp: 50000 * FIXED_ONE, hitpoints: 50000 * FIXED_ONE, magicresist: rawRes, }) const target: CombatUnitContext = { id: `target_${seed}`, name: `Fuzz Target ${seed}`, statList: targetStats, stateBus: new StateBus(targetStats, registry), isUndead, isDemon, unitType: 'monster', } const out = executeSUnitDmg(paladin, target, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseDamage * FIXED_ONE, elemMax256: baseDamage * FIXED_ONE, }) // Oracle calculation: const expectedPreRes = isUndead ? Math.trunc(baseDamage * 1.5) : baseDamage let expectedDamage = 0 if (rawRes < 100) { const effRes = Math.max(-100, Math.min(99, rawRes)) expectedDamage = Math.trunc((expectedPreRes * (100 - effRes)) / 100) } expect(out.elemDamage256 / FIXED_ONE).toBe(expectedDamage) if (rawRes >= 100) { expect(out.immuneToElem).toBe(true) } } }) }) // ========================================================================== // 2. Smite Mechanics // ========================================================================== describe('2. Smite Mechanics', () => { it('AutoHit Invariant: Smite always hits regardless of attacker AR (0) and defender Defense (10,000,000)', async () => { const registry = await getSharedDataRegistry() // Extreme test case: Attacker AR = 0, Defender Defense = 10,000,000, Defender Shield Block = 75% const attackerStats = new UnitStatList(registry, { level: 1, tohit: 0, }) const attacker: CombatUnitContext = { id: 'clueless_smiter', name: 'Paladin', statList: attackerStats, stateBus: new StateBus(attackerStats, registry), } const defenderStats = new UnitStatList(registry, { level: 99, armorclass: 10000000, // 10 million defense toblock: 75, // 75% max block hitpoints: 50000 * FIXED_ONE, maxhp: 50000 * FIXED_ONE, }) const defender: CombatUnitContext = { id: 'godlike_wall', name: 'Godlike Defender', statList: defenderStats, stateBus: new StateBus(defenderStats, registry), hasShield: true, } // RollToHit formula check directly const toHit = computeToHitChance({ attackerAr: 0, defenderDef: 10000000, attackerLvl: 1, defenderLvl: 99, autoHit: true, }) expect(toHit).toBe(100) // Test 100 attack rolls: every single roll in [0..99] must hit and never be blocked for (let roll = 0; roll < 100; roll++) { const out = executeSUnitDmg(attacker, defender, { skillId: 97, attackKind: 'melee', srcDam: 0, flatPhysMin256: 50 * FIXED_ONE, flatPhysMax256: 50 * FIXED_ONE, autoHit: true, unblockable: true, roll100: roll, }) expect(out.hit, `Smite must hit at roll ${roll}`).toBe(true) expect(out.avoidedReason).toBe('none') expect(out.physDamage256).toBe(50 * FIXED_ONE) } }) it('Base Damage Invariant: Shield min/max damage + Holy Shield flat damage bonus applies to Smite', () => { // 1. Plain shield without Holy Shield: Herald of Zakarum (Gilded Shield: 20..28 smite damage) const plainSmite = calculateSmiteStats(1, 20, 28) expect(plainSmite.minDamage).toBe(20) expect(plainSmite.maxDamage).toBe(28) expect(plainSmite.damagePct).toBe(15) // Slvl 1: +15% ED // 2. Slvl 20 Holy Shield: +57 flat min, +60 flat max const hs20 = calculateHolyShieldStats(20, 0) expect(hs20.smiteMinFlat).toBe(57) expect(hs20.smiteMaxFlat).toBe(60) // 3. Combined Smite + Holy Shield: (20 + 57) .. (28 + 60) = 77 .. 88 base damage const buffedSmite = calculateSmiteStats(20, 20, 28, { min: hs20.smiteMinFlat, max: hs20.smiteMaxFlat }) expect(buffedSmite.minDamage).toBe(77) expect(buffedSmite.maxDamage).toBe(88) expect(buffedSmite.damagePct).toBe(15 + 19 * 15) // +300% ED }) it('Stun Duration Formula: Stun frames = min(250, 15 + (slvl - 1) * 5)', () => { // Oracle check across slvls 1 through 60 for (let slvl = 1; slvl <= 60; slvl++) { const expected = Math.min(250, 15 + (slvl - 1) * 5) const stats = calculateSmiteStats(slvl, 10, 20) expect(stats.stunDurationFrames).toBe(expected) } // Explicit boundary values: expect(calculateSmiteStats(1, 10, 20).stunDurationFrames).toBe(15) // 0.6s expect(calculateSmiteStats(10, 10, 20).stunDurationFrames).toBe(60) // 2.4s expect(calculateSmiteStats(20, 10, 20).stunDurationFrames).toBe(110) // 4.4s expect(calculateSmiteStats(47, 10, 20).stunDurationFrames).toBe(245) // 9.8s expect(calculateSmiteStats(48, 10, 20).stunDurationFrames).toBe(250) // 10.0s (capped) expect(calculateSmiteStats(50, 10, 20).stunDurationFrames).toBe(250) // 10.0s (capped) }) it('Leech Suppression: Life leech and mana leech from items are strictly 0 when attacking with Smite', async () => { const registry = await getSharedDataRegistry() const initialHp = 500 * FIXED_ONE const initialMana = 500 * FIXED_ONE // Attacker is equipped with 20% Life Leech and 20% Mana Leech from items const attackerStats = new UnitStatList(registry, { hitpoints: initialHp, maxhp: 1000 * FIXED_ONE, mana: initialMana, maxmana: 1000 * FIXED_ONE, lifesteal: 20, manasteal: 20, }) const attacker: CombatUnitContext = { id: 'smiter_with_leech_gear', name: 'Paladin', statList: attackerStats, stateBus: new StateBus(attackerStats, registry), unitType: 'player', } const defenderStats = new UnitStatList(registry, { hitpoints: 50000 * FIXED_ONE, maxhp: 50000 * FIXED_ONE, damageresist: 0, }) const defender: CombatUnitContext = { id: 'target_dummy', name: 'Training Dummy', statList: defenderStats, stateBus: new StateBus(defenderStats, registry), unitType: 'monster', } // Execute Smite dealing 500 physical damage const out = executeSUnitDmg(attacker, defender, { skillId: 97, attackKind: 'melee', srcDam: 0, flatPhysMin256: 500 * FIXED_ONE, flatPhysMax256: 500 * FIXED_ONE, autoHit: true, unblockable: true, }) expect(out.hit).toBe(true) expect(out.physDamage256).toBe(500 * FIXED_ONE) // In Diablo II v1.13c ground truth: // Smite attacks CANNOT leech life or mana from gear! (Arreat Summit & D2Common/D2Game ground truth) // Attacker HP and Mana must strictly remain at 500 (0 leeched), NOT 600. expect(attackerStats.getHp256()).toBe(initialHp) expect(attackerStats.getMana256()).toBe(initialMana) }) }) // ========================================================================== // 3. Concentration Synergy to Blessed Hammer // ========================================================================== describe('3. Concentration Synergy to Blessed Hammer', () => { it('Concentration aura provides exactly 50% of its listed % enhanced damage bonus to Blessed Hammer', () => { // Test across slvl 1 through 30 of Concentration for (let slvl = 1; slvl <= 30; slvl++) { const concStats = calculateConcentrationStats(slvl) const expectedBonus = Math.trunc(concStats.damagePercent / 2) expect(concStats.blessedHammerBonusPct).toBe(expectedBonus) } // Concrete sample verification: // slvl 1: 60% ED -> 30% to Hammer expect(calculateConcentrationStats(1).blessedHammerBonusPct).toBe(30) // slvl 10: 60 + 9*15 = 195% ED -> 97% to Hammer expect(calculateConcentrationStats(10).blessedHammerBonusPct).toBe(97) // slvl 20: 60 + 19*15 = 345% ED -> 172% to Hammer expect(calculateConcentrationStats(20).blessedHammerBonusPct).toBe(172) }) it('Runtime executeSUnitDmg scales Blessed Hammer damage by exactly 50% of active Concentration aura %ED', async () => { const registry = await getSharedDataRegistry() const baseHammerDamage = 100 // Baseline: Paladin without Concentration const palNoConcStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const palNoConc: CombatUnitContext = { id: 'pal_no_conc', name: 'Paladin', statList: palNoConcStats, stateBus: new StateBus(palNoConcStats, registry), unitType: 'player', } const targetStats = new UnitStatList(registry, { maxhp: 50000 * FIXED_ONE, hitpoints: 50000 * FIXED_ONE, magicresist: 0 }) const target: CombatUnitContext = { id: 'target', name: 'Monster', statList: targetStats, stateBus: new StateBus(targetStats, registry), unitType: 'monster', } const baseOut = executeSUnitDmg(palNoConc, target, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseHammerDamage * FIXED_ONE, elemMax256: baseHammerDamage * FIXED_ONE, }) expect(baseOut.elemDamage256).toBe(100 * FIXED_ONE) // Test with various Concentration levels (e.g. 100%, 200%, 345% listed ED) const testCases = [ { listedConcEd: 100, expectedMult: 1.5 }, // +50% -> 150 { listedConcEd: 200, expectedMult: 2.0 }, // +100% -> 200 { listedConcEd: 300, expectedMult: 2.5 }, // +150% -> 250 { listedConcEd: 345, expectedMult: 2.72 }, // +172% -> 272 ] for (const tc of testCases) { const out = executeSUnitDmg(palNoConc, target, { skillId: 112, attackKind: 'spell', elemType: 'mag', elemMin256: baseHammerDamage * FIXED_ONE, elemMax256: baseHammerDamage * FIXED_ONE, concentrationBonusPct: tc.listedConcEd, }) const expectedDmg = Math.trunc((baseHammerDamage * (100 + Math.trunc(tc.listedConcEd / 2))) / 100) expect(out.elemDamage256 / FIXED_ONE).toBe(expectedDmg) } }) }) // ========================================================================== // 4. Conviction Invariants // ========================================================================== describe('4. Conviction Invariants', () => { it('-150% Resistance Floor: Conviction elemental resistance reduction cannot exceed -150% even at extreme slvls', () => { // Slvl 1 to 25 scales: 30 + (slvl - 1) * 5 expect(calculateConvictionStats(1).resistanceReductionPct).toBe(30) expect(calculateConvictionStats(10).resistanceReductionPct).toBe(75) expect(calculateConvictionStats(20).resistanceReductionPct).toBe(125) expect(calculateConvictionStats(25).resistanceReductionPct).toBe(150) // Slvl 26 through 99 must strictly cap at 150% for (let slvl = 26; slvl <= 99; slvl++) { const stats = calculateConvictionStats(slvl) expect(stats.resistanceReductionPct, `Slvl ${slvl} must cap at 150`).toBe(150) expect(stats.immunityBreakingReductionPct, `Slvl ${slvl} 1/5th cap must be 30`).toBe(30) } }) it('1/5th Efficiency vs Immunes: Against monsters with base elemental res >= 100%, Conviction applies at 1/5th efficiency', () => { const convPierce = 150 // Slvl 25 Conviction (-150%) // Non-immune monster (< 100%): Full 150% applied const resNonImmune = computeEffectiveResistance({ baseRes: 99, convictionPierce: convPierce }) expect(resNonImmune.convictionAndLRApplied).toBe(150) expect(resNonImmune.isImmune).toBe(false) expect(resNonImmune.effectiveRes).toBe(-51) // 99 - 150 = -51% // Immune monsters (>= 100%): 1/5th efficiency = Math.trunc(150 / 5) = 30% applied const immuneCases = [ { baseRes: 100, expectedApplied: 30, expectedRes: 70, expectedImmune: false }, { baseRes: 110, expectedApplied: 30, expectedRes: 80, expectedImmune: false }, { baseRes: 125, expectedApplied: 30, expectedRes: 95, expectedImmune: false }, { baseRes: 129, expectedApplied: 30, expectedRes: 99, expectedImmune: false }, { baseRes: 130, expectedApplied: 30, expectedRes: 100, expectedImmune: true }, // Unbroken! { baseRes: 140, expectedApplied: 30, expectedRes: 100, expectedImmune: true }, // Unbroken! { baseRes: 160, expectedApplied: 30, expectedRes: 100, expectedImmune: true }, // Unbroken! ] for (const tc of immuneCases) { const res = computeEffectiveResistance({ baseRes: tc.baseRes, convictionPierce: convPierce }) expect(res.convictionAndLRApplied, `baseRes ${tc.baseRes} must have 1/5th efficiency`).toBe(tc.expectedApplied) expect(res.isImmune, `baseRes ${tc.baseRes} immunity check`).toBe(tc.expectedImmune) expect(res.effectiveRes, `baseRes ${tc.baseRes} effective resistance`).toBe(tc.expectedRes) } }) it('Elemental Only: Conviction does NOT reduce Poison resistance or Magic resistance', async () => { const registry = await getSharedDataRegistry() const palStats = new UnitStatList(registry, { maxhp: 1000 * FIXED_ONE, hitpoints: 1000 * FIXED_ONE }) const paladin: CombatUnitContext = { id: 'paladin', name: 'Paladin', statList: palStats, stateBus: new StateBus(palStats, registry), unitType: 'player', } // Defender with active Conviction state (-150% conviction pierce) const defStats = new UnitStatList(registry, { maxhp: 10000 * FIXED_ONE, hitpoints: 10000 * FIXED_ONE, fireresist: 50, poisonresist: 50, magicresist: 50, }) const defBus = new StateBus(defStats, registry) defBus.applyState({ stateNameOrId: 'conviction', slvl: 25, stats: { conviction_pierce: 150, item_armor_percent: -90 }, }) const defender: CombatUnitContext = { id: 'convicted_monster', name: 'Convicted Monster', statList: defStats, stateBus: defBus, unitType: 'monster', } // 1. Fire attack (Fire is pierced by Conviction: 50% - 150% = -100% res -> takes 2x damage) const fireOut = executeSUnitDmg(paladin, defender, { skillId: 0, attackKind: 'melee', elemType: 'fire', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }) expect(fireOut.elemDamage256).toBe(200 * FIXED_ONE) // 2. Poison attack (Conviction does NOT pierce Poison: 50% res remains -> takes 50 damage) const poisOut = executeSUnitDmg(paladin, defender, { skillId: 0, attackKind: 'melee', elemType: 'pois', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }) expect(poisOut.elemDamage256).toBe(50 * FIXED_ONE) // 3. Magic attack (Conviction does NOT pierce Magic: 50% res remains -> takes 50 damage) const magOut = executeSUnitDmg(paladin, defender, { skillId: 0, attackKind: 'spell', elemType: 'mag', elemMin256: 100 * FIXED_ONE, elemMax256: 100 * FIXED_ONE, autoHit: true, }) expect(magOut.elemDamage256).toBe(50 * FIXED_ONE) }) }) // ========================================================================== // 5. Sacrifice Self-Damage // ========================================================================== describe('5. Sacrifice Self-Damage', () => { it('Calculates exact 8% physical self-damage per hit', async () => { const registry = await getSharedDataRegistry() const initialHp = 10000 * FIXED_ONE const attackerStats = new UnitStatList(registry, { hitpoints: initialHp, maxhp: 10000 * FIXED_ONE, }) const attacker: CombatUnitContext = { id: 'sacrificer', name: 'Paladin', statList: attackerStats, stateBus: new StateBus(attackerStats, registry), unitType: 'player', } const defenderStats = new UnitStatList(registry, { hitpoints: 100000 * FIXED_ONE, maxhp: 100000 * FIXED_ONE, damageresist: 0, }) const defender: CombatUnitContext = { id: 'punching_bag', name: 'Monster', statList: defenderStats, stateBus: new StateBus(defenderStats, registry), unitType: 'monster', } // Deal 2,500 damage -> 8% self-damage = 200 damage const out = executeSUnitDmg(attacker, defender, { skillId: 96, attackKind: 'melee', srcDam: 0, flatPhysMin256: 2500 * FIXED_ONE, flatPhysMax256: 2500 * FIXED_ONE, selfDamagePct: 8, autoHit: true, }) expect(out.hit).toBe(true) expect(out.physDamage256).toBe(2500 * FIXED_ONE) expect(out.selfDamageTaken256).toBe(200 * FIXED_ONE) expect(attackerStats.getHp256()).toBe((10000 - 200) * FIXED_ONE) }) it('Suicide Prevention Invariant: Caster HP strictly does NOT drop below 1 from self-damage', async () => { const registry = await getSharedDataRegistry() // Case A: Attacker has 10 HP remaining, deals 100,000 damage (8,000 self-damage) const atkStatsA = new UnitStatList(registry, { hitpoints: 10 * FIXED_ONE, maxhp: 1000 * FIXED_ONE, }) const attackerA: CombatUnitContext = { id: 'low_hp_pal', name: 'Paladin', statList: atkStatsA, stateBus: new StateBus(atkStatsA, registry), unitType: 'player', } const defStats = new UnitStatList(registry, { hitpoints: 500000 * FIXED_ONE, maxhp: 500000 * FIXED_ONE, damageresist: 0, }) const defender: CombatUnitContext = { id: 'target', name: 'Monster', statList: defStats, stateBus: new StateBus(defStats, registry), unitType: 'monster', } const outA = executeSUnitDmg(attackerA, defender, { skillId: 96, attackKind: 'melee', srcDam: 0, flatPhysMin256: 100000 * FIXED_ONE, flatPhysMax256: 100000 * FIXED_ONE, selfDamagePct: 8, autoHit: true, }) expect(outA.hit).toBe(true) expect(outA.selfDamageTaken256).toBe(8000 * FIXED_ONE) // Clamped to exactly 1 HP (256 in FIXED_ONE), preventing death expect(atkStatsA.getHp256()).toBe(FIXED_ONE) // Case B: Attacker already at 1 HP, deals 50,000 damage const atkStatsB = new UnitStatList(registry, { hitpoints: FIXED_ONE, maxhp: 1000 * FIXED_ONE, }) const attackerB: CombatUnitContext = { id: 'one_hp_pal', name: 'Paladin', statList: atkStatsB, stateBus: new StateBus(atkStatsB, registry), unitType: 'player', } const outB = executeSUnitDmg(attackerB, defender, { skillId: 96, attackKind: 'melee', srcDam: 0, flatPhysMin256: 50000 * FIXED_ONE, flatPhysMax256: 50000 * FIXED_ONE, selfDamagePct: 8, autoHit: true, }) expect(outB.hit).toBe(true) expect(atkStatsB.getHp256()).toBe(FIXED_ONE) }) it('Physical Immune target: 0 physical damage dealt -> 0 self-damage taken', async () => { const registry = await getSharedDataRegistry() const atkStats = new UnitStatList(registry, { hitpoints: 1000 * FIXED_ONE, maxhp: 1000 * FIXED_ONE }) const attacker: CombatUnitContext = { id: 'paladin', name: 'Paladin', statList: atkStats, stateBus: new StateBus(atkStats, registry), unitType: 'player', } const defStats = new UnitStatList(registry, { hitpoints: 10000 * FIXED_ONE, maxhp: 10000 * FIXED_ONE, damageresist: 100, // Physical Immune }) const defender: CombatUnitContext = { id: 'immune_target', name: 'Ghost', statList: defStats, stateBus: new StateBus(defStats, registry), unitType: 'monster', } const out = executeSUnitDmg(attacker, defender, { skillId: 96, attackKind: 'melee', srcDam: 0, flatPhysMin256: 1000 * FIXED_ONE, flatPhysMax256: 1000 * FIXED_ONE, selfDamagePct: 8, autoHit: true, }) expect(out.hit).toBe(true) expect(out.physDamage256).toBe(0) expect(out.selfDamageTaken256).toBeUndefined() expect(atkStats.getHp256()).toBe(1000 * FIXED_ONE) // No self damage! }) }) })