299 lines
12 KiB
TypeScript
299 lines
12 KiB
TypeScript
/**
|
|
* Comprehensive Unit Test Suite for Paladin Skill Tree Modules:
|
|
* - src/game/skills/paladin-combat.ts
|
|
* - src/game/skills/paladin-offensive-auras.ts
|
|
* - src/game/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/game/skills/paladin-combat.ts'
|
|
import {
|
|
calculateMightStats,
|
|
calculateHolyFireStats,
|
|
calculateThornsStats,
|
|
calculateBlessedAimStats,
|
|
calculateConcentrationStats,
|
|
calculateHolyFreezeStats,
|
|
calculateHolyShockStats,
|
|
calculateSanctuaryStats,
|
|
calculateFanaticismStats,
|
|
calculateConvictionStats,
|
|
} from '../../../src/game/skills/paladin-offensive-auras.ts'
|
|
import {
|
|
calculatePrayerStats,
|
|
calculateResistFireStats,
|
|
calculateDefianceStats,
|
|
calculateResistColdStats,
|
|
calculateCleansingStats,
|
|
calculateResistLightningStats,
|
|
calculateVigorStats,
|
|
calculateMeditationStats,
|
|
calculateRedemptionStats,
|
|
calculateSalvationStats,
|
|
} from '../../../src/game/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)
|
|
})
|
|
})
|
|
})
|