32 lines
7.4 KiB
Markdown
32 lines
7.4 KiB
Markdown
# 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.
|