forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
+404
-31
@@ -15,8 +15,12 @@ import {
|
||||
type RomSourceComponent,
|
||||
} from "./romAbilitySemantics";
|
||||
import { overrideRomPriestKnightEffects, romPriestKnightAdditionalAbilities } from "./romPriestKnightOverrides";
|
||||
import { overrideRomKnightEffects } from "./romKnightOverrides";
|
||||
import { overrideRomWarriorEffects } from "./romWarriorOverrides";
|
||||
import { overrideRomScoutRogueEffects } from "./romScoutRogueOverrides";
|
||||
import { overrideRomMageWarlockEffects } from "./romMageWarlockOverrides";
|
||||
import { overrideRomWarlockEffects } from "./romWarlockOverrides";
|
||||
import { overrideRomDruidEffects } from "./romDruidOverrides";
|
||||
import { romPriestDescription } from "./romPriestTooltips";
|
||||
|
||||
export type RomSkillGroup = "primary" | "general" | "elite" | "passive";
|
||||
@@ -89,7 +93,38 @@ function costs(rows: readonly GeneratedCost[]): readonly AbilityCost[] {
|
||||
}));
|
||||
}
|
||||
|
||||
type GeneratedSemanticRow = Pick<GeneratedSkill, "sourceSkillId" | "name" | "description" | "target" | "passive" | "source">;
|
||||
function costsFor(row: Pick<GeneratedRank | GeneratedSkill, "sourceSkillId" | "costs">): readonly AbilityCost[] {
|
||||
let resolved = costs(row.costs);
|
||||
if (row.sourceSkillId === 490136) {
|
||||
resolved = resolved.map((cost) => cost.resource === "rage" ? { ...cost, amount: 15 } : cost);
|
||||
}
|
||||
if (![493536, 494217].includes(row.sourceSkillId) || resolved.some((entry) => entry.resource === "nature-power")) return resolved;
|
||||
return [...resolved, { resource: "nature-power", amount: 1 }];
|
||||
}
|
||||
|
||||
function cooldownMsFor(row: Pick<GeneratedRank | GeneratedSkill, "sourceSkillId" | "cooldownMs">): number {
|
||||
// The local snapshot omitted Prayer of Tribulation's timing. CoA's current
|
||||
// skill list identifies this same Priest/Warlock spell (498770) as a 1.5s
|
||||
// channel with a 5s cooldown; its healing and buff payload still come from
|
||||
// the local RuneWaker tooltip/components below.
|
||||
if (row.sourceSkillId === 498770) return 5_000;
|
||||
return row.sourceSkillId === 490136 ? 40_000 : row.cooldownMs;
|
||||
}
|
||||
|
||||
function castTimeMsFor(row: Pick<GeneratedRank | GeneratedSkill, "sourceSkillId" | "castTimeMs">): number {
|
||||
if (row.sourceSkillId === 498770) return 1_500;
|
||||
if (row.sourceSkillId === 491164) return 4_000;
|
||||
return row.castTimeMs;
|
||||
}
|
||||
|
||||
function castModeFor(row: Pick<GeneratedRank | GeneratedSkill, "sourceSkillId" | "castTimeMs">): AbilityDefinition["castMode"] {
|
||||
return row.sourceSkillId === 491164 || row.sourceSkillId === 498770
|
||||
? "channel"
|
||||
: row.castTimeMs > 0 ? "cast" : "instant";
|
||||
}
|
||||
|
||||
type GeneratedSemanticRow = Pick<GeneratedSkill, "sourceSkillId" | "name" | "description" | "target" | "passive" | "source">
|
||||
& Partial<Pick<GeneratedSkill, "classId" | "secondaryClassId">>;
|
||||
|
||||
function slug(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "effect";
|
||||
@@ -128,6 +163,16 @@ function auraRecipient(row: GeneratedSemanticRow, semantic: Extract<RomAbilitySe
|
||||
return "ability-target";
|
||||
}
|
||||
|
||||
function abilityTarget(row: Pick<GeneratedSemanticRow, "sourceSkillId" | "target">): GeneratedSemanticRow["target"] {
|
||||
if ([490290, 490491, 492636, 493531, 494021, 494027, 498641].includes(row.sourceSkillId)) return "friendly";
|
||||
// RuneWaker's tooltip calls this a four-pulse heal on a friendly target;
|
||||
// sourceTarget 7 is stale/conflicting and was incorrectly making it hostile.
|
||||
if (row.sourceSkillId === 498770) return "friendly";
|
||||
if (row.sourceSkillId === 490239) return "hostile";
|
||||
if (row.sourceSkillId === 494217) return "self";
|
||||
return row.target;
|
||||
}
|
||||
|
||||
const MAGIC_SCHOOLS: readonly DamageSchool[] = ["arcane", "fire", "frost", "holy", "nature", "shadow"];
|
||||
|
||||
function percent(value: number): number {
|
||||
@@ -340,7 +385,10 @@ function semanticEffect(row: GeneratedSemanticRow, semantic: RomAbilitySemantic,
|
||||
if (semantic.kind === "aura") {
|
||||
const recipient = auraRecipient(row, semantic);
|
||||
const modifiers = auraModifiers(semantic);
|
||||
const partyScope = /\b(?:(?:all|every) party members?|all teammates)\b/i.test(row.description);
|
||||
const partyScope = [490248, 491335, 491644, 494027, 498201, 498202, 498219, 498649, 498719]
|
||||
.includes(row.sourceSkillId)
|
||||
|| (row.sourceSkillId === 498722 && semantic.sourceComponentIds.includes(621463))
|
||||
|| /\b(?:(?:all|every) party members?|all teammates)\b/i.test(row.description);
|
||||
const aura: AuraDefinition = {
|
||||
id: `rom-aura-${effectId}-${slug(semantic.effect)}`,
|
||||
name: `${row.name}: ${semantic.effect}`,
|
||||
@@ -354,7 +402,12 @@ function semanticEffect(row: GeneratedSemanticRow, semantic: RomAbilitySemantic,
|
||||
? { utilityTags: ["stealth"] }
|
||||
: {}),
|
||||
};
|
||||
return { kind: "apply-aura", aura, recipient, ...(partyScope ? { scope: "party" as const } : {}), sourceEffectType: -1, sourceAuraType: semantic.modifierType ?? -1, miscValue: semantic.value ?? 0, triggerSpellId: 0 };
|
||||
return {
|
||||
kind: "apply-aura", aura, recipient,
|
||||
...(partyScope ? { scope: "party" as const } : {}),
|
||||
...(row.sourceSkillId === 498719 ? { excludeCaster: true } : {}),
|
||||
sourceEffectType: -1, sourceAuraType: semantic.modifierType ?? -1, miscValue: semantic.value ?? 0, triggerSpellId: 0,
|
||||
};
|
||||
}
|
||||
if (semantic.kind === "absorb") {
|
||||
if (semantic.amount <= 0 && semantic.perSkillLevel <= 0) {
|
||||
@@ -369,7 +422,11 @@ function semanticEffect(row: GeneratedSemanticRow, semantic: RomAbilitySemantic,
|
||||
dispelCategory: "magic",
|
||||
absorbs: [{ id: `rom-absorb-pool-${effectId}`, amount: Math.max(0, semantic.amount + semantic.perSkillLevel), school: combatSchool(semantic.school) }],
|
||||
};
|
||||
return { kind: "apply-aura", aura, recipient, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 0 };
|
||||
return {
|
||||
kind: "apply-aura", aura, recipient,
|
||||
...([491513, 494027, 498201].includes(row.sourceSkillId) ? { scope: "party" as const } : {}),
|
||||
sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: 0,
|
||||
};
|
||||
}
|
||||
if (semantic.kind === "proc") {
|
||||
const recipient = auraRecipient(row, semantic);
|
||||
@@ -390,7 +447,10 @@ function semanticEffect(row: GeneratedSemanticRow, semantic: RomAbilitySemantic,
|
||||
return { kind: "apply-aura", aura, recipient, sourceEffectType: -1, sourceAuraType: -1, miscValue: 0, triggerSpellId: semantic.triggerSkillId };
|
||||
}
|
||||
if (semantic.kind === "resource") {
|
||||
const resourceType = knownResource(semantic.resource);
|
||||
const manaPercentRegen = semantic.resource === "mana-percent"
|
||||
&& semantic.intervalSeconds !== null
|
||||
&& semantic.intervalSeconds > 0;
|
||||
const resourceType = manaPercentRegen ? "mana" : knownResource(semantic.resource);
|
||||
if (semantic.intervalSeconds && semantic.intervalSeconds > 0 && resourceType) {
|
||||
const amountPerSecond = semantic.amount / semantic.intervalSeconds;
|
||||
const aura: AuraDefinition = {
|
||||
@@ -398,15 +458,31 @@ function semanticEffect(row: GeneratedSemanticRow, semantic: RomAbilitySemantic,
|
||||
name: row.name,
|
||||
disposition: semantic.operation === "drain" ? "debuff" : "buff",
|
||||
durationMs: semantic.durationSeconds && semantic.durationSeconds > 0 ? durationMs(semantic.durationSeconds) : null,
|
||||
modifiers: [{
|
||||
kind: "resource",
|
||||
resource: resourceType,
|
||||
property: "regeneration",
|
||||
operation: "flat",
|
||||
value: semantic.operation === "drain" ? -amountPerSecond : amountPerSecond,
|
||||
}],
|
||||
modifiers: manaPercentRegen
|
||||
? [{
|
||||
kind: "stat",
|
||||
stat: "mana-regeneration-max-percent-per-second",
|
||||
operation: "flat",
|
||||
value: (semantic.operation === "drain" ? -amountPerSecond : amountPerSecond) / 100,
|
||||
}]
|
||||
: [{
|
||||
kind: "resource",
|
||||
resource: resourceType,
|
||||
property: "regeneration",
|
||||
operation: "flat",
|
||||
value: semantic.operation === "drain" ? -amountPerSecond : amountPerSecond,
|
||||
}],
|
||||
};
|
||||
return {
|
||||
kind: "apply-aura",
|
||||
aura,
|
||||
recipient: row.sourceSkillId === 498529 || row.target === "self" ? "caster" : "ability-target",
|
||||
...(row.sourceSkillId === 491644 ? { scope: "party" as const } : {}),
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: semantic.amount,
|
||||
triggerSpellId: 0,
|
||||
};
|
||||
return { kind: "apply-aura", aura, recipient: row.target === "hostile" ? "ability-target" : "caster", sourceEffectType: -1, sourceAuraType: -1, miscValue: semantic.amount, triggerSpellId: 0 };
|
||||
}
|
||||
if (semantic.operation === "drain") return {
|
||||
kind: "resource-drain",
|
||||
@@ -462,7 +538,137 @@ function stateAuraEffect(
|
||||
};
|
||||
}
|
||||
|
||||
function targetStateAuraEffect(row: GeneratedSemanticRow, stateId: string, name: string, duration: number): AbilityEffect {
|
||||
return {
|
||||
kind: "apply-aura",
|
||||
recipient: "ability-target",
|
||||
aura: {
|
||||
id: `rom-state-${stateId}`,
|
||||
name,
|
||||
disposition: "debuff",
|
||||
durationMs: duration,
|
||||
utilityTags: [`rom-${stateId}`],
|
||||
},
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: row.sourceSkillId,
|
||||
};
|
||||
}
|
||||
|
||||
function specializedEffects(row: GeneratedSemanticRow): readonly AbilityEffect[] | null {
|
||||
if (row.classId === "rom-warden" && row.sourceSkillId === 494610) {
|
||||
// This passive is a trigger marker; its source damage reduction is applied
|
||||
// to Frantic Briar's target, not permanently to the Warden.
|
||||
return [stateAuraEffect(row, "will-attack", row.name, null)];
|
||||
}
|
||||
if (row.classId === "rom-warden" && row.sourceSkillId === 493344) {
|
||||
return translateRomAbilitySemantics(row).map((semantic, index) => {
|
||||
const effect = semanticEffect(row, semantic, index);
|
||||
return effect.kind === "summon"
|
||||
? { ...effect, durationMs: Number.MAX_SAFE_INTEGER }
|
||||
: effect;
|
||||
});
|
||||
}
|
||||
if (row.classId === "rom-warden" && row.sourceSkillId === 491668) {
|
||||
let keptAbsorb = false;
|
||||
return translateRomAbilitySemantics(row).map((semantic, index) => semanticEffect(row, semantic, index))
|
||||
.filter((effect) => {
|
||||
if (effect.kind !== "apply-aura" || !effect.aura.absorbs?.length) return true;
|
||||
if (keptAbsorb) return false;
|
||||
keptAbsorb = true;
|
||||
return true;
|
||||
})
|
||||
.map((effect) => effect.kind === "apply-aura" && effect.aura.absorbs?.length
|
||||
? {
|
||||
...effect,
|
||||
recipient: "caster" as const,
|
||||
aura: {
|
||||
...effect.aura,
|
||||
absorbs: effect.aura.absorbs.map((absorb) => ({
|
||||
...absorb,
|
||||
amount: 0,
|
||||
hitCharges: 2,
|
||||
})),
|
||||
},
|
||||
}
|
||||
: effect);
|
||||
}
|
||||
if (row.classId === "rom-champion" && row.sourceSkillId === 498564) {
|
||||
return [
|
||||
...translateRomAbilitySemantics(row).map((semantic, index) => semanticEffect(row, semantic, index)),
|
||||
{ kind: "pull", toward: "caster" },
|
||||
];
|
||||
}
|
||||
if (row.sourceSkillId === 494611) {
|
||||
const modifiers = [...new Map(translateRomAbilitySemantics(row)
|
||||
.filter((semantic): semantic is Extract<RomAbilitySemantic, { kind: "aura" }> => semantic.kind === "aura")
|
||||
.flatMap(auraModifiers)
|
||||
.map((modifier) => [JSON.stringify(modifier), modifier] as const)).values()];
|
||||
if (!modifiers.length) return null;
|
||||
const aura: AuraDefinition = {
|
||||
id: "rom-coat-of-arms",
|
||||
name: row.name,
|
||||
disposition: "buff",
|
||||
durationMs: durationMs(Math.max(0, ...(row.source?.components ?? []).map((component) => Number(component.effectTime ?? 0)))),
|
||||
dispelCategory: "magic",
|
||||
modifiers,
|
||||
};
|
||||
const apply = (recipient: "caster" | "ability-target"): AbilityEffect => ({
|
||||
kind: "apply-aura",
|
||||
aura,
|
||||
recipient,
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: 0,
|
||||
});
|
||||
return [apply("caster"), apply("ability-target")];
|
||||
}
|
||||
if (row.sourceSkillId === 491173) {
|
||||
return [stateAuraEffect(row, "cripple-joint", row.name, null, {
|
||||
procs: [{
|
||||
id: "rom-cripple-joint-root",
|
||||
triggers: ["hit", "crit"],
|
||||
chance: 0.55,
|
||||
action: { kind: "custom", id: "rom-cripple-joint" },
|
||||
}],
|
||||
})];
|
||||
}
|
||||
if (row.sourceSkillId === 491351) {
|
||||
return [{
|
||||
kind: "apply-aura",
|
||||
aura: {
|
||||
id: "rom-free-will-immunity",
|
||||
name: row.name,
|
||||
disposition: "buff",
|
||||
durationMs: durationMs(row.source?.components?.[0]?.effectTime ?? 0),
|
||||
controlImmunities: ["root", "slow"],
|
||||
},
|
||||
recipient: "ability-target",
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: 0,
|
||||
}];
|
||||
}
|
||||
if (row.sourceSkillId === 498570) {
|
||||
return [{
|
||||
kind: "apply-aura",
|
||||
aura: {
|
||||
id: "rom-overrule-immunity",
|
||||
name: row.name,
|
||||
disposition: "buff",
|
||||
durationMs: durationMs(row.source?.components?.[0]?.effectTime ?? 3),
|
||||
controlImmunities: ["fear", "confuse", "sleep", "stun", "root", "slow"],
|
||||
},
|
||||
recipient: "caster",
|
||||
sourceEffectType: -1,
|
||||
sourceAuraType: -1,
|
||||
miscValue: 0,
|
||||
triggerSpellId: 0,
|
||||
}];
|
||||
}
|
||||
if (row.sourceSkillId === 498744) {
|
||||
return [{ kind: "cooldown-reset", mode: "reset", abilityName: "Electrostatic Charge" }];
|
||||
}
|
||||
@@ -524,10 +730,88 @@ function specializedEffects(row: GeneratedSemanticRow): readonly AbilityEffect[]
|
||||
function effects(row: GeneratedSemanticRow): readonly AbilityEffect[] {
|
||||
const specialized = specializedEffects(row);
|
||||
if (specialized) return specialized;
|
||||
const translated = translateRomAbilitySemantics(row).map((semantic, index) => semanticEffect(row, semantic, index));
|
||||
const pairOverride = overrideRomPriestKnightEffects(row, translated)
|
||||
const semantics = translateRomAbilitySemantics(row);
|
||||
const translated = semantics.map((semantic, index) => semanticEffect(row, semantic, index))
|
||||
.map((effect) => row.sourceSkillId === 493642 && effect.kind === "apply-aura" ? {
|
||||
...effect,
|
||||
aura: {
|
||||
...effect.aura,
|
||||
utilityTags: [...(effect.aura.utilityTags ?? []), "rom-briar-shield"],
|
||||
},
|
||||
} : effect);
|
||||
const druidOverride = row.classId === "rom-druid" ? overrideRomDruidEffects(row, translated) : null;
|
||||
if (druidOverride) return druidOverride;
|
||||
if (row.sourceSkillId === 498825) return [
|
||||
...translated,
|
||||
{ kind: "cooldown-reset", mode: "reset", abilityName: "Lightning" },
|
||||
{ kind: "cooldown-reset", mode: "reset", abilityName: "Discharge" },
|
||||
];
|
||||
if (row.sourceSkillId === 491606 || row.sourceSkillId === 490078) {
|
||||
const damageComponentId = row.sourceSkillId === 491606 ? 502050 : 502102;
|
||||
const triggerComponentId = row.sourceSkillId === 491606 ? 502049 : 502103;
|
||||
const triggerComponent = row.source?.components?.find((component) => component.id === triggerComponentId);
|
||||
const intervalSeconds = triggerComponent?.triggers?.onTimeSeconds ?? (row.sourceSkillId === 491606 ? 2 : 3);
|
||||
const durationSeconds = triggerComponent?.effectTime ?? (row.sourceSkillId === 491606 ? 6 : 18);
|
||||
const ticks = Math.max(1, Math.round(durationSeconds / intervalSeconds));
|
||||
const damageSemanticIndex = semantics.findIndex((semantic) => semantic.kind === "damage"
|
||||
&& semantic.sourceComponentIds.includes(damageComponentId));
|
||||
const damageSemantic = semantics[damageSemanticIndex];
|
||||
const damageEffect = damageSemantic?.kind === "damage"
|
||||
? semanticEffect(row, damageSemantic, damageSemanticIndex)
|
||||
: null;
|
||||
const periodicDamage: AbilityEffect | null = damageEffect?.kind === "damage" && row.sourceSkillId === 491606
|
||||
? {
|
||||
kind: "dot",
|
||||
coefficient: damageEffect.coefficient,
|
||||
...(damageEffect.school ? { school: damageEffect.school } : {}),
|
||||
...(damageEffect.basePoints === undefined ? {} : { basePoints: damageEffect.basePoints }),
|
||||
...(damageEffect.dieSides === undefined ? {} : { dieSides: damageEffect.dieSides }),
|
||||
...(damageEffect.pointsPerLevel === undefined ? {} : { pointsPerLevel: damageEffect.pointsPerLevel }),
|
||||
...(damageEffect.coefficientPerLevel === undefined ? {} : { coefficientPerLevel: damageEffect.coefficientPerLevel }),
|
||||
...(damageEffect.bonusPowerCoefficient === undefined ? {} : { bonusPowerCoefficient: damageEffect.bonusPowerCoefficient }),
|
||||
...(damageEffect.sourceLevel === undefined ? {} : { sourceLevel: damageEffect.sourceLevel }),
|
||||
ticks,
|
||||
intervalMs: durationMs(intervalSeconds),
|
||||
tag: "rom-knight-holy-light-domain",
|
||||
}
|
||||
: row.sourceSkillId === 490078
|
||||
? {
|
||||
kind: "dot",
|
||||
coefficient: 0,
|
||||
school: "holy",
|
||||
basePoints: 0,
|
||||
pointsPerLevel: 0,
|
||||
sourceLevel: 1,
|
||||
ticks,
|
||||
intervalMs: durationMs(intervalSeconds),
|
||||
tag: "rom-knight-war-prayer-seal",
|
||||
}
|
||||
: null;
|
||||
if (!periodicDamage) return translated;
|
||||
const output: AbilityEffect[] = [periodicDamage];
|
||||
if (row.sourceSkillId === 490078 && damageEffect?.kind === "damage") output.unshift(damageEffect);
|
||||
if (row.sourceSkillId === 491606) {
|
||||
semantics.forEach((semantic, index) => {
|
||||
if (!semantic.sourceComponentIds.includes(502051)) return;
|
||||
output.push(semanticEffect(row, semantic, index));
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
if (row.sourceSkillId === 491133) {
|
||||
const vulnerableDuration = row.source?.components?.find((component) => component.id === 501502)?.effectTime ?? 6;
|
||||
return [...translated, targetStateAuraEffect(row, "vulnerable", "Vulnerable", durationMs(vulnerableDuration))];
|
||||
}
|
||||
if (row.sourceSkillId === 491136) {
|
||||
const weakenedDuration = row.source?.components?.find((component) => component.id === 501503)?.effectTime ?? 6;
|
||||
return [...translated, targetStateAuraEffect(row, "weakened", "Weakened", durationMs(weakenedDuration))];
|
||||
}
|
||||
const pairOverride = overrideRomKnightEffects(row, translated)
|
||||
?? overrideRomWarriorEffects(row, translated)
|
||||
?? overrideRomPriestKnightEffects(row, translated)
|
||||
?? overrideRomScoutRogueEffects(row, translated)
|
||||
?? overrideRomMageWarlockEffects(row, translated);
|
||||
?? overrideRomMageWarlockEffects(row, translated)
|
||||
?? overrideRomWarlockEffects(row, translated);
|
||||
if (pairOverride) return pairOverride;
|
||||
const outputStrength = (effect: AbilityEffect): number => (
|
||||
"coefficient" in effect ? effect.coefficient : 0
|
||||
@@ -541,6 +825,42 @@ function effects(row: GeneratedSemanticRow): readonly AbilityEffect[] {
|
||||
const holySealDot = (strongest: boolean): readonly AbilityEffect[] => oneOutput("dot", strongest).map((effect) => (
|
||||
effect.kind === "dot" ? { ...effect, tag: "rom-holy-seal" } : effect
|
||||
));
|
||||
if (row.sourceSkillId === 494025) {
|
||||
return oneOutput("damage", true).map((effect) => effect.kind === "damage"
|
||||
? { ...effect, falloffPerJump: 0.7 }
|
||||
: effect);
|
||||
}
|
||||
if (row.sourceSkillId === 499558) {
|
||||
const shot = semantics.find((semantic): semantic is Extract<RomAbilitySemantic, { kind: "damage" }> => (
|
||||
semantic.kind === "damage" && semantic.sourceComponentIds.includes(620194)
|
||||
));
|
||||
const healSource = semantics.find((semantic) => semantic.sourceComponentIds.includes(501888));
|
||||
const output: AbilityEffect[] = [];
|
||||
if (shot) {
|
||||
const shotEffect = semanticEffect(row, shot, semantics.indexOf(shot));
|
||||
if (shotEffect.kind === "damage") output.push(shotEffect);
|
||||
}
|
||||
if (healSource && (healSource.kind === "damage" || healSource.kind === "heal")) {
|
||||
const healSemantic: RomAbilitySemantic = { ...healSource, kind: "heal" };
|
||||
const healEffect = semanticEffect({ ...row, target: "friendly" }, healSemantic, semantics.indexOf(healSource));
|
||||
if (healEffect.kind === "heal") output.push({
|
||||
...healEffect,
|
||||
recipient: "ability-target",
|
||||
scope: "party",
|
||||
maximumTargets: 1,
|
||||
selection: "lowest-max-health",
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
if (row.classId === "rom-druid" && row.secondaryClassId === "rom-warden" && row.sourceSkillId === 494323) {
|
||||
return translated.filter((effect) => effect.kind === "heal").slice(0, 2).map((effect) => effect.kind === "heal"
|
||||
? { ...effect, falloffPerJump: 0.5 }
|
||||
: effect);
|
||||
}
|
||||
// Source order is the Nature's Power enhanced HoT (504529), then the
|
||||
// unconditional baseline HoT (620446). Runtime selects exactly one.
|
||||
if (row.sourceSkillId === 494021) return translated.filter((effect) => effect.kind === "hot").slice(0, 2);
|
||||
// These source rows carry alternate/duplicate payloads, while their player
|
||||
// tooltips specify a single projectile hit. The first attack component is
|
||||
// the primary shot; executing every component also applies unrelated Shot
|
||||
@@ -576,6 +896,26 @@ function effects(row: GeneratedSemanticRow): readonly AbilityEffect[] {
|
||||
aura: { ...effect.aura, utilityTags: [...(effect.aura.utilityTags ?? []), "rom-self-health-drain"] },
|
||||
} : effect);
|
||||
}
|
||||
if (row.sourceSkillId === 498770) {
|
||||
const heal = translated.find((effect) => effect.kind === "heal");
|
||||
const buffs = translated.filter((effect) => effect.kind === "apply-aura");
|
||||
if (!heal) return translated;
|
||||
const buff = buffs[0];
|
||||
const defenseModifiers = buffs.flatMap((effect) => effect.kind === "apply-aura"
|
||||
? effect.aura.modifiers ?? []
|
||||
: []);
|
||||
// Local text says four heals; preserve that count as four separate channel
|
||||
// payloads so the runtime emits one equal portion on each of four pulses.
|
||||
return [
|
||||
...Array.from({ length: 4 }, () => heal.kind === "heal"
|
||||
? { ...heal, recipient: "ability-target" as const }
|
||||
: heal),
|
||||
...(buff?.kind === "apply-aura" ? [{
|
||||
...buff,
|
||||
aura: { ...buff.aura, modifiers: defenseModifiers },
|
||||
}] : []),
|
||||
];
|
||||
}
|
||||
if (row.sourceSkillId === 490506 || row.sourceSkillId === 490218) {
|
||||
return translated.filter((effect) => effect.kind === "apply-aura");
|
||||
}
|
||||
@@ -615,7 +955,10 @@ function effects(row: GeneratedSemanticRow): readonly AbilityEffect[] {
|
||||
return [{ kind: "status", status: "root", durationMs: 10_000, maxTargets: 1, breakOnDamage: true }];
|
||||
}
|
||||
if (row.sourceSkillId === 490438) {
|
||||
return [{ kind: "status", status: "stun", durationMs: 10_000, maxTargets: 1, breakOnDamage: true }];
|
||||
// Neck Strike has a six-second NPC stun (component 500877); the separate
|
||||
// 503863 component supplies the reduced three-second player duration.
|
||||
const durationMsForTarget = durationMs(row.source?.components?.find((component) => component.id === 500877)?.effectTime ?? 6);
|
||||
return [{ kind: "status", status: "stun", durationMs: durationMsForTarget, maxTargets: 1, breakOnDamage: true }];
|
||||
}
|
||||
if (row.sourceSkillId === 491172) {
|
||||
return [{ kind: "status", status: "stun", durationMs: 10_000, maxTargets: 1, breakOnDamage: true }];
|
||||
@@ -673,12 +1016,30 @@ function effects(row: GeneratedSemanticRow): readonly AbilityEffect[] {
|
||||
|
||||
function areaRadius(row: Pick<GeneratedSkill, "description" | "range">): number | undefined {
|
||||
if (row.description.includes("up to 3 targets") && row.description.includes("Inflicts")) return 6;
|
||||
if (!/\b(?:multiple targets|all targets|surrounding (?:enemies|targets)|party members within|all teammates within|friendly targets within|(?:enemies|targets) within (?:a )?range)\b/i.test(row.description)) return undefined;
|
||||
const sourceRange = row.description.match(/\b(?:range|radius) of\s+(\d+(?:\.\d+)?)/i)?.[1];
|
||||
if (!/\b(?:multiple targets|multiple enemies|all targets|all enemies|nearby targets|nearby party members|surrounding (?:enemies|targets)|party members within|all teammates within|friendly targets within|(?:enemies|targets) (?:within (?:a )?range|in a range|in range))\b/i.test(row.description)) return undefined;
|
||||
const sourceRange = row.description.match(/\b(?:range|radius)\s+(?:of\s+)?(\d+(?:\.\d+)?)/i)?.[1];
|
||||
return Math.max(3, Math.min(40, sourceRange ? Number(sourceRange) / 10 : row.range > 1 ? row.range : 10));
|
||||
}
|
||||
|
||||
function sourceTargeting(sourceSkillId: number): AbilityDefinition["targeting"] | undefined {
|
||||
if (sourceSkillId === 490311 || sourceSkillId === 490337) return { shape: "unit", requiresBehindTarget: true };
|
||||
if (sourceSkillId === 490239) return { shape: "line", radius: 12 };
|
||||
if (sourceSkillId === 498201 || sourceSkillId === 498202) return { shape: "circle", radius: 7.5 };
|
||||
if (sourceSkillId === 490136) return { shape: "circle", radius: 10 };
|
||||
if (sourceSkillId === 491513) return { shape: "circle", radius: 5, maximumTargets: 6 };
|
||||
if (sourceSkillId === 492636) return { shape: "circle", radius: 20, maximumTargets: 5 };
|
||||
if (sourceSkillId === 494021) return { shape: "circle", radius: 20, maximumTargets: 5 };
|
||||
if (sourceSkillId === 494026) return { shape: "circle", radius: 12 };
|
||||
if (sourceSkillId === 494027) return { shape: "circle", radius: 6, maximumTargets: 3 };
|
||||
if (sourceSkillId === 494217) return { shape: "circle", radius: 5 };
|
||||
if (sourceSkillId === 494025) return { shape: "chain", maximumTargets: 3, jumpRange: 6 };
|
||||
if (sourceSkillId === 494323) return { shape: "circle", radius: 6, maximumTargets: 3 };
|
||||
if (sourceSkillId === 499936) return { shape: "circle", radius: 10 };
|
||||
if (sourceSkillId === 498641) return { shape: "circle", radius: 20, maximumTargets: 6 };
|
||||
if (sourceSkillId === 493540) return { shape: "line" };
|
||||
if (sourceSkillId === 498658) return { shape: "line", maximumTargets: 8 };
|
||||
if (sourceSkillId === 498721) return { shape: "circle", radius: 10 };
|
||||
if (sourceSkillId === 498534) return { shape: "cone", radius: 10, attackArcRadians: Math.PI / 2 };
|
||||
if (sourceSkillId === 490400) return { shape: "line", maximumTargets: 8 };
|
||||
if (sourceSkillId === 490457) return { shape: "chain", maximumTargets: 3, jumpRange: 6 };
|
||||
if (sourceSkillId === 492920) return { shape: "circle", radius: 5, maximumTargets: 8 };
|
||||
@@ -686,22 +1047,32 @@ function sourceTargeting(sourceSkillId: number): AbilityDefinition["targeting"]
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sourceRange(sourceSkillId: number, fallback: number): number {
|
||||
// RoM tooltip ranges are expressed in decimeters; Phoenix's source row is
|
||||
// self-targeted, so use its described 120-unit path length for line casting.
|
||||
return sourceSkillId === 490239 ? 12 : fallback;
|
||||
}
|
||||
|
||||
function rank(row: GeneratedRank, parent: GeneratedSkill): AbilityRankDefinition {
|
||||
const rankCosts = costs(row.costs);
|
||||
const rankCosts = costsFor(row);
|
||||
const target = abilityTarget({ ...row, target: row.target ?? parent.target });
|
||||
return {
|
||||
rank: row.rank,
|
||||
spellId: row.sourceSkillId,
|
||||
level: row.level,
|
||||
description: parent.classId === "rom-priest" ? romPriestDescription(row.sourceSkillId, row.description) : row.description,
|
||||
castMode: row.sourceSkillId === 491164 ? "channel" : row.castTimeMs > 0 ? "cast" : "instant",
|
||||
castTimeMs: row.sourceSkillId === 491164 ? 4_000 : row.castTimeMs,
|
||||
cooldownMs: row.cooldownMs,
|
||||
castMode: castModeFor(row),
|
||||
castTimeMs: castTimeMsFor(row),
|
||||
cooldownMs: cooldownMsFor(row),
|
||||
gcdMs: 1_500,
|
||||
...(row.sourceSkillId === 498770 ? { channelPulses: 4 } : {}),
|
||||
cost: rankCosts[0] ?? { resource: "mana", amount: 0 },
|
||||
costs: rankCosts,
|
||||
effects: effects({
|
||||
...row,
|
||||
target: row.target ?? parent.target,
|
||||
classId: parent.classId,
|
||||
secondaryClassId: parent.secondaryClassId,
|
||||
target,
|
||||
source: row.source?.components?.length ? row.source : parent.source,
|
||||
}),
|
||||
};
|
||||
@@ -713,7 +1084,8 @@ function ability(row: GeneratedSkill): AbilityDefinition {
|
||||
const secondaryClassId = row.secondaryClassId && ROM_CLASS_IDS.includes(row.secondaryClassId as RomClassId)
|
||||
? row.secondaryClassId as RomClassId
|
||||
: undefined;
|
||||
const abilityCosts = costs(row.costs);
|
||||
const abilityCosts = costsFor(row);
|
||||
const target = abilityTarget(row);
|
||||
return {
|
||||
id: row.id,
|
||||
classId,
|
||||
@@ -724,19 +1096,20 @@ function ability(row: GeneratedSkill): AbilityDefinition {
|
||||
icon: row.icon,
|
||||
sigil: row.name.split(/\s+/).map((part) => part[0]).join("").slice(0, 3).toUpperCase() || "RW",
|
||||
description: classId === "rom-priest" ? romPriestDescription(row.sourceSkillId, row.description) : row.description,
|
||||
target: row.target,
|
||||
range: { min: 0, max: row.range },
|
||||
target,
|
||||
range: { min: 0, max: sourceRange(row.sourceSkillId, row.range) },
|
||||
...(sourceTargeting(row.sourceSkillId)?.radius !== undefined
|
||||
? { radius: sourceTargeting(row.sourceSkillId)!.radius }
|
||||
: areaRadius(row) !== undefined ? { radius: areaRadius(row) } : {}),
|
||||
...(sourceTargeting(row.sourceSkillId) ? { targeting: sourceTargeting(row.sourceSkillId) } : {}),
|
||||
castMode: row.sourceSkillId === 491164 ? "channel" : row.castTimeMs > 0 ? "cast" : "instant",
|
||||
castTimeMs: row.sourceSkillId === 491164 ? 4_000 : row.castTimeMs,
|
||||
cooldownMs: row.cooldownMs,
|
||||
castMode: castModeFor(row),
|
||||
castTimeMs: castTimeMsFor(row),
|
||||
cooldownMs: cooldownMsFor(row),
|
||||
gcdMs: 1_500,
|
||||
...(row.sourceSkillId === 498770 ? { channelPulses: 4 } : {}),
|
||||
cost: abilityCosts[0] ?? { resource: "mana", amount: 0 },
|
||||
costs: abilityCosts,
|
||||
effects: effects(row),
|
||||
effects: effects({ ...row, target, classId }),
|
||||
passive: row.passive,
|
||||
source: "runewaker",
|
||||
sourceUnlockLevel: row.unlockLevel,
|
||||
|
||||
Reference in new issue
Block a user