/** * Comprehensive Unit Test Suite for Paladin Skill Tree Modules: * - src/server/skills/paladin-combat.ts * - src/server/skills/paladin-offensive-auras.ts * - src/server/skills/paladin-defensive-auras.ts * * Verifies that all 30 Paladin skill calculators and interfaces are correctly exposed, * maintain exact 1.13c ground truth parity, and adhere to strict parity rules. */ import { describe, expect, it } from 'vitest' import { calculateSacrificeStats, calculateSmiteStats, calculateHolyBoltDamage, calculateHolyBoltHeal, calculateZealStats, calculateChargeStats, calculateVengeanceStats, calculateBlessedHammerDamage, calculateConversionStats, calculateHolyShieldStats, calculateFistOfTheHeavensDamage, calculateFistOfTheHeavensHolyBoltDamage, } from '../../../src/server/skills/paladin-combat.ts' import { calculateMightStats, calculateHolyFireStats, calculateThornsStats, calculateBlessedAimStats, calculateConcentrationStats, calculateHolyFreezeStats, calculateHolyShockStats, calculateSanctuaryStats, calculateFanaticismStats, calculateConvictionStats, } from '../../../src/server/skills/paladin-offensive-auras.ts' import { calculatePrayerStats, calculateResistFireStats, calculateDefianceStats, calculateResistColdStats, calculateCleansingStats, calculateResistLightningStats, calculateVigorStats, calculateMeditationStats, calculateRedemptionStats, calculateSalvationStats, } from '../../../src/server/skills/paladin-defensive-auras.ts' describe('Paladin Skill Tree Modules — 1.13c Parity & Interface Contracts', () => { describe('paladin-combat.ts (10 Combat Skills)', () => { it('Sacrifice (#96): calculates %ED, AR, self-damage (strictly 8%) and synergies', () => { const s1 = calculateSacrificeStats(1) expect(s1.toHitBonusPct).toBe(20) expect(s1.damagePct).toBe(180) expect(s1.selfDamagePct).toBe(8) const s20 = calculateSacrificeStats(20, { redemption: 20, fanaticism: 20 }) expect(s20.toHitBonusPct).toBe(20 + 19 * 7) // 153% expect(s20.damagePct).toBe(180 + 19 * 15 + 20 * 15 + 20 * 5) // 465 + 300 + 100 = 865% expect(s20.selfDamagePct).toBe(8) }) it('Smite (#97): calculates shield damage, Holy Shield bonus, and stun scaling', () => { const s1 = calculateSmiteStats(1, 2, 5, { min: 3, max: 6 }) expect(s1.damagePct).toBe(15) expect(s1.stunDurationFrames).toBe(15) expect(s1.minDamage).toBe(5) expect(s1.maxDamage).toBe(11) const s20 = calculateSmiteStats(20, 10, 20, { min: 57, max: 60 }) expect(s20.damagePct).toBe(300) expect(s20.stunDurationFrames).toBe(110) expect(s20.minDamage).toBe(67) expect(s20.maxDamage).toBe(80) }) it('Holy Bolt (#101): calculates undead magic damage and ally heal with synergies', () => { const dmg = calculateHolyBoltDamage(10, { blessedHammer: 10, fistOfTheHeavens: 5 }) expect(dmg.min).toBeGreaterThan(0) expect(dmg.max).toBeGreaterThan(dmg.min) const heal = calculateHolyBoltHeal(10, 10) expect(heal.min).toBeGreaterThan(0) expect(heal.max).toBeGreaterThan(heal.min) }) it('Zeal (#106): multi-hit progression clamps between 2 and 5 hits', () => { expect(calculateZealStats(1).hits).toBe(2) expect(calculateZealStats(2).hits).toBe(3) expect(calculateZealStats(3).hits).toBe(4) expect(calculateZealStats(4).hits).toBe(5) expect(calculateZealStats(20).hits).toBe(5) }) it('Charge (#107): calculates %ED, AR, synergies, and 300% movement speed multiplier', () => { const c = calculateChargeStats(10, { vigor: 10, might: 10 }) expect(c.speedMultiplier).toBe(3.0) expect(c.damagePct).toBe(100 + 9 * 25 + 20 * 20) }) it('Vengeance (#111): calculates Fire, Cold, Lightning elemental percentages and chill duration', () => { const v = calculateVengeanceStats(10, { resistFire: 10, resistCold: 10, resistLightning: 10, salvation: 10 }) expect(v.firePct).toBe(70 + 9 * 6 + 10 * 10 + 10 * 2) expect(v.coldPct).toBe(v.firePct) expect(v.ltngPct).toBe(v.firePct) expect(v.chillDurationFrames).toBe(30 + 9 * 15) }) it('Blessed Hammer (#112): calculates magic damage, synergies, and Concentration 50% bonus', () => { const bh = calculateBlessedHammerDamage(20, { blessedAim: 20, vigor: 20 }, 300) expect(bh.concentrationBonusPct).toBe(150) expect(bh.min).toBeGreaterThan(1000) expect(bh.max).toBeGreaterThan(bh.min) }) it('Conversion (#116): calculates dm34 conversion chance and 16s duration', () => { const conv = calculateConversionStats(10) expect(conv.durationSeconds).toBe(16) expect(conv.durationFrames).toBe(400) expect(conv.chancePct).toBe(Math.floor((10 * 50) / 20)) }) it('Holy Shield (#117): calculates duration, defense %, block %, and flat Smite bonus', () => { const hs = calculateHolyShieldStats(20, 20) expect(hs.durationFrames).toBe(750 + 19 * 625) expect(hs.defensePct).toBe(25 + 19 * 15 + 20 * 15) expect(hs.blockChancePct).toBe(35) // 1.13c dm56: 10 + Math.floor(66000 / 2600) = 35% expect(hs.smiteMinFlat).toBeGreaterThan(0) expect(hs.smiteMaxFlat).toBeGreaterThan(hs.smiteMinFlat) }) it('Fist of the Heavens (#121): calculates primary celestial lightning and radial holy bolts', () => { const foh = calculateFistOfTheHeavensDamage(20, { holyShock: 20 }) expect(foh.lightningMin).toBeGreaterThan(500) expect(foh.lightningMax).toBeGreaterThan(foh.lightningMin) const hb = calculateFistOfTheHeavensHolyBoltDamage(20, 20) expect(hb.holyBoltMin).toBeGreaterThan(100) expect(hb.holyBoltMax).toBeGreaterThan(hb.holyBoltMin) }) }) describe('paladin-offensive-auras.ts (10 Offensive Auras)', () => { it('Might (#98): calculates party %ED and radius scaling', () => { const m1 = calculateMightStats(1) expect(m1.damagePercent).toBe(40) const m10 = calculateMightStats(10) expect(m10.damagePercent).toBe(130) }) it('Holy Fire (#102): calculates radial pulse and weapon fire damage with synergies', () => { const hf = calculateHolyFireStats(20, { resistFire: 20, salvation: 20 }) expect(hf.synergyBonusPct).toBe(20 * 18 + 20 * 6) expect(hf.weaponFireMin).toBeGreaterThan(0) expect(hf.weaponFireMax).toBeGreaterThan(hf.weaponFireMin) }) it('Thorns (#103): calculates physical melee reflection percentage', () => { expect(calculateThornsStats(1).reflectPct).toBe(250) expect(calculateThornsStats(10).reflectPct).toBe(250 + 9 * 40) }) it('Blessed Aim (#108): calculates active AR and passive AR (+5%/blvl)', () => { const ba = calculateBlessedAimStats(10, 20) expect(ba.attackRatingPct).toBe(75 + 9 * 15) expect(ba.passiveArBonusPct).toBe(100) // 20 * 5% }) it('Concentration (#113): calculates party %ED, 20% uninterruptible chance, and +50% Blessed Hammer synergy', () => { const c = calculateConcentrationStats(20) expect(c.damagePercent).toBe(60 + 19 * 15) // 345% expect(c.uninterruptedChancePct).toBe(20) expect(c.blessedHammerBonusPct).toBe(Math.trunc(345 / 2)) // 172% }) it('Holy Freeze (#114): calculates slow % (capped at 54%), cold pulse, and weapon cold damage', () => { const hf = calculateHolyFreezeStats(20, { resistCold: 20, salvation: 20 }) expect(hf.slowPct).toBeLessThanOrEqual(54) expect(hf.weaponColdMin).toBeGreaterThan(0) expect(hf.weaponColdMax).toBeGreaterThan(hf.weaponColdMin) }) it('Holy Shock (#118): min damage is strictly 1, max scales with synergies', () => { const hsBase = calculateHolyShockStats(20) expect(hsBase.pulseMin).toBe(1) expect(hsBase.weaponLightningMin).toBe(1) expect(hsBase.weaponLightningMax).toBe(936) const hs = calculateHolyShockStats(20, { resistLightning: 20, salvation: 20 }) expect(hs.weaponLightningMin).toBe(1) expect(hs.synergyBonusPct).toBe(320) expect(hs.weaponLightningMax).toBeGreaterThan(1000) }) it('Sanctuary (#119): calculates undead %ED, AR, and magic pulse with Cleansing synergy', () => { const s = calculateSanctuaryStats(10, { cleansing: 10 }) expect(s.undeadDamagePercent).toBe(150 + 9 * 30) expect(s.undeadAttackRating).toBe(100 + 9 * 50) expect(s.synergyBonusPct).toBe(70) }) it('Fanaticism (#122): party %ED is strictly half of caster %ED', () => { const f1 = calculateFanaticismStats(1) expect(f1.casterDamagePercent).toBe(50) expect(f1.partyDamagePercent).toBe(25) // exactly half const f20 = calculateFanaticismStats(20) expect(f20.casterDamagePercent).toBe(50 + 19 * 17) // 373% expect(f20.partyDamagePercent).toBe(Math.trunc(373 / 2)) // 186% }) it('Conviction (#123): capped at -150% res reduction, 1/5th immunity breaking', () => { const c1 = calculateConvictionStats(1) expect(c1.defenseReductionPct).toBe(30) expect(c1.resistanceReductionPct).toBe(30) expect(c1.immunityBreakingReductionPct).toBe(6) const c20 = calculateConvictionStats(20) expect(c20.defenseReductionPct).toBe(90) // capped at -90% expect(c20.resistanceReductionPct).toBe(125) expect(c20.immunityBreakingReductionPct).toBe(25) const c30 = calculateConvictionStats(30) expect(c30.resistanceReductionPct).toBe(150) // capped at -150% expect(c30.immunityBreakingReductionPct).toBe(30) }) }) describe('paladin-defensive-auras.ts (10 Defensive Auras)', () => { it('Prayer (#99): periodic heal pulse and 16/256 mana drain per frame', () => { const p1 = calculatePrayerStats(1) expect(p1.healAmount).toBe(2) expect(p1.manaCostPerFrame256).toBe(16) const p20 = calculatePrayerStats(20) expect(p20.healAmount).toBe(25) expect(p20.manaCostPerFrame256).toBe(16) }) it('Resist Fire (#100): active fire res and passive +1% max res per 2 hard points', () => { const rf = calculateResistFireStats(10, 20) expect(rf.passiveMaxFireResistPct).toBe(10) // 20 / 2 = +10% expect(rf.maxFireResistBonusPct).toBe(10) }) it('Defiance (#104): active party defense % scaling', () => { expect(calculateDefianceStats(1).defensePercent).toBe(70) expect(calculateDefianceStats(20).defensePercent).toBe(70 + 19 * 10) }) it('Resist Cold (#105): active cold res and passive +1% max res per 2 hard points', () => { const rc = calculateResistColdStats(10, 14) expect(rc.passiveMaxColdResistPct).toBe(7) }) it('Cleansing (#109): curse/poison length reduction and free Prayer heal synergy', () => { const c = calculateCleansingStats(10, 10) expect(c.curseReductionPct).toBeGreaterThan(0) expect(c.poisonLengthReductionPct).toBe(c.curseReductionPct) expect(c.prayerHealAmount).toBe(11) // Prayer slvl 10 heal amount }) it('Resist Lightning (#110): active lightning res and passive +1% max res per 2 hard points', () => { const rl = calculateResistLightningStats(10, 18) expect(rl.passiveMaxLightningResistPct).toBe(9) }) it('Vigor (#115): FRW velocity %, stamina recovery %, max stamina %', () => { const v = calculateVigorStats(10) expect(v.velocityPercent).toBe(36) expect(v.staminaRecoveryBonusPct).toBe(50 + 9 * 25) expect(v.maxStaminaPercent).toBe(50 + 9 * 25) }) it('Meditation (#120): active mana recovery % and free Prayer heal synergy', () => { const m = calculateMeditationStats(10, 10) expect(m.manaRecoveryBonusPct).toBe(300 + 9 * 25) expect(m.prayerHealAmount).toBe(11) // Prayer slvl 10 heal amount }) it('Redemption (#124): 50-tick cadence, fixed 16 subtiles radius, dm34 chance, life/mana restore', () => { const r = calculateRedemptionStats(10) expect(r.radiusSubtiles).toBe(16) expect(r.lifeRecoverPts).toBe(70) expect(r.manaRecoverPts).toBe(70) expect(r.redemptionChancePct).toBe(71) }) it('Salvation (#125): party All Res (Fire, Cold, Lightning) with identical values', () => { const s = calculateSalvationStats(10) expect(s.fireResistPct).toBe(98) expect(s.coldResistPct).toBe(98) expect(s.lightningResistPct).toBe(98) }) }) })