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" }); }); });