Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user