Files
healer-man/src/game/romNativePeriodicDamage.test.ts
T
2026-10-04 13:13:35 -04:00

252 lines
9.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { deriveRomNativeStats } from "./romNativeStats";
import { EMPTY_ITEM_STATS } from "./itemStats";
import {
nativePeriodicDamageSnapshot,
nativePeriodicDamageTick,
romNativePeriodicDamageBinding,
romNativePeriodicDamageComponent,
type RomNativePeriodicDamageSource,
} from "./romNativePeriodicDamage";
const LEVEL = 30;
function nativeStats(classId: "rom-mage" | "rom-rogue" = "rom-mage") {
const stats = deriveRomNativeStats({
classId,
level: LEVEL,
raceId: "rom-human",
gender: "male",
gear: EMPTY_ITEM_STATS.romNative,
});
return {
...stats,
attributes: { ...stats.attributes, wisdom: 100 },
health: { max: 1_000 },
physicalAttack: 400,
physicalDamage: 200,
physicalDefense: 200,
magicalAttack: 500,
magicalDamage: 100,
magicalDefense: 100,
healing: {
...stats.healing,
healPowerRate: 1,
healPoint: 0,
targetHealAbsorbRate: 0,
targetHealAbsorb: 0,
},
};
}
function sourceFor(componentId: number, sourceSkillId: number): RomNativePeriodicDamageSource {
const binding = romNativePeriodicDamageBinding({ skillId: sourceSkillId, componentId, kind: "dot" });
if (!binding) throw new Error(`Missing generated DotMagic binding ${sourceSkillId}:${componentId}.`);
return binding;
}
describe("native RoM periodic damage", () => {
it("exports exact active-five source rows and can find shared component metadata", () => {
const binding = sourceFor(620313, 490306);
expect(binding).toMatchObject({
abilityId: "rom-rogue-general-0",
classId: "rom-rogue",
rank: 1,
sourceSkillId: 490306,
component: { componentId: 620313, kind: "dot", dotType: 0, dotBase: -5, dotTimeSeconds: 2 },
});
expect(romNativePeriodicDamageComponent(620313)).toEqual(binding.component);
expect(romNativePeriodicDamageBinding({ skillId: 490306, componentId: 620313, kind: "hot" })).toBeUndefined();
});
it("snapshots magical target resistance and magic-versus-defense power into Buff.Power", () => {
const source = sourceFor(501550, 491168);
const result = nativePeriodicDamageSnapshot({
source,
caster: nativeStats(),
target: nativeStats("rom-rogue"),
sourceIsPlayer: true,
magicLevel: 10,
casterLevel: 30,
targetLevel: 30,
spellTimeSeconds: 2,
effectiveMagicPower: 1,
magicResistance: { value: 5, rateFactor: 0.9 },
});
expect(result.status).toBe("supported");
if (result.status !== "supported") return;
expect(result.value).toMatchObject({
componentId: 501550,
sourceSkillId: 491168,
magicLevel: 10,
spellTime: 2,
effectTimeSeconds: 12,
tickIntervalSeconds: 2,
ticks: 6,
rankCorrectionTicks: 6,
magicLevelAdjustment: 16,
buffPower: 26,
sourceTickDamage: 91,
targetDefenseStage: "dot-application-rank-correction",
tickEventType: "magic",
});
expect(nativePeriodicDamageTick(result.value)).toMatchObject({
damage: 91,
healthDelta: -91,
firesPeriodicAttackEvents: true,
});
});
it("keeps raw duration rank-correction ticks distinct from the applied effect duration", () => {
const source = sourceFor(620313, 490306);
const shifted = {
...source,
component: {
...source.component,
effectTimeSeconds: 20,
effectTimeSkillLevelArg: 50,
levelDeltaEffectTime: 1,
isFixDotPower: true,
},
};
const result = nativePeriodicDamageSnapshot({
source: shifted,
caster: nativeStats("rom-rogue"),
target: nativeStats(),
sourceIsPlayer: true,
magicLevel: 10,
casterLevel: 30,
targetLevel: 20,
spellTimeSeconds: 1,
});
expect(result.status).toBe("supported");
if (result.status !== "supported") return;
expect(result.value.effectTimeSeconds).toBe(110);
expect(result.value.ticks).toBe(55);
expect(result.value.rankCorrectionTicks).toBe(10);
});
it("uses Main_DMG and Main_ATKSpeed for physical DotMagic rank correction", () => {
const source = sourceFor(620313, 490306);
const physical = {
...source,
component: { ...source.component, effectType: 1, magicType: -1, dotBase: -10, dotSkillLevelArg: 50 },
};
const result = nativePeriodicDamageSnapshot({
source: physical,
caster: nativeStats("rom-rogue"),
target: nativeStats(),
sourceIsPlayer: true,
magicLevel: 10,
casterLevel: 30,
targetLevel: 30,
spellTimeSeconds: 2,
mainDamage: 200,
mainAttackSpeed: 20,
});
expect(result.status).toBe("supported");
if (result.status !== "supported") return;
expect(result.value).toMatchObject({
rankCorrectionTicks: 5,
magicLevelAdjustment: 38,
buffPower: 48,
sourceTickDamage: 250,
tickEventType: "skill",
});
});
it("uses raw MagicLv for fixed-power rows and ShowPowerLv suppresses it", () => {
const source = sourceFor(501550, 491168);
const fixed = { ...source, component: { ...source.component, isFixDotPower: true } };
const fixedResult = nativePeriodicDamageSnapshot({
source: fixed, caster: nativeStats(), target: nativeStats("rom-rogue"), sourceIsPlayer: true,
magicLevel: 10, casterLevel: 30, targetLevel: 30,
});
expect(fixedResult.status).toBe("supported");
if (fixedResult.status === "supported") {
expect(fixedResult.value).toMatchObject({ magicLevelAdjustment: 0, buffPower: 10, sourceTickDamage: 40 });
}
const shown = { ...source, component: { ...source.component, isShowPowerLevel: true } };
const shownResult = nativePeriodicDamageSnapshot({
source: shown, caster: nativeStats(), target: nativeStats("rom-rogue"), sourceIsPlayer: true,
magicLevel: 10, casterLevel: 30, targetLevel: 30,
spellTimeSeconds: 2, effectiveMagicPower: 1, magicResistance: { value: 0, rateFactor: 1 },
});
expect(shownResult.status).toBe("supported");
if (shownResult.status === "supported") {
expect(shownResult.value).toMatchObject({ buffPower: 0, sourceTickDamage: 8 });
}
});
it("uses current MaxHP for HP_Per pulses and never rerolls source critical damage", () => {
const source = sourceFor(500549, 490304);
const result = nativePeriodicDamageSnapshot({
source, caster: nativeStats("rom-rogue"), target: nativeStats(), sourceIsPlayer: true,
magicLevel: 10, casterLevel: 30, targetLevel: 30,
});
expect(result.status).toBe("supported");
if (result.status !== "supported") return;
expect(result.value).toMatchObject({ percentHealthDot: true, sourceTickDamage: 6, tickDamageAtApplication: 60 });
expect(nativePeriodicDamageTick(result.value, 1_500)).toMatchObject({
damage: 90,
healthDelta: -90,
firesPeriodicAttackEvents: true,
});
expect(nativePeriodicDamageTick(result.value, Number.NaN)).toBeUndefined();
});
it("fails closed when source-required elemental resistance or physical weapon inputs are missing", () => {
const elemental = nativePeriodicDamageSnapshot({
source: sourceFor(501550, 491168), caster: nativeStats(), target: nativeStats("rom-rogue"),
sourceIsPlayer: true, magicLevel: 10, casterLevel: 30, targetLevel: 30,
spellTimeSeconds: 2, effectiveMagicPower: 1,
});
expect(elemental).toEqual({ status: "unsupported", reason: "periodic-damage-magical-resistance-required" });
const physicalSource = sourceFor(620313, 490306);
const physical = nativePeriodicDamageSnapshot({
source: { ...physicalSource, component: { ...physicalSource.component, effectType: 1 } },
caster: nativeStats("rom-rogue"), target: nativeStats(), sourceIsPlayer: true,
magicLevel: 10, casterLevel: 30, targetLevel: 30,
});
expect(physical).toEqual({ status: "unsupported", reason: "periodic-damage-physical-weapon-profile-required" });
});
it("fails closed when a dot refresh can retain or increase the source buff power", () => {
const result = nativePeriodicDamageSnapshot({
source: sourceFor(620313, 490306), caster: nativeStats("rom-rogue"), target: nativeStats(),
sourceIsPlayer: true, magicLevel: 10, casterLevel: 30, targetLevel: 30, isReapplication: true,
});
expect(result).toEqual({ status: "unsupported", reason: "periodic-damage-reapplication-stacking-unsupported" });
});
it("keeps the player cap at 80 while accepting NPC levels within exported source bounds", () => {
const source = sourceFor(620313, 490306);
const enemyBoundedSource = {
...source,
maxCasterLevel: 100,
maxMagicLevel: 100,
component: { ...source.component, isFixDotPower: true },
};
const npcResult = nativePeriodicDamageSnapshot({
source: enemyBoundedSource, caster: nativeStats(), target: nativeStats(), sourceIsPlayer: false,
magicLevel: 90, casterLevel: 90, targetLevel: 90,
});
expect(npcResult.status).toBe("supported");
const playerResult = nativePeriodicDamageSnapshot({
source: enemyBoundedSource, caster: nativeStats(), target: nativeStats(), sourceIsPlayer: true,
magicLevel: 90, casterLevel: 90, targetLevel: 90,
});
expect(playerResult).toEqual({ status: "unsupported", reason: "periodic-damage-input-invalid" });
const unboundedNpcResult = nativePeriodicDamageSnapshot({
source: { ...source, component: { ...source.component, isFixDotPower: true } },
caster: nativeStats(), target: nativeStats(), sourceIsPlayer: false,
magicLevel: 90, casterLevel: 90, targetLevel: 90,
});
expect(unboundedNpcResult).toEqual({ status: "unsupported", reason: "periodic-damage-npc-source-bounds-required" });
});
});