Update Healer Man server

This commit is contained in:
phenom committed 2026-09-28 22:46:45 -04:00
1 parent 9d7e0e2f9e
commit 35dfdaed61
94 files changed
+17034 -418

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
import type { AbilityEffect } from "./abilityCatalog";
import type { RomGeneratedAbilityLike } from "./romAbilitySemantics";
function sourceDurationMs(row: RomGeneratedAbilityLike, fallbackSeconds: number): number {
const duration = row.source?.components?.find((component) => (component.effectTime ?? 0) > 0)?.effectTime;
return Math.max(1_000, Math.round((duration ?? fallbackSeconds) * 1_000));
}
function spiritOfLifeHot(effects: readonly AbilityEffect[]): AbilityEffect {
const heals = effects.filter((effect): effect is Extract<AbilityEffect, { kind: "heal" }> => effect.kind === "heal");
const sum = (key: "coefficient" | "basePoints" | "dieSides" | "pointsPerLevel" | "coefficientPerLevel"
| "bonusPowerCoefficient" | "attackPowerCoefficient" | "spiritCoefficient" | "percentMaxHealth") => (
heals.reduce((total, effect) => total + (effect[key] ?? 0), 0)
);
const hot: Extract<AbilityEffect, { kind: "hot" }> = {
kind: "hot",
coefficient: sum("coefficient"),
ticks: 10,
intervalMs: 4_000,
recipient: "caster",
scope: "party",
center: "caster",
radius: 7,
...(sum("percentMaxHealth") ? { percentMaxHealth: sum("percentMaxHealth") } : {}),
...(sum("basePoints") ? { basePoints: sum("basePoints") } : {}),
...(sum("dieSides") ? { dieSides: sum("dieSides") } : {}),
...(sum("pointsPerLevel") ? { pointsPerLevel: sum("pointsPerLevel") } : {}),
...(sum("coefficientPerLevel") ? { coefficientPerLevel: sum("coefficientPerLevel") } : {}),
...(sum("bonusPowerCoefficient") ? { bonusPowerCoefficient: sum("bonusPowerCoefficient") } : {}),
...(sum("attackPowerCoefficient") ? { attackPowerCoefficient: sum("attackPowerCoefficient") } : {}),
...(sum("spiritCoefficient") ? { spiritCoefficient: sum("spiritCoefficient") } : {}),
...(heals.find((effect) => effect.sourceLevel !== undefined)?.sourceLevel === undefined
? {} : { sourceLevel: heals.find((effect) => effect.sourceLevel !== undefined)!.sourceLevel }),
};
return hot;
}
function stateAura(
id: string,
name: string,
durationMs: number,
modifiers: NonNullable<Extract<AbilityEffect, { kind: "apply-aura" }>['aura']['modifiers']>,
): AbilityEffect {
return {
kind: "apply-aura",
recipient: "caster",
aura: {
id,
name,
disposition: "buff",
durationMs,
modifiers,
utilityTags: ["rom-druid-unity-with-mother-earth", "rom-druid-movement-locked"],
},
sourceEffectType: -1,
sourceAuraType: -1,
miscValue: 0,
triggerSpellId: 0,
};
}
function passiveState(sourceId: number, name: string, tag: string): AbilityEffect {
return {
kind: "apply-aura",
recipient: "caster",
aura: {
id: `rom-druid-passive-${sourceId}`,
name,
disposition: "buff",
durationMs: null,
utilityTags: [tag],
},
sourceEffectType: -1,
sourceAuraType: -1,
miscValue: 0,
triggerSpellId: sourceId,
};
}
/** Source-specific player effects for Druid rows the generic translator cannot express. */
export function overrideRomDruidEffects(
row: RomGeneratedAbilityLike,
translated: readonly AbilityEffect[],
): readonly AbilityEffect[] | null {
switch (row.sourceSkillId) {
case 493531: // Savage Blessing buffs a selected friendly target.
return translated.map((effect) => effect.kind === "apply-aura" ? {
...effect,
recipient: "ability-target",
aura: { ...effect.aura, disposition: "buff" },
} : effect);
case 493536: { // Spirit Guidance charms a non-player enemy for the source effect duration.
const effectTimeSeconds = row.source?.components?.find((component) => component.id === 503805)?.effectTime ?? 3;
return [{
kind: "status",
status: "charm",
durationMs: Math.max(1_000, Math.round(effectTimeSeconds * 1_000)),
maxTargets: 1,
breakOnDamage: false,
}];
}
case 493548: // Unity guarantees critical spell results and adds 10% healing for 15 seconds.
return [stateAura(
"rom-druid-unity-with-mother-earth",
row.name,
sourceDurationMs(row, 15),
[
{ kind: "stat", stat: "magical-critical-strike", operation: "flat", value: 100 },
{ kind: "healing", direction: "done", operation: "percent", value: 0.1 },
],
)];
case 494200: // Extended Recovery: passive state extends Recover by six seconds.
return [passiveState(494200, row.name, "rom-druid-extended-recovery")];
case 494935: // Holy Fury: enables the zero-Nature-Power Wrath duration branch.
return [passiveState(494935, row.name, "rom-druid-holy-fury")];
case 499944: // Knee Ulcer gates the five-second root child on Necrotic Wound.
return [passiveState(499944, row.name, "rom-druid-knee-ulcer")];
case 494025: // Preserve both Earth Chain payload branches for cast-time selection.
return translated.filter((effect) => effect.kind === "damage").map((effect) => ({
...effect,
falloffPerJump: 0.7,
}));
case 494321: // Its root is supplied by the optional Knee Ulcer passive.
return translated.filter((effect) => effect.kind !== "apply-aura" || effect.control?.kind !== "root");
case 494938: // 505938 supplies the 40-second lifetime; the tooltip states 4-second party pulses.
return [spiritOfLifeHot(translated)];
default:
return null;
}
}
function hasTag(tags: readonly string[] | ReadonlySet<string>, tag: string): boolean {
return Array.isArray(tags) ? tags.includes(tag) : (tags as ReadonlySet<string>).has(tag);
}
function naturePowerDrain(): AbilityEffect {
return { kind: "resource-drain", amount: 1, burn: false, resource: "nature-power" };
}
function spendBeforeNaturePowerRefund(effects: readonly AbilityEffect[]): readonly AbilityEffect[] {
const refundIndex = effects.findIndex((effect) => effect.kind === "resource"
&& effect.resource === "nature-power" && effect.amount > 0);
if (refundIndex < 0) return [...effects, naturePowerDrain()];
return [...effects.slice(0, refundIndex), naturePowerDrain(), ...effects.slice(refundIndex)];
}
/** Resolve Druid source branches that depend on current Nature Power or learned passive state. */
export function romDruidCastEffects(
ability: import("./abilityCatalog").AbilityDefinition,
effects: readonly AbilityEffect[],
naturePower: number,
passiveTags: readonly string[] | ReadonlySet<string> = [],
): readonly AbilityEffect[] {
if (ability.source !== "runewaker") return effects;
const empowered = naturePower >= 1;
const damage = effects.filter((effect) => effect.kind === "damage");
const heals = effects.filter((effect): effect is Extract<AbilityEffect, { kind: "heal" }> => effect.kind === "heal");
const selectDamage = (index: number, count = 1): readonly AbilityEffect[] => {
const selected = damage[index];
return selected ? Array.from({ length: count }, () => selected) : [];
};
switch (ability.dbcSpellId) {
case 493529: { // DMG1 is baseline; DMG0 is the one-point Nature Power branch.
const selected = heals[empowered ? 1 : 0] ?? heals[0];
if (!selected) return effects;
return [
...effects.filter((effect) => effect.kind !== "heal"),
{
...selected,
scope: "party",
center: "ability-target",
radius: 10,
},
...(empowered ? [naturePowerDrain()] : []),
];
}
case 493546: // Weakening Seed consumes and refunds one point when powered.
case 493545: // Withering Seed has the same source resource branch.
return empowered ? spendBeforeNaturePowerRefund(effects) : effects;
case 493549: { // Select DMG1/DMG0, keep the single source DoT, and consume one point if powered.
const selectedDamage = damage[empowered ? 1 : 0];
if (!selectedDamage) return effects;
const selected = effects.filter((effect) => effect.kind !== "damage" || effect === selectedDamage)
.map((effect) => (
effect.kind === "dot" && !empowered && hasTag(passiveTags, "rom-druid-holy-fury")
? { ...effect, ticks: effect.ticks + 3 }
: effect
));
return empowered ? spendBeforeNaturePowerRefund(selected) : selected;
}
case 493542: { // The powered DMG0 component hits all targets twice; no creature summon row exists.
const hits = selectDamage(empowered ? 1 : 0, empowered ? 2 : 1);
return [
...hits,
...effects.filter((effect) => effect.kind !== "damage" && effect.kind !== "summon"),
...(empowered ? [naturePowerDrain()] : []),
];
}
case 494025: { // Earth Chain keeps its 70% per-jump falloff in either branch.
const selectedDamage = damage[empowered ? 1 : 0];
if (!selectedDamage) return effects;
return [selectedDamage, ...(empowered ? [naturePowerDrain()] : [])];
}
case 494365: { // Healing Wind selects DMG1 or DMG0 and heals the caster's nearby party.
const selected = heals[empowered ? 1 : 0] ?? heals[0];
if (!selected) return effects;
return [{
...selected,
scope: "party",
center: "caster",
radius: 10,
}, ...(empowered ? [naturePowerDrain()] : [])];
}
case 494216: { // Heart Piercing selects its DMG1/DMG0 strike and spends one point when powered.
const selectedDamage = damage[empowered ? 1 : 0];
if (!selectedDamage) return effects;
return [
...effects.filter((effect) => effect.kind !== "damage" || effect === selectedDamage),
...(empowered ? [naturePowerDrain()] : []),
];
}
case 494018: { // Cross of Thorns uses two DMG1 hits or two DMG0 hits after spending one point.
const selectedHits = empowered ? selectDamage(1, 2) : [damage[0], damage[2]].filter((effect) => effect !== undefined);
if (!selectedHits.length) return effects;
const selected = effects.filter((effect) => effect.kind !== "damage"
&& !(empowered && effect.kind === "resource" && effect.resource === "nature-power"));
return [...selectedHits, ...selected, ...(empowered ? [naturePowerDrain()] : [])];
}
case 494022: { // Poisonous Widow Embrace selects one damage component and refunds a spent point.
const selectedDamage = damage[empowered ? 1 : 0];
if (!selectedDamage) return effects;
const selected = effects.filter((effect) => effect.kind !== "damage" || effect === selectedDamage);
return empowered ? spendBeforeNaturePowerRefund(selected) : selected;
}
case 493528: { // Extended Recovery's source-backed duration increase; proc chance is absent.
if (!hasTag(passiveTags, "rom-druid-extended-recovery")) return effects;
return effects.map((effect) => effect.kind === "hot"
? { ...effect, ticks: effect.ticks + 3 }
: effect);
}
case 494321: { // The passive's root is only emitted when Knee Ulcer is learned.
if (!hasTag(passiveTags, "rom-druid-knee-ulcer")) return effects;
return [...effects, {
kind: "apply-aura",
recipient: "ability-target",
aura: {
id: "rom-druid-knee-ulcer-root",
name: "Knee Ulcer",
disposition: "debuff",
durationMs: 5_000,
dispelCategory: "magic",
},
control: { kind: "root" },
sourceEffectType: -1,
sourceAuraType: -1,
miscValue: 0,
triggerSpellId: 499944,
}];
}
default:
return effects;
}
}
/** Add Spirit of Life's source target-max-health component to a periodic heal tick. */
export function romDruidPeriodicHealAmount(
baseAmount: number,
targetMaxHealth: number,
percentMaxHealth: number,
): number {
return Math.max(0, baseAmount) + Math.max(0, targetMaxHealth) * Math.max(0, percentMaxHealth);
}