498 lines
19 KiB
TypeScript
498 lines
19 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
BATCH1_SKILLS,
|
|
getBatch1SkillDef,
|
|
isBatch1Skill,
|
|
getSkillManaCost,
|
|
getSkillCooldownTicks,
|
|
calculateSkillDamage,
|
|
executeSkill,
|
|
type SkillExecutionContext,
|
|
type Batch1SkillDef,
|
|
} from '../src/game/skills.ts'
|
|
import type { CombatPlayer } from '../src/game/combat.ts'
|
|
|
|
function makeTestPlayer(overrides?: Partial<CombatPlayer>): CombatPlayer {
|
|
return {
|
|
x: 100,
|
|
y: 100,
|
|
facing: 0,
|
|
hp: 100,
|
|
maxHp: 100,
|
|
mana: 100,
|
|
maxMana: 100,
|
|
level: 30,
|
|
xp: 0,
|
|
cooldown: 0,
|
|
alive: true,
|
|
respawnIn: 0,
|
|
swingTicks: 0,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('Batch 1 Combat Skills Implementation (1.13c Parity)', () => {
|
|
describe('Skill Catalog & Coverage Across All 7 Classes', () => {
|
|
const expectedSkills: readonly { readonly id: number; readonly name: string; readonly charClass: string }[] = [
|
|
// Amazon (5)
|
|
{ id: 6, name: 'Magic Arrow', charClass: 'amazon' },
|
|
{ id: 7, name: 'Fire Arrow', charClass: 'amazon' },
|
|
{ id: 10, name: 'Jab', charClass: 'amazon' },
|
|
{ id: 11, name: 'Cold Arrow', charClass: 'amazon' },
|
|
{ id: 14, name: 'Power Strike', charClass: 'amazon' },
|
|
// Sorceress (6)
|
|
{ id: 36, name: 'Fire Bolt', charClass: 'sorceress' },
|
|
{ id: 37, name: 'Warmth', charClass: 'sorceress' },
|
|
{ id: 39, name: 'Ice Bolt', charClass: 'sorceress' },
|
|
{ id: 44, name: 'Frost Nova', charClass: 'sorceress' },
|
|
{ id: 47, name: 'Fire Ball', charClass: 'sorceress' },
|
|
{ id: 54, name: 'Teleport', charClass: 'sorceress' },
|
|
// Necromancer (5)
|
|
{ id: 66, name: 'Amplify Damage', charClass: 'necromancer' },
|
|
{ id: 67, name: 'Teeth', charClass: 'necromancer' },
|
|
{ id: 68, name: 'Bone Armor', charClass: 'necromancer' },
|
|
{ id: 70, name: 'Raise Skeleton', charClass: 'necromancer' },
|
|
{ id: 74, name: 'Corpse Explosion', charClass: 'necromancer' },
|
|
// Paladin (6)
|
|
{ id: 96, name: 'Sacrifice', charClass: 'paladin' },
|
|
{ id: 97, name: 'Smite', charClass: 'paladin' },
|
|
{ id: 98, name: 'Might', charClass: 'paladin' },
|
|
{ id: 101, name: 'Holy Bolt', charClass: 'paladin' },
|
|
{ id: 106, name: 'Zeal', charClass: 'paladin' },
|
|
{ id: 112, name: 'Blessed Hammer', charClass: 'paladin' },
|
|
// Barbarian (6)
|
|
{ id: 126, name: 'Bash', charClass: 'barbarian' },
|
|
{ id: 130, name: 'Howl', charClass: 'barbarian' },
|
|
{ id: 132, name: 'Leap', charClass: 'barbarian' },
|
|
{ id: 133, name: 'Double Swing', charClass: 'barbarian' },
|
|
{ id: 138, name: 'Shout', charClass: 'barbarian' },
|
|
{ id: 143, name: 'Leap Attack', charClass: 'barbarian' },
|
|
// Druid (5)
|
|
{ id: 223, name: 'Werewolf', charClass: 'druid' },
|
|
{ id: 224, name: 'Lycanthropy', charClass: 'druid' },
|
|
{ id: 225, name: 'Firestorm', charClass: 'druid' },
|
|
{ id: 226, name: 'Oak Sage', charClass: 'druid' },
|
|
{ id: 229, name: 'Molten Boulder', charClass: 'druid' },
|
|
// Assassin (5)
|
|
{ id: 251, name: 'Fire Blast', charClass: 'assassin' },
|
|
{ id: 252, name: 'Claw Mastery', charClass: 'assassin' },
|
|
{ id: 254, name: 'Tiger Strike', charClass: 'assassin' },
|
|
{ id: 258, name: 'Burst of Speed', charClass: 'assassin' },
|
|
{ id: 261, name: 'Charged Bolt Sentry', charClass: 'assassin' },
|
|
]
|
|
|
|
it('contains exactly 38 canonical skills across all 7 classes', () => {
|
|
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
|
for (const item of expectedSkills) {
|
|
expect(isBatch1Skill(item.id)).toBe(true)
|
|
const def = getBatch1SkillDef(item.id)
|
|
expect(def).toBeDefined()
|
|
expect(def!.name).toBe(item.name)
|
|
expect(def!.charClass).toBe(item.charClass)
|
|
}
|
|
})
|
|
|
|
it('returns undefined and false for non-batch1 skill IDs', () => {
|
|
expect(isBatch1Skill(0)).toBe(false)
|
|
expect(isBatch1Skill(999)).toBe(false)
|
|
expect(isBatch1Skill(-1)).toBe(false)
|
|
expect(getBatch1SkillDef(999)).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('Mana Cost Scaling & 24.8 Fixed-Point Clamping', () => {
|
|
it('scales standard spell mana costs accurately', () => {
|
|
// Fire Bolt (36): base 2.5 (shift 640), lvlmana 0 -> constant 2.5 mana across levels
|
|
expect(getSkillManaCost(36, 1)).toBe(2.5)
|
|
expect(getSkillManaCost(36, 5)).toBe(2.5)
|
|
expect(getSkillManaCost(36, 9)).toBe(2.5)
|
|
|
|
// Teeth (67): base 3.0 (shift 768), lvlmana 1 (shift 128 = 0.5 per level)
|
|
expect(getSkillManaCost(67, 1)).toBe(3.0)
|
|
expect(getSkillManaCost(67, 5)).toBe(5.0)
|
|
expect(getSkillManaCost(67, 9)).toBe(7.0)
|
|
})
|
|
|
|
it('handles decreasing mana costs reaching exactly 0.0 mana', () => {
|
|
// Magic Arrow (6): base 1.5, manaadd -0.125, minmana = 0
|
|
expect(getSkillManaCost(6, 1)).toBe(1.5)
|
|
expect(getSkillManaCost(6, 5)).toBe(1.0)
|
|
expect(getSkillManaCost(6, 9)).toBe(0.5)
|
|
expect(getSkillManaCost(6, 13)).toBe(0.0)
|
|
expect(getSkillManaCost(6, 20)).toBe(0.0)
|
|
|
|
// Double Swing (133): base 1.0, manaadd -0.125, minmana = 0
|
|
expect(getSkillManaCost(133, 1)).toBe(1.0)
|
|
expect(getSkillManaCost(133, 5)).toBe(0.5)
|
|
expect(getSkillManaCost(133, 9)).toBe(0.0)
|
|
expect(getSkillManaCost(133, 20)).toBe(0.0)
|
|
})
|
|
|
|
it('clamps decreasing mana cost at minmana = 1 for Teleport', () => {
|
|
// Teleport (54): base 24, manaadd -1, minmana = 1
|
|
expect(getSkillManaCost(54, 1)).toBe(24)
|
|
expect(getSkillManaCost(54, 10)).toBe(15)
|
|
expect(getSkillManaCost(54, 23)).toBe(2)
|
|
expect(getSkillManaCost(54, 24)).toBe(1)
|
|
expect(getSkillManaCost(54, 30)).toBe(1)
|
|
expect(getSkillManaCost(54, 50)).toBe(1)
|
|
})
|
|
|
|
it('returns 0 mana for passives and zero-cost auras', () => {
|
|
expect(getSkillManaCost(37, 1)).toBe(0) // Warmth
|
|
expect(getSkillManaCost(98, 1)).toBe(0) // Might
|
|
expect(getSkillManaCost(224, 1)).toBe(0) // Lycanthropy
|
|
expect(getSkillManaCost(252, 1)).toBe(0) // Claw Mastery
|
|
})
|
|
})
|
|
|
|
describe('Casting Delay & Cooldown Mechanics', () => {
|
|
it('returns exact 1.13c frame delays for skills with casting delays', () => {
|
|
// Werewolf: 25 frames (1.0 sec)
|
|
expect(getSkillCooldownTicks(223)).toBe(25)
|
|
// Firestorm: 15 frames (0.6 sec)
|
|
expect(getSkillCooldownTicks(225)).toBe(15)
|
|
// Molten Boulder: 50 frames (2.0 sec)
|
|
expect(getSkillCooldownTicks(229)).toBe(50)
|
|
})
|
|
|
|
it('returns 0 frames for skills without casting delays', () => {
|
|
expect(getSkillCooldownTicks(36)).toBe(0) // Fire Bolt
|
|
expect(getSkillCooldownTicks(47)).toBe(0) // Fire Ball
|
|
expect(getSkillCooldownTicks(54)).toBe(0) // Teleport
|
|
expect(getSkillCooldownTicks(106)).toBe(0) // Zeal
|
|
expect(getSkillCooldownTicks(112)).toBe(0) // Blessed Hammer
|
|
})
|
|
})
|
|
|
|
describe('Damage Calculation & Synergies', () => {
|
|
it('calculates spell damage across level bands with HitShift parity', () => {
|
|
// Fire Bolt (36): HitShift 7 (0.5x), base raw min 6 (3.0), max 12 (6.0)
|
|
const fbL1 = calculateSkillDamage(36, 1)
|
|
expect(fbL1.min).toBe(3)
|
|
expect(fbL1.max).toBe(6)
|
|
|
|
// Fire Bolt level 5: +4 levels * (min +2 raw (1.0), max +2 raw (1.0))
|
|
// min = (6 + 8) * 0.5 = 7, max = (12 + 8) * 0.5 = 10
|
|
const fbL5 = calculateSkillDamage(36, 5)
|
|
expect(fbL5.min).toBe(7)
|
|
expect(fbL5.max).toBe(10)
|
|
|
|
// Ice Bolt (39): HitShift 7, base raw min 6 (3.0), max 10 (5.0)
|
|
const ibL1 = calculateSkillDamage(39, 1)
|
|
expect(ibL1.min).toBe(3)
|
|
expect(ibL1.max).toBe(5)
|
|
|
|
// Teeth (67): HitShift 7, base raw min 4 (2.0), max 8 (4.0)
|
|
const teethL1 = calculateSkillDamage(67, 1)
|
|
expect(teethL1.min).toBe(2)
|
|
expect(teethL1.max).toBe(4)
|
|
})
|
|
|
|
it('multiplies base damage by synergyMultiplier accurately', () => {
|
|
const fbBase = calculateSkillDamage(47, 20) // Fire Ball L20
|
|
const synergyMult = 1.28 // +28% synergy bonus
|
|
const fbSynergized = calculateSkillDamage(47, 20, synergyMult)
|
|
|
|
expect(fbSynergized.min).toBe(Math.floor(fbBase.min * synergyMult))
|
|
expect(fbSynergized.max).toBe(Math.floor(fbBase.max * synergyMult))
|
|
|
|
// Blessed Hammer L20 with 2.10 synergy (Blessed Aim + Vigor)
|
|
const bhBase = calculateSkillDamage(112, 20)
|
|
const bhSynergized = calculateSkillDamage(112, 20, 2.10)
|
|
expect(bhSynergized.min).toBe(Math.floor(bhBase.min * 2.10))
|
|
expect(bhSynergized.max).toBe(Math.floor(bhBase.max * 2.10))
|
|
})
|
|
|
|
it('scales weapon damage for weapon-dependent skills', () => {
|
|
const weapon = { min: 10, max: 20 }
|
|
|
|
// Jab (10): srcDamage 128 (100% weapon damage)
|
|
const jab = calculateSkillDamage(10, 1, 1.0, weapon)
|
|
expect(jab.min).toBe(10)
|
|
expect(jab.max).toBe(20)
|
|
|
|
// Bash (126): srcDamage 128
|
|
const bash = calculateSkillDamage(126, 1, 1.0, weapon)
|
|
expect(bash.min).toBe(10)
|
|
expect(bash.max).toBe(20)
|
|
|
|
// Sacrifice (96): srcDamage 128
|
|
const sac = calculateSkillDamage(96, 1, 1.0, weapon)
|
|
expect(sac.min).toBe(10)
|
|
expect(sac.max).toBe(20)
|
|
})
|
|
})
|
|
|
|
describe('Skill Execution Mechanics Across Archetypes', () => {
|
|
it('blocks execution when cooldown casting delay is active', () => {
|
|
const player = makeTestPlayer({ cooldown: 15 })
|
|
const ctx: SkillExecutionContext = {
|
|
caster: player,
|
|
skillLevel: 5,
|
|
hardPoints: 5,
|
|
synergyMultiplier: 1.0,
|
|
}
|
|
// Firestorm (225) has a 15-frame delay
|
|
const res = executeSkill(225, ctx)
|
|
expect(res.kind).toBe('cooldown')
|
|
if (res.kind === 'cooldown') {
|
|
expect(res.ticksLeft).toBe(15)
|
|
}
|
|
})
|
|
|
|
it('blocks execution when player mana is insufficient', () => {
|
|
const player = makeTestPlayer({ mana: 5 }) // Teleport needs 24 mana at L1
|
|
const ctx: SkillExecutionContext = {
|
|
caster: player,
|
|
skillLevel: 1,
|
|
hardPoints: 1,
|
|
synergyMultiplier: 1.0,
|
|
}
|
|
const res = executeSkill(54, ctx)
|
|
expect(res.kind).toBe('mana')
|
|
if (res.kind === 'mana') {
|
|
expect(res.cost).toBe(24)
|
|
expect(res.currentMana).toBe(5)
|
|
}
|
|
// Mana was not deducted
|
|
expect(player.mana).toBe(5)
|
|
})
|
|
|
|
it('executes projectile skill and spawns valid projectile entity', () => {
|
|
const player = makeTestPlayer({ mana: 50, x: 100, y: 100, facing: 0 })
|
|
const target = { x: 200, y: 100 } // Directly to the right
|
|
const ctx: SkillExecutionContext = {
|
|
caster: player,
|
|
target,
|
|
skillLevel: 5,
|
|
hardPoints: 5,
|
|
synergyMultiplier: 1.0,
|
|
}
|
|
const res = executeSkill(36, ctx) // Fire Bolt
|
|
expect(res.kind).toBe('projectile')
|
|
if (res.kind === 'projectile') {
|
|
expect(res.projectile).toBeDefined()
|
|
expect(res.projectile.x).toBe(100)
|
|
expect(res.projectile.y).toBe(100)
|
|
expect(res.projectile.vx).toBeGreaterThan(0)
|
|
expect(res.projectile.vy).toBeCloseTo(0, 4)
|
|
expect(res.projectile.fromPlayer).toBe(true)
|
|
expect(res.manaCost).toBe(2.5)
|
|
}
|
|
expect(player.mana).toBe(47.5) // 50 - 2.5
|
|
})
|
|
|
|
it('executes multi-hit melee skills (Jab, Double Swing, Zeal)', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
const ctx: SkillExecutionContext = { caster: player, skillLevel: 1 }
|
|
|
|
// Jab (10): always 3 hits
|
|
const jab = executeSkill(10, ctx)
|
|
expect(jab.kind).toBe('melee')
|
|
if (jab.kind === 'melee') expect(jab.hits).toBe(3)
|
|
|
|
// Double Swing (133): always 2 hits
|
|
const ds = executeSkill(133, ctx)
|
|
expect(ds.kind).toBe('melee')
|
|
if (ds.kind === 'melee') expect(ds.hits).toBe(2)
|
|
|
|
// Zeal (106): level 1 -> 2 hits, level 3 -> 4 hits, level 4+ -> 5 hits cap
|
|
const zeal1 = executeSkill(106, { caster: player, skillLevel: 1 })
|
|
if (zeal1.kind === 'melee') expect(zeal1.hits).toBe(2)
|
|
|
|
const zeal3 = executeSkill(106, { caster: player, skillLevel: 3 })
|
|
if (zeal3.kind === 'melee') expect(zeal3.hits).toBe(4)
|
|
|
|
const zeal4 = executeSkill(106, { caster: player, skillLevel: 4 })
|
|
if (zeal4.kind === 'melee') expect(zeal4.hits).toBe(5)
|
|
|
|
const zeal20 = executeSkill(106, { caster: player, skillLevel: 20 })
|
|
if (zeal20.kind === 'melee') expect(zeal20.hits).toBe(5)
|
|
})
|
|
|
|
it('executes special melee mechanics (Knockback, Auto-Hit, Self-Damage)', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Bash (126): knockback
|
|
const bash = executeSkill(126, { caster: player, skillLevel: 1 })
|
|
if (bash.kind === 'melee') expect(bash.knockback).toBe(true)
|
|
|
|
// Smite (97): autoHit and knockback
|
|
const smite = executeSkill(97, { caster: player, skillLevel: 1 })
|
|
if (smite.kind === 'melee') {
|
|
expect(smite.autoHit).toBe(true)
|
|
expect(smite.knockback).toBe(true)
|
|
}
|
|
|
|
// Sacrifice (96): 8% self-damage
|
|
const sac = executeSkill(96, { caster: player, skillLevel: 1 })
|
|
if (sac.kind === 'melee') expect(sac.selfDamagePct).toBe(8)
|
|
})
|
|
|
|
it('executes area effect skills with appropriate radii', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Frost Nova (44): radius 240
|
|
const fn = executeSkill(44, { caster: player, skillLevel: 1 })
|
|
expect(fn.kind).toBe('area')
|
|
if (fn.kind === 'area') expect(fn.radius).toBe(240)
|
|
|
|
// Corpse Explosion (74): radius 80 + 20*(lvl-1)
|
|
const ce1 = executeSkill(74, { caster: player, skillLevel: 1 })
|
|
if (ce1.kind === 'area') expect(ce1.radius).toBe(80)
|
|
|
|
const ce5 = executeSkill(74, { caster: player, skillLevel: 5 })
|
|
if (ce5.kind === 'area') expect(ce5.radius).toBe(160)
|
|
})
|
|
|
|
it('executes buff skills with duration and stat effects', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Bone Armor (68)
|
|
const ba = executeSkill(68, { caster: player, skillLevel: 3 })
|
|
expect(ba.kind).toBe('buff')
|
|
if (ba.kind === 'buff') {
|
|
expect(ba.buffEffect.damageAbsorption).toBe(40) // 20 + 10*(3-1)
|
|
}
|
|
|
|
// Shout (138)
|
|
const shout = executeSkill(138, { caster: player, skillLevel: 2 })
|
|
expect(shout.kind).toBe('buff')
|
|
if (shout.kind === 'buff') {
|
|
expect(shout.buffEffect.defensePct).toBe(110) // 100 + 10*(2-1)
|
|
}
|
|
|
|
// Burst of Speed (258)
|
|
const bos = executeSkill(258, { caster: player, skillLevel: 1 })
|
|
expect(bos.kind).toBe('buff')
|
|
if (bos.kind === 'buff') {
|
|
expect(bos.buffEffect.attackSpeedPct).toBe(20)
|
|
expect(bos.buffEffect.walkRunSpeedPct).toBe(20)
|
|
}
|
|
})
|
|
|
|
it('executes curse debuff skills (Amplify Damage, Howl)', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Amplify Damage (66): physicalResistPenaltyPct = 100
|
|
const amp = executeSkill(66, { caster: player, skillLevel: 1 })
|
|
expect(amp.kind).toBe('curse')
|
|
if (amp.kind === 'curse') {
|
|
expect(amp.debuffEffect.physicalResistPenaltyPct).toBe(100)
|
|
expect(amp.radius).toBe(80)
|
|
}
|
|
|
|
// Howl (130): flee = 1
|
|
const howl = executeSkill(130, { caster: player, skillLevel: 1 })
|
|
expect(howl.kind).toBe('curse')
|
|
if (howl.kind === 'curse') {
|
|
expect(howl.debuffEffect.flee).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('executes movement skills with proper destination coordinates', () => {
|
|
const player = makeTestPlayer({ mana: 50, x: 200, y: 300 })
|
|
const target = { x: 450, y: 600 }
|
|
|
|
const tp = executeSkill(54, { caster: player, target, skillLevel: 1 })
|
|
expect(tp.kind).toBe('movement')
|
|
if (tp.kind === 'movement') {
|
|
expect(tp.destination).toEqual({ x: 450, y: 600 })
|
|
}
|
|
})
|
|
|
|
it('executes summon skills with proper minion counts', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Raise Skeleton (70): 1 at L1..2, 2 at L3..5, 3 at L6..8, etc.
|
|
const rs1 = executeSkill(70, { caster: player, skillLevel: 1 })
|
|
expect(rs1.kind).toBe('summon')
|
|
if (rs1.kind === 'summon') {
|
|
expect(rs1.summonType).toBe('skeleton')
|
|
expect(rs1.count).toBe(1)
|
|
}
|
|
|
|
const rs6 = executeSkill(70, { caster: player, skillLevel: 6 })
|
|
if (rs6.kind === 'summon') {
|
|
expect(rs6.count).toBe(3) // 1 + floor(6/3)
|
|
}
|
|
|
|
// Oak Sage (226)
|
|
const oak = executeSkill(226, { caster: player, skillLevel: 1 })
|
|
if (oak.kind === 'summon') {
|
|
expect(oak.summonType).toBe('oak_sage')
|
|
expect(oak.count).toBe(1)
|
|
}
|
|
})
|
|
|
|
it('executes aura and passive skills', () => {
|
|
const player = makeTestPlayer({ mana: 50 })
|
|
|
|
// Might (98): aura
|
|
const might = executeSkill(98, { caster: player, skillLevel: 1 })
|
|
expect(might.kind).toBe('aura')
|
|
if (might.kind === 'aura') {
|
|
expect(might.auraBonusPct).toBe(40)
|
|
}
|
|
|
|
// Warmth (37): passive
|
|
const warmth = executeSkill(37, { caster: player, skillLevel: 1 })
|
|
expect(warmth.kind).toBe('passive')
|
|
if (warmth.kind === 'passive') {
|
|
expect(warmth.passiveBonus.manaRegenPct).toBe(30)
|
|
}
|
|
|
|
// Claw Mastery (252): passive
|
|
const cm = executeSkill(252, { caster: player, skillLevel: 1 })
|
|
expect(cm.kind).toBe('passive')
|
|
if (cm.kind === 'passive') {
|
|
expect(cm.passiveBonus.clawDamagePct).toBe(35)
|
|
}
|
|
})
|
|
|
|
it('respects deductResources: false option', () => {
|
|
const player = makeTestPlayer({ mana: 100, cooldown: 0 })
|
|
const ctx: SkillExecutionContext = { caster: player, skillLevel: 1 }
|
|
|
|
// Werewolf (223) costs 15 mana and has 25f cooldown
|
|
const res = executeSkill(223, ctx, { deductResources: false })
|
|
expect(res.kind).toBe('buff')
|
|
expect(player.mana).toBe(100) // Unchanged
|
|
expect(player.cooldown).toBe(0) // Unchanged
|
|
})
|
|
})
|
|
|
|
describe('Adversarial & Input Hardening Protections', () => {
|
|
it('gracefully handles NaN and non-finite skill levels without throwing', () => {
|
|
expect(getSkillManaCost(36, NaN)).toBe(2.5)
|
|
expect(getSkillManaCost(36, -5)).toBe(2.5)
|
|
expect(getSkillManaCost(36, Infinity)).toBeDefined()
|
|
|
|
const dmg = calculateSkillDamage(36, NaN)
|
|
expect(Number.isFinite(dmg.min)).toBe(true)
|
|
expect(Number.isFinite(dmg.max)).toBe(true)
|
|
})
|
|
|
|
it('gracefully handles NaN synergy multiplier', () => {
|
|
const dmg = calculateSkillDamage(47, 20, NaN)
|
|
expect(Number.isFinite(dmg.min)).toBe(true)
|
|
expect(Number.isFinite(dmg.max)).toBe(true)
|
|
expect(dmg.min).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('handles zero-distance target vector without generating NaN', () => {
|
|
const player = makeTestPlayer({ x: 100, y: 100, facing: 0 })
|
|
const res = executeSkill(36, {
|
|
caster: player,
|
|
target: { x: 100, y: 100 }, // Same as caster
|
|
skillLevel: 1,
|
|
})
|
|
if (res.kind === 'projectile') {
|
|
expect(Number.isFinite(res.projectile.vx)).toBe(true)
|
|
expect(Number.isFinite(res.projectile.vy)).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
})
|