Validate RoM primary five hybrid stats and pair mechanics
This commit is contained in:
1 parent
8e95480d4f
commit
c8b3a3d95a
18 files changed
+2061
-48
No files matched your search
@@ -0,0 +1,319 @@
|
||||
import type { AbilityDefinition, AbilityEffect } from "./abilityCatalog";
|
||||
import type { AuraDefinition, AuraModifier, AuraProcDefinition } from "./combatAuras";
|
||||
import type { RomGeneratedAbilityLike } from "./romAbilitySemantics";
|
||||
|
||||
function firstDamage(effects: readonly AbilityEffect[]): AbilityEffect[] {
|
||||
return effects.filter((effect) => effect.kind === "damage").slice(0, 1);
|
||||
}
|
||||
|
||||
function taggedPassive(
|
||||
sourceId: number,
|
||||
name: string,
|
||||
tag: string,
|
||||
proc?: AuraProcDefinition,
|
||||
): AbilityEffect[] {
|
||||
return [{
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: {
|
||||
id: `rom-pair-passive-${sourceId}`,
|
||||
name,
|
||||
disposition: "buff",
|
||||
durationMs: null,
|
||||
utilityTags: [tag],
|
||||
...(proc ? { procs: [proc] } : {}),
|
||||
},
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: sourceId,
|
||||
}];
|
||||
}
|
||||
|
||||
function customProc(
|
||||
sourceId: number,
|
||||
trigger: "hit" | "crit",
|
||||
actionId: string,
|
||||
options: { readonly chance?: number; readonly internalCooldownMs?: number } = {},
|
||||
): AuraProcDefinition {
|
||||
return {
|
||||
id: `rom-pair-proc-${sourceId}`,
|
||||
triggers: trigger === "hit" ? ["hit", "crit"] : trigger,
|
||||
chance: options.chance ?? 1,
|
||||
...(options.internalCooldownMs ? { internalCooldownMs: options.internalCooldownMs } : {}),
|
||||
action: { kind: "custom", id: actionId },
|
||||
};
|
||||
}
|
||||
|
||||
function timedCasterAura(
|
||||
sourceId: number,
|
||||
name: string,
|
||||
durationMs: number,
|
||||
modifiers: readonly AuraModifier[],
|
||||
): AbilityEffect {
|
||||
return {
|
||||
kind: "apply-aura",
|
||||
recipient: "caster",
|
||||
aura: {
|
||||
id: `rom-pair-buff-${sourceId}`,
|
||||
name,
|
||||
disposition: "buff",
|
||||
durationMs,
|
||||
modifiers,
|
||||
},
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: sourceId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Source-specific translations for the accepted Scout/Rogue and Rogue/Warrior
|
||||
* pairings. Passive tags and custom proc IDs are consumed by combat-store
|
||||
* transitions; they intentionally do not apply unrelated generic stat buffs.
|
||||
*/
|
||||
export function overrideRomScoutRogueEffects(
|
||||
row: RomGeneratedAbilityLike,
|
||||
translated: readonly AbilityEffect[],
|
||||
): readonly AbilityEffect[] | null {
|
||||
switch (row.sourceSkillId) {
|
||||
case 490400: // Piercing Arrow: one projectile, then a line hit behind its target.
|
||||
return firstDamage(translated);
|
||||
case 490424: { // Combo Shot: three impacts of the same source shot.
|
||||
const hit = firstDamage(translated)[0];
|
||||
return hit ? [hit, hit, hit] : translated;
|
||||
}
|
||||
case 490457: { // Reflected Shot: two jumps, each retaining half the prior hit.
|
||||
const hit = firstDamage(translated)[0];
|
||||
return hit?.kind === "damage" ? [{ ...hit, falloffPerJump: 0.5 }] : translated;
|
||||
}
|
||||
case 490354: // Vanish is an aggro/stealth transition, not a self-heal.
|
||||
return translated.filter((effect) => effect.kind !== "heal").map((effect) => effect.kind === "apply-aura"
|
||||
? { ...effect, aura: { ...effect.aura, utilityTags: ["stealth", "rom-vanish"] } }
|
||||
: effect);
|
||||
case 491493: { // Weak Spot's buff is component 501895, not its permanent hit modifier.
|
||||
const source = row.source?.components?.find((component) => component.id === 501895);
|
||||
const critical = source?.abilityModifiers?.find((modifier) => modifier.type === 18)?.value ?? 20;
|
||||
const durationMs = Math.max(1_000, Math.round((source?.effectTime ?? 15) * 1_000));
|
||||
return [
|
||||
...firstDamage(translated),
|
||||
timedCasterAura(491493, "Weak Spot critical strike", durationMs, [
|
||||
{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: critical },
|
||||
]),
|
||||
];
|
||||
}
|
||||
case 492342:
|
||||
return taggedPassive(492342, "Lasso Master", "rom-lasso-master");
|
||||
case 492916:
|
||||
return taggedPassive(492916, "Exploiting Shot", "rom-exploiting-shot",
|
||||
customProc(492916, "crit", "rom-exploiting-shot", { internalCooldownMs: 20_000 }));
|
||||
case 493007:
|
||||
return taggedPassive(493007, "Deadly Counterattack", "rom-deadly-counterattack",
|
||||
customProc(493007, "hit", "rom-deadly-counterattack", { internalCooldownMs: 20_000 }));
|
||||
case 493253:
|
||||
return taggedPassive(493253, "Poisonous Spit", "rom-poisonous-spit",
|
||||
customProc(493253, "hit", "rom-poisonous-spit", { internalCooldownMs: 20_000 }));
|
||||
case 499534:
|
||||
return taggedPassive(499534, "Master Reflected Shot", "rom-master-reflected-shot");
|
||||
case 499536:
|
||||
return taggedPassive(499536, "Meridian Attack", "rom-meridian-attack");
|
||||
case 491526:
|
||||
return taggedPassive(491526, "Throwing Mastery", "rom-throwing-mastery");
|
||||
case 491527: // The bonus applies to one-hand axes only, never all melee damage.
|
||||
return taggedPassive(491527, "One-Hand Axe Mastery", "rom-one-hand-axe-mastery");
|
||||
case 492349:
|
||||
return taggedPassive(492349, "Whirlwind Mastery", "rom-whirlwind-mastery");
|
||||
case 491339: // A negative RuneWaker value means reduced received healing.
|
||||
return translated.map((effect) => effect.kind === "apply-aura"
|
||||
? {
|
||||
...effect,
|
||||
aura: {
|
||||
...effect.aura,
|
||||
modifiers: effect.aura.modifiers?.map((modifier) => modifier.kind === "healing"
|
||||
&& modifier.direction === "taken"
|
||||
? { ...modifier, value: -Math.abs(modifier.value) }
|
||||
: modifier),
|
||||
},
|
||||
}
|
||||
: effect);
|
||||
case 492625: // Source attack component 620287 must not make its victim immune to physical damage.
|
||||
return translated.filter((effect) => effect.kind === "damage" || effect.kind === "dot");
|
||||
case 492920: { // Poison Spray spreads Infection's ten-second poison to nearby enemies.
|
||||
const hit = firstDamage(translated)[0];
|
||||
const poison: AbilityEffect = {
|
||||
kind: "dot",
|
||||
coefficient: 0,
|
||||
basePoints: 12,
|
||||
pointsPerLevel: 4.2,
|
||||
sourceLevel: 1,
|
||||
ticks: 5,
|
||||
intervalMs: 2_000,
|
||||
tag: "rom-poisonous-infection",
|
||||
};
|
||||
return hit ? [hit, poison] : [poison];
|
||||
}
|
||||
case 493017: // Decay's empty indefinite state is not its timed critical buff.
|
||||
return translated.filter((effect) => effect.kind !== "apply-aura"
|
||||
|| effect.aura.modifiers?.length || effect.aura.procs?.length || effect.aura.utilityTags?.length);
|
||||
case 493257:
|
||||
return taggedPassive(493257, "Reclaim Energy", "rom-reclaim-energy",
|
||||
customProc(493257, "hit", "rom-reclaim-energy", { chance: 0.25 }));
|
||||
case 499476:
|
||||
return taggedPassive(499476, "Toxic Splash", "rom-toxic-splash",
|
||||
customProc(499476, "hit", "rom-toxic-splash"));
|
||||
case 499480:
|
||||
return taggedPassive(499480, "Poison Battle Technique", "rom-poison-battle-technique",
|
||||
customProc(499480, "hit", "rom-poison-battle-technique"));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Select source-conditional pair effects after the target's existing states are known. */
|
||||
export function romScoutRogueCastEffects(
|
||||
ability: AbilityDefinition,
|
||||
effects: readonly AbilityEffect[],
|
||||
passiveTags: readonly string[],
|
||||
targetTags: readonly string[],
|
||||
): readonly AbilityEffect[] {
|
||||
const passives = new Set(passiveTags);
|
||||
const target = new Set(targetTags);
|
||||
if (ability.dbcSpellId === 490457) {
|
||||
const masteryFactor = passives.has("rom-master-reflected-shot") ? 1.2 : 1;
|
||||
return effects.map((effect) => effect.kind === "damage" ? {
|
||||
...effect,
|
||||
coefficient: effect.coefficient * masteryFactor,
|
||||
...(effect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: effect.coefficientPerLevel * masteryFactor }),
|
||||
...(effect.basePoints === undefined ? {} : { basePoints: effect.basePoints * masteryFactor }),
|
||||
...(effect.pointsPerLevel === undefined ? {} : { pointsPerLevel: effect.pointsPerLevel * masteryFactor }),
|
||||
...(effect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: effect.bonusPowerCoefficient * masteryFactor }),
|
||||
...(effect.bonusFlat === undefined ? {} : { bonusFlat: effect.bonusFlat * masteryFactor }),
|
||||
falloffPerJump: passives.has("rom-poisonous-spit") ? 1 : effect.falloffPerJump,
|
||||
} : effect);
|
||||
}
|
||||
if (ability.dbcSpellId === 490454 && passives.has("rom-lasso-master")) {
|
||||
return [...effects, {
|
||||
kind: "status",
|
||||
status: "disarm",
|
||||
durationMs: 10_000,
|
||||
maxTargets: 1,
|
||||
breakOnDamage: true,
|
||||
}];
|
||||
}
|
||||
if (ability.dbcSpellId === 490309 && passives.has("rom-throwing-mastery")) {
|
||||
return [...effects, { kind: "status", status: "slow", durationMs: 5_000, magnitude: 0.4 }];
|
||||
}
|
||||
if (ability.dbcSpellId === 492621) {
|
||||
const hasSourcePoison = target.has("rom-deadly-poison") || target.has("rom-501690");
|
||||
let seenHits = 0;
|
||||
return effects.filter((effect) => {
|
||||
if (effect.kind !== "damage") return true;
|
||||
seenHits += 1;
|
||||
return seenHits === 1 || (seenHits === 2 && hasSourcePoison);
|
||||
});
|
||||
}
|
||||
return effects;
|
||||
}
|
||||
|
||||
export type RomScoutRogueProcActionSpec =
|
||||
| {
|
||||
readonly kind: "aura";
|
||||
readonly recipient: "caster" | "target";
|
||||
readonly aura: AuraDefinition;
|
||||
readonly provenance: "source" | "hybrid-estimate";
|
||||
}
|
||||
| {
|
||||
readonly kind: "dot";
|
||||
readonly recipient: "target";
|
||||
readonly effect: Extract<AbilityEffect, { kind: "dot" }>;
|
||||
readonly provenance: "source" | "hybrid-estimate";
|
||||
}
|
||||
| {
|
||||
readonly kind: "resource";
|
||||
readonly recipient: "caster";
|
||||
readonly resource: "energy" | "rage";
|
||||
readonly amount: number;
|
||||
readonly provenance: "source" | "hybrid-estimate";
|
||||
}
|
||||
| {
|
||||
readonly kind: "damage";
|
||||
readonly recipient: "target";
|
||||
readonly effect: Extract<AbilityEffect, { kind: "damage" }>;
|
||||
readonly provenance: "source" | "hybrid-estimate";
|
||||
};
|
||||
|
||||
/**
|
||||
* A pure result for combat-store proc execution. `triggeringSourceSkillId` is
|
||||
* the RuneWaker skill ID of the attack that generated the hit/crit event.
|
||||
* The level-60/70 replacement skills 499477 and 499481 are absent from the
|
||||
* playable source catalog; those unresolved actions deliberately return null.
|
||||
*/
|
||||
export function romScoutRogueProcActionSpec(
|
||||
actionId: string,
|
||||
triggeringSourceSkillId: number,
|
||||
casterPoisonous: boolean,
|
||||
): RomScoutRogueProcActionSpec | null {
|
||||
switch (actionId) {
|
||||
case "rom-exploiting-shot":
|
||||
if (triggeringSourceSkillId !== 490423) return null;
|
||||
return {
|
||||
kind: "aura",
|
||||
recipient: "target",
|
||||
aura: {
|
||||
id: "rom-pair-exploiting-shot-vulnerability",
|
||||
name: "Exploiting Shot",
|
||||
disposition: "debuff",
|
||||
durationMs: 12_000, // Source component 503169.
|
||||
maxStacks: 5, // Explicit hybrid cap; source tooltip references a cap without a numeric value.
|
||||
stackBehavior: "add",
|
||||
modifiers: [{ kind: "damage", direction: "taken", school: "physical", operation: "percent", value: 0.02 }],
|
||||
},
|
||||
provenance: "hybrid-estimate",
|
||||
};
|
||||
case "rom-deadly-counterattack":
|
||||
if (triggeringSourceSkillId !== 490400) return null;
|
||||
return {
|
||||
kind: "aura",
|
||||
recipient: "caster",
|
||||
aura: {
|
||||
id: "rom-pair-deadly-counterattack-crit",
|
||||
name: "Deadly Counterattack",
|
||||
disposition: "buff",
|
||||
durationMs: 5_000, // Hybrid estimate; source specifies a 20s internal cooldown, not buff duration.
|
||||
modifiers: [{ kind: "stat", stat: "physical-critical-strike", operation: "flat", value: 20 }],
|
||||
},
|
||||
provenance: "hybrid-estimate",
|
||||
};
|
||||
case "rom-poisonous-spit":
|
||||
if (!casterPoisonous || ![490457, 490424].includes(triggeringSourceSkillId)) return null;
|
||||
return {
|
||||
kind: "dot",
|
||||
recipient: "target",
|
||||
effect: {
|
||||
kind: "dot",
|
||||
coefficient: 0,
|
||||
basePoints: 16,
|
||||
pointsPerLevel: 8,
|
||||
sourceLevel: 1,
|
||||
ticks: 5,
|
||||
intervalMs: 2_000,
|
||||
tag: "rom-poisonous-spit",
|
||||
},
|
||||
provenance: "hybrid-estimate", // 501690 poison scale is a proxy; Poison Powder payload is not in this catalog.
|
||||
};
|
||||
case "rom-reclaim-energy":
|
||||
if (triggeringSourceSkillId === 490306) return {
|
||||
kind: "resource", recipient: "caster", resource: "energy", amount: 15, provenance: "hybrid-estimate",
|
||||
};
|
||||
if (triggeringSourceSkillId === 492625) return {
|
||||
kind: "resource", recipient: "caster", resource: "rage", amount: 10, provenance: "hybrid-estimate",
|
||||
};
|
||||
return null;
|
||||
case "rom-toxic-splash": // Requires absent replacement skill 499477 and triggered payload 493296.
|
||||
case "rom-poison-battle-technique": // Requires absent replacement skill 499481.
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user