fix(skills): route Blizzard damage calculation, eliminate shard silent fallback, and correct blizzardexplode3 celFile (Ref #400)
This commit is contained in:
parent
adaef0d222
commit
39de19364f
|
|
@ -614,7 +614,7 @@ export const CANONICAL_113C_MISSILES: Readonly<Record<string, MissileTxtData>> =
|
|||
maxVel: 0,
|
||||
range: 6,
|
||||
levRange: 0,
|
||||
celFile: 'icestormimpactvar01',
|
||||
celFile: 'icestormimpactvar02',
|
||||
animLen: 6,
|
||||
animSpeed: 16,
|
||||
loopAnim: 0,
|
||||
|
|
@ -3719,7 +3719,7 @@ export function tickProjectiles(
|
|||
vx: 0,
|
||||
vy: 0,
|
||||
altitude: 120,
|
||||
damage: projectile.damage > 0 ? projectile.damage : 45,
|
||||
damage: projectile.damage,
|
||||
ttl: 9,
|
||||
fromPlayer: projectile.fromPlayer,
|
||||
missileType: shardType,
|
||||
|
|
@ -4041,12 +4041,21 @@ export function getSkillCooldownTicks(skillId: number): number {
|
|||
* @param weaponDamage - Optional base weapon damage range { min, max }.
|
||||
*/
|
||||
export function calculateSkillDamage(
|
||||
skillId: number,
|
||||
effectiveLevel: number,
|
||||
synergyMultiplier: number = 1.0,
|
||||
skillId: number | string,
|
||||
slvl: number,
|
||||
synergies?: number | {
|
||||
readonly iceBolt?: number | undefined
|
||||
readonly iceBlast?: number | undefined
|
||||
readonly glacialSpike?: number | undefined
|
||||
},
|
||||
weaponDamage?: { min: number; max: number }
|
||||
): { min: number; max: number } {
|
||||
const def = getBatch1SkillDef(skillId)
|
||||
if (skillId === 59 || skillId === '59' || skillId === 'blizzard') {
|
||||
return calculateBlizzardDamage(slvl, synergies)
|
||||
}
|
||||
|
||||
const numericSkillId = typeof skillId === 'number' ? skillId : Number(skillId)
|
||||
const def = Number.isFinite(numericSkillId) ? getBatch1SkillDef(numericSkillId) : undefined
|
||||
if (!def) {
|
||||
if (weaponDamage) {
|
||||
const wMin = Number.isFinite(weaponDamage.min) ? Math.max(0, Math.floor(weaponDamage.min)) : 0
|
||||
|
|
@ -4056,7 +4065,7 @@ export function calculateSkillDamage(
|
|||
return { min: 0, max: 0 }
|
||||
}
|
||||
|
||||
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
|
||||
const lvl = Number.isFinite(slvl) ? Math.max(1, Math.floor(slvl)) : 1
|
||||
let min = def.emin
|
||||
let max = def.emax
|
||||
|
||||
|
|
@ -4101,7 +4110,7 @@ export function calculateSkillDamage(
|
|||
max *= shiftMultiplier
|
||||
|
||||
// Apply synergy multiplier
|
||||
const syn = Number.isFinite(synergyMultiplier) && synergyMultiplier > 0 ? synergyMultiplier : 1.0
|
||||
const syn = typeof synergies === 'number' && Number.isFinite(synergies) && synergies > 0 ? synergies : 1.0
|
||||
min = Math.floor(min * syn)
|
||||
max = Math.floor(max * syn)
|
||||
|
||||
|
|
@ -4139,10 +4148,15 @@ export function calculateBlizzardDamage(
|
|||
readonly iceBolt?: number | undefined
|
||||
readonly iceBlast?: number | undefined
|
||||
readonly glacialSpike?: number | undefined
|
||||
} = {},
|
||||
} | number = {},
|
||||
): { min: number; max: number } {
|
||||
const syn = calculateBlizzardSynergyMultiplier(synergies)
|
||||
const lvl = Math.max(1, Math.floor(effectiveLevel))
|
||||
let syn = 1.0
|
||||
if (typeof synergies === 'number') {
|
||||
syn = Number.isFinite(synergies) && synergies > 0 ? synergies : 1.0
|
||||
} else if (synergies && typeof synergies === 'object') {
|
||||
syn = calculateBlizzardSynergyMultiplier(synergies)
|
||||
}
|
||||
const lvl = Number.isFinite(effectiveLevel) ? Math.max(1, Math.floor(effectiveLevel)) : 1
|
||||
let min = 45
|
||||
let max = 75
|
||||
if (lvl > 1) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
/**
|
||||
* 1.13c Numerical & Runtime Verification Suite — Skill #059: Blizzard (SOR)
|
||||
* Gitea Tracking Issue: #213
|
||||
* Gitea Tracking Issue: #213 / #400
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getSharedDataRegistry } from '../../../src/game/engine/data-registry.ts'
|
||||
import { UnitStatList } from '../../../src/game/engine/stat-list.ts'
|
||||
import { WorldArena } from '../../../src/game/engine/world-arena.ts'
|
||||
import { skillModule } from '../../../src/game/skills/impl/sor/skill-059-blizzard.ts'
|
||||
import {
|
||||
calculateSkillDamage,
|
||||
calculateBlizzardDamage,
|
||||
tickProjectiles,
|
||||
CANONICAL_113C_MISSILES,
|
||||
type Projectile,
|
||||
} from '../../../src/game/skills.ts'
|
||||
|
||||
describe('[Skill #059] Blizzard (SOR) — 1.13c Parity (Issue #213)', () => {
|
||||
describe('[Skill #059] Blizzard (SOR) — 1.13c Parity (Issue #213 / #400)', () => {
|
||||
it('evaluates exact 1.13c Skills.txt / Missiles.txt formulas at slvl 1, 10, and 20', async () => {
|
||||
const registry = await getSharedDataRegistry()
|
||||
const skillRec = registry.getSkillById(59)!
|
||||
|
|
@ -106,4 +113,103 @@ describe('[Skill #059] Blizzard (SOR) — 1.13c Parity (Issue #213)', () => {
|
|||
expect(dbg.actionFrameTriggered).toBe(true)
|
||||
expect(outcome?.executed).toBe(true)
|
||||
})
|
||||
|
||||
it('calculateSkillDamage routes Skill 59 and evaluates exact 1.13c damage ranges at slvl 1, 10, 20', () => {
|
||||
// slvl 1: { min: 45, max: 75 }
|
||||
const d1 = calculateSkillDamage(59, 1)
|
||||
expect(d1).toEqual({ min: 45, max: 75 })
|
||||
|
||||
// slvl 10: { min: 210, max: 249 }
|
||||
const d10 = calculateSkillDamage(59, 10)
|
||||
expect(d10).toEqual({ min: 210, max: 249 })
|
||||
|
||||
// slvl 20: { min: 570, max: 619 }
|
||||
const d20 = calculateSkillDamage(59, 20)
|
||||
expect(d20).toEqual({ min: 570, max: 619 })
|
||||
|
||||
// String skill ID routing
|
||||
expect(calculateSkillDamage('59', 1)).toEqual({ min: 45, max: 75 })
|
||||
expect(calculateSkillDamage('blizzard', 20)).toEqual({ min: 570, max: 619 })
|
||||
|
||||
// Synergy integration via multiplier and synergy object
|
||||
expect(calculateSkillDamage(59, 20, 2.0)).toEqual({ min: 1140, max: 1238 })
|
||||
expect(calculateSkillDamage(59, 20, { iceBolt: 20 })).toEqual({ min: 1140, max: 1238 })
|
||||
})
|
||||
|
||||
it('blizzardcenter missile and falling shards scale with slvl and do not default to 45', () => {
|
||||
const openTerrain = { overlap: () => 0 }
|
||||
|
||||
// Case A: slvl = 1 -> damage range { 45, 75 } -> average 60
|
||||
const dmgRange1 = calculateSkillDamage(59, 1)
|
||||
const avgDamage1 = Math.round((dmgRange1.min + dmgRange1.max) / 2)
|
||||
expect(avgDamage1).toBe(60)
|
||||
|
||||
const centerL1: Projectile = {
|
||||
skillId: '59',
|
||||
x: 200,
|
||||
y: 200,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
damage: avgDamage1,
|
||||
ttl: 100,
|
||||
fromPlayer: true,
|
||||
missileType: 'blizzardcenter',
|
||||
subMissileIdx: 0,
|
||||
ageTicks: 3, // next tick will be tick 4 -> emit shard
|
||||
}
|
||||
|
||||
const outcomeL1 = tickProjectiles([centerL1], [], openTerrain)
|
||||
expect(outcomeL1.spawnedProjectiles).toBeDefined()
|
||||
expect(outcomeL1.spawnedProjectiles!.length).toBe(1)
|
||||
const shardL1 = outcomeL1.spawnedProjectiles![0]!
|
||||
expect(shardL1.damage).toBe(60)
|
||||
expect(shardL1.damage).not.toBe(45)
|
||||
|
||||
// Case B: slvl = 20 -> damage range { 570, 619 } -> average 595
|
||||
const dmgRange20 = calculateSkillDamage(59, 20)
|
||||
const avgDamage20 = Math.round((dmgRange20.min + dmgRange20.max) / 2)
|
||||
expect(avgDamage20).toBe(595)
|
||||
|
||||
const centerL20: Projectile = {
|
||||
skillId: '59',
|
||||
x: 200,
|
||||
y: 200,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
damage: avgDamage20,
|
||||
ttl: 100,
|
||||
fromPlayer: true,
|
||||
missileType: 'blizzardcenter',
|
||||
subMissileIdx: 0,
|
||||
ageTicks: 3, // next tick will be tick 4 -> emit shard
|
||||
}
|
||||
|
||||
const outcomeL20 = tickProjectiles([centerL20], [], openTerrain)
|
||||
expect(outcomeL20.spawnedProjectiles).toBeDefined()
|
||||
expect(outcomeL20.spawnedProjectiles!.length).toBe(1)
|
||||
const shardL20 = outcomeL20.spawnedProjectiles![0]!
|
||||
expect(shardL20.damage).toBe(595)
|
||||
expect(shardL20.damage).not.toBe(45)
|
||||
|
||||
// Shard expiry ground explosion inherits shard damage
|
||||
const expiringShard: Projectile = {
|
||||
skillId: '59',
|
||||
x: 200,
|
||||
y: 200,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
altitude: 0,
|
||||
damage: 595,
|
||||
ttl: 0,
|
||||
fromPlayer: true,
|
||||
missileType: 'blizzard1',
|
||||
}
|
||||
const expOutcome = tickProjectiles([expiringShard], [], openTerrain)
|
||||
expect(expOutcome.spawnedProjectiles?.length).toBe(1)
|
||||
expect(expOutcome.spawnedProjectiles![0]!.damage).toBe(595)
|
||||
})
|
||||
|
||||
it('CANONICAL_113C_MISSILES blizzardexplode3 matches Missiles.txt celFile icestormimpactvar02', () => {
|
||||
expect(CANONICAL_113C_MISSILES.blizzardexplode3.celFile).toBe('icestormimpactvar02')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue