252 lines
9.6 KiB
TypeScript
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" });
|
|
});
|
|
});
|