Files
healer-man/docs/ROM_NATIVE_PERIODIC_DAMAGE_SOURCE_2026-10-03.md
2026-10-04 13:13:35 -04:00

7.4 KiB

RoM native periodic damage source pass — 2026-10-03

This pass adds a source-bound first-application snapshot and per-tick amount helper for negative HP DotMagic effects. It covers the active five RoM builds and deliberately leaves resource drains, healing-over-time, positive-base effects, unsupported refresh behavior, and unverified NPC stat derivation to their separate paths.

The exporter reads the sibling Runewaker tree without modifying it. scripts/runewaker-pipeline/export-rom-native-periodic-damage.mjs binds exact MagicCollect skill IDs to the active player catalog and imported NPC combat profiles, then emits src/game/generated/romNativePeriodicDamageBindings.generated.json. The active five contribute 18 ability rows, 18 source-rank rows, and 14 unique negative HP/HP_Per DotMagic components. The imported NPC source table has 440 NPC rows and 446 population-entity mappings; 50 NPCs currently have bound negative HP/HP_Per DotMagic, with 50 source-rank rows and 42 unique components. The union has 56 unique component IDs. NPC tick sources preserve the assigned slot MagicLv and use the verified native NPC growth upper bound of 200; this does not imply that every NPC stat profile is ready for native resolution. The JSON records the raw source IDs, selected fields, filter, record offsets, and SHA-256 hashes for the source databases/code and local catalogs.

Application and tick order

MagicProcess.cpp passes the effective cast SpellTime and MagicProc.MagicLv to CalAddDotMagicLv when applying an assist effect. The rank helper uses DotTimes = max(1, int(MagicBase.EffectTime / Dot.Time)) from the raw MagicBase duration; this is distinct from the effect duration later computed for the applied buff. For a negative HP DotMagic row, the correction first builds DotBasePower = Dot.Base * (1 + MagicLv * DotSkillLVArg / 100). FixDotPower returns no correction, and a zero DotSkillLVArg also returns no correction.

For magical effects the source adds or subtracts caster MDMG scaled by the player/NPC ExRate, cast SpellTime, FIX.MDmg_DotArg, and DotTimes. If the resulting power is negative, the source applies the target's CalResistDMG to the aggregate power across DotTimes, divides back by DotTimes, then applies the source (MATK + 100)/(MDEF + 200) power rate. If it becomes nonnegative, it enters the source heal-power branch and multiplies by MagicPow(MagicType). Physical effects instead use Main_DMG, Main_ATKSpeed, FIX.MDmg_DotArg, and (ATK + 100)/(DEF + 200); they do not call magical resistance. The returned correction is int((FinDmg - Dot.Base) / (Dot.Base * DotSkillLVArg / 100) - OrgMagicLv).

MagicProcess adds this correction to the cast's MagicLv, then forces it to zero when ShowPowerLv is set. It computes applied duration from the base effect time, effect-time skill argument, and caster-target level difference. AssistMagic then applies the source maximum-skill-level and BuffMaxLv caps. The helper requires the actual effective MagicLv, caster and target levels, and exact source-derived combatant inputs instead of guessing a spell rank or a missing weapon/resistance value.

RD_ValueCal.cxx::BuffProc emits a tick when elapsed buff time is divisible by Dot.Time. Each tick starts with int(-Dot.Base * (100 + DotSkillLVArg * Buff.Power) / 100). HP_Per then multiplies by the target's current Fin.MaxHP / 100 and truncates again. The source dispatches its periodic special-attack event before the PK adjustment and HP shield/HP application; magical DoTs use the magic event and physical DoTs use the skill event. MagicShieldAbsorbProc runs before HP application. DotMagicNoKill keeps the target at 1 HP when applicable. The tick reuses stored Buff.Power: it does not reroll a critical hit or reapply armor/resistance. The pure tick helper returns the integer amount and event/no-kill metadata; the shared combat runtime remains responsible for shields, health application, and side effects.

The runtime API in src/game/romNativePeriodicDamage.ts separates application snapshot inputs from tick inputs. Elemental magical rank correction requires the target's exact resistance channels and caster's effective MagicPow(MagicType). Physical rank correction requires exact Main_DMG and Main_ATKSpeed. NPC callers must provide the NPC's exact FIX.MDmg_DotArg and source-bounded caster/MagicLv limits; only the source player value is taken from generated verified rules. Player builds remain capped at level 80. Enemy lookup accepts the MagicCollect spellId and exact component ID, and the NPC source list is limited to imported encounter NPC spell bindings. An existing-stack reapplication is unsupported because AssistMagic can retain or raise the existing buff power. Unsupported inputs return a reason rather than a substitute amount.

Active-five special fixed inputs

The active-five direct-damage binding audit found 66 negative Attack.DmgPower components. Of these, four use fixed type 4 (Throw): Rogue source skills 490338 and 490309, components 502578 and 620283, each with FixValue=-3. Three use fixed type 12 (ShieldDef): Knight skill 490175/component 500175 and skill 490186/components 500177 and 500185, each with FixValue=-0.1. No active-five negative direct component uses type 10 (LastSkillDmg), type 11 (LastPhyDmg), or the separate calculation-type Throw branch.

MagicProcess.cpp evaluates each fixed input as Owner->GetValue(FixType), so the fixed source belongs to the caster. RD_ValueCal.cxx::GetValue resolves type 4 to caster Fin.Throw_DMG, type 10 to caster BackInfo.LastSkillHitPoint, type 11 to caster BackInfo.LastPhyHitPoint, and type 12 to caster raw BaseEQ.Def[EM_Armor_Shield] (not the target's shield or total defense).

RoleAttr.cpp::_Throw_DMG() is max(0, (Fin.DMG + BaseEQ.Throw_DMG) * BaseEQ.Throw_DMG_Per * BodyRate.DMG_AllRange * BodyRate.DMG_Per). Without throw equipment, source initialization leaves BaseEQ.Throw_DMG=0 and Throw_DMG_Per=1, but Fin.DMG itself already applies its DMG_Per factor, and _Throw_DMG applies DMG_Per once more plus the AllRange factor. A single generic physical-damage stat is therefore not enough to derive this input. Runtime should supply verified caster Fin.Throw_DMG explicitly or leave these four components unsupported.

Last-hit channels are per caster. MagicProcess clears LastSkillHitPoint at cast start and sets it from signed int(Temp.Value) after supported skill effects. The auto-attack scheduler clears LastPhyHitPoint before the attack sequence and stores Damage * -1 after each attack. Since neither type occurs in the active-five negative direct bindings, no persistent prior-hit channel is needed for those current rows.

Focused validation

src/game/romNativePeriodicDamage.test.ts checks source lookup, magical and physical rank correction, cast duration versus raw rank-correction duration, fixed/show-power rows, HP_Per tick scaling, no tick-time critical reroll, fail-closed inputs, and reapplication handling. src/game/romNativePeriodicCombat.integration.test.ts exercises a real player Vampire Arrows application, an active-five Priest/Knight Bone Chill companion application with native magical defense/resistance and the required typed MagicPower inputs, per-actor RoM versus WoW fallback, and an imported NPC HP_Per DoT whose later tick reads the target's current MaxHP. The focused helper/integration command passed all five tests.