Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
import type { AbilityAmountScaling } from "./abilityCatalog";
import { clampPlayerLevel } from "./progression";
import { resolveRomNativeHolyHeal, resolveRomNativePeriodicHolyHeal, romNativeHolyHealSourceMetadata } from "./romNativeHealing";
import { resolveRomNativeTrainingWeaponProfile, findRomNativeAttackComponent, calculateRomNativeDamagePower, resolveRomNativeAutoAttackProfile, romNativeTrainingShieldDefense, type RomNativeDamageWeaponSlot } from "./romNativeDamage";
import { resolvedEquipmentStats } from "./heirloomEquipment";
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
import { nativePeriodicDamageSnapshot, romNativePeriodicDamageBinding,
type RomNativePeriodicDamageCombatantStats, type RomNativePeriodicDamageMagicResistance } from "./romNativePeriodicDamage";
import type { RomNativeDerivedStats } from "./romNativeStatsTypes";
/** Simulator policy: skills train with the character; there is no separate TP/skill-level progression yet. */
export const romNativeMagicLevel = (level: number): number => clampPlayerLevel(level);
const bindings = romNativeHolyHealSourceMetadata.attackBindings.flatMap((ability) => ability.ranks.flatMap((rank) =>
rank.componentIds.map((component) => ({ abilityId: ability.abilityId, rank, component })),
));
export function romNativeAmountBinding(source: NonNullable<AbilityAmountScaling["romNativeSource"]>) {
return bindings.find(({ rank, component }) => rank.sourceSkillId === source.skillId
&& component.componentId === source.componentId) ?? null;
}
/** Source caster power only. Recipient modifiers are resolved at each real healing target. */
export function nativeHolyHealCasterAmount(
scaling: AbilityAmountScaling,
level: number,
caster: RomNativeDerivedStats,
): number | null {
const source = scaling.romNativeSource;
if (!source || source.kind !== "heal") return null;
const binding = romNativeAmountBinding(source);
if (!binding || binding.component.calculationType !== 5) return null;
const { rank, component } = binding;
return Math.max(0, resolveRomNativeHolyHeal({
source: {
componentId: component.componentId,
sourceSkillId: rank.sourceSkillId,
damagePower: component.damagePower,
damagePowerSkillLevelArg: component.damagePowerSkillLevelArg,
magicLevel: romNativeMagicLevel(level),
spellTimeSeconds: rank.spellTimeSeconds,
isIgnoreDmgEffect: rank.isIgnoreDmgEffect,
isFixDmgEffect: rank.isFixDmgEffect,
ignoreMagicAbsorb: component.ignoreMagicAbsorb,
randomRange: component.randomRange,
},
caster,
target: { healing: { ...caster.healing, targetHealAbsorbRate: 0, targetHealAbsorb: 0 } },
// Combat policy uses source mean variance; the common healing resolver handles crit separately.
randomUnit: 0.5,
}).amount);
}
export function nativeHolyHealTargetAmount(
amount: number,
scaling: AbilityAmountScaling,
target: RomNativeDerivedStats | undefined,
): number {
if (!target || !scaling.romNativeSource || scaling.romNativeSource.kind !== "heal") return amount;
const binding = romNativeAmountBinding(scaling.romNativeSource);
if (!binding || binding.component.calculationType !== 5 || binding.component.ignoreMagicAbsorb) return amount;
return Math.max(0, amount * (1 + target.healing.targetHealAbsorbRate / 100) + target.healing.targetHealAbsorb);
}
const referenceGearCache = new Map<string, Readonly<Record<string, number>>>();
export interface RomNativeDamageTargetContext {
readonly targetCount: number;
readonly targetOrdinal: number;
}
export function nativeDamageCasterAmount(
scaling: AbilityAmountScaling,
level: number,
caster: RomNativeDerivedStats,
targetContext?: RomNativeDamageTargetContext,
): number | null {
const source = scaling.romNativeSource;
if (!source || source.kind !== "damage") return null;
const binding = romNativeAmountBinding(source);
if (!binding) return null;
// The shared amount adapter runs before recipient enumeration. Avoid using
// first-target power for every recipient of a source share/decrease attack.
if (!targetContext && (binding.rank.isShareDamage || (binding.rank.rangeType !== 0 && binding.rank.decreaseArg !== 0))) return null;
const trainingGearProfileMatches = matchesNativeTrainingGear(caster, level);
const profile = resolveRomNativeTrainingWeaponProfile(caster, { trainingGearProfileMatches });
const component = findRomNativeAttackComponent({ abilityId: binding.abilityId,
sourceSkillId: source.skillId, componentId: source.componentId, rank: binding.rank.rank ?? 1 });
if (profile.status !== "supported" || component.status !== "supported") return null;
const result = calculateRomNativeDamagePower({ stats: caster, trainingWeapons: profile.value,
component: component.value, magicLevel: romNativeMagicLevel(level), ownerKind: "player",
// Native school modifiers remain on the shared outgoing-aura stage, once.
magicSchoolRate: 1, ...targetContext,
specialInputs: {
// Training profiles equip no throwing item. All-range BodyRate is neutral
// in this adapter; dealt-damage auras remain in the shared stage.
// residualFlatDamage is the source Fin.DMG bridge after weapon removal.
throwDamage: profile.value.residualFlatDamage * caster.rateFactors.physicalDamage,
// ShieldDef is Owner.BaseEQ's raw category sum, never aggregate DEF.
shieldDefense: caster.sourceInputs.classId === "rom-knight"
? romNativeTrainingShieldDefense("rom-knight", level) : 0,
} });
return result.status === "supported" ? result.value.damageBeforeTargetResolver : null;
}
const periodicBindings = romNativeHolyHealSourceMetadata.periodicHealBindings.flatMap((ability) =>
ability.ranks.flatMap((rank) => rank.componentIds.map((component) => ({ rank, component }))));
/** Source DotMagic stores a rank correction at application. Keep this amount frozen for the buff. */
export function nativePeriodicHealSnapshot(
scaling: AbilityAmountScaling,
level: number,
caster: RomNativeDerivedStats | undefined,
target?: RomNativeDerivedStats,
targetLevel = level,
) {
const source = scaling.romNativeSource;
if (!caster || !source || source.kind !== "hot") return undefined;
const binding = periodicBindings.find(({ rank, component }) => rank.sourceSkillId === source.skillId
&& component.componentId === source.componentId);
if (!binding) return undefined;
const { rank, component } = binding;
return resolveRomNativePeriodicHolyHeal({
source: { ...component, sourceSkillId: rank.sourceSkillId,
rawSpellTimeSeconds: rank.spellTimeSeconds, isFixDmgEffect: rank.isFixDmgEffect,
magicLevel: romNativeMagicLevel(level) },
caster: { ...caster, effectiveMagicPower: caster.magicPowerRate },
target: target ?? { healing: { ...caster.healing, targetHealAbsorbRate: 0, targetHealAbsorb: 0 } },
sourceIsPlayer: true, casterLevel: level, targetLevel,
});
}
export function matchesNativeTrainingGear(caster: RomNativeDerivedStats, level: number): boolean {
const identity = ROM_FIVE_IDENTITIES.find((entry) => entry.classId === caster.sourceInputs.classId);
if (!identity) return false;
const key = `${identity.classId}:${level}:${caster.sourceInputs.secondaryClassId}`;
let referenceGear = referenceGearCache.get(key);
if (!referenceGear) {
referenceGear = resolvedEquipmentStats({}, [], {
ownerId: "native-source-reference", ownerKind: "player", classId: identity.classId,
secondaryClassId: caster.sourceInputs.secondaryClassId, specializationId: null, role: identity.role, level,
}).romNative as Readonly<Record<string, number>> | undefined;
if (!referenceGear) return false;
referenceGearCache.set(key, referenceGear);
}
const currentGear = caster.sourceInputs.gear as Readonly<Record<string, number>>;
return [...new Set([...Object.keys(referenceGear), ...Object.keys(currentGear)])]
.every((key) => Math.abs((referenceGear![key] ?? 0) - (currentGear[key] ?? 0)) < 0.00001);
}
export function nativeTrainingAutoAttackProfile(caster: RomNativeDerivedStats | undefined, slot: RomNativeDamageWeaponSlot) {
if (!caster) return undefined;
const profile = resolveRomNativeTrainingWeaponProfile(caster,
{ trainingGearProfileMatches: matchesNativeTrainingGear(caster, caster.sourceInputs.level) });
if (profile.status !== "supported") return undefined;
const result = resolveRomNativeAutoAttackProfile({ stats: caster, trainingWeapons: profile.value, slot });
return result.status === "supported" ? result.value : undefined;
}
/** Native outgoing DotMagic freezes its source rank and target defense at application. */
export function nativePeriodicDamageCasterSnapshot(
scaling: AbilityAmountScaling,
level: number,
caster: RomNativeDerivedStats | undefined,
target: RomNativePeriodicDamageCombatantStats | undefined,
targetLevel: number,
magicResistance?: RomNativePeriodicDamageMagicResistance,
effectiveMagicPower?: number,
) {
if (!caster || !target || scaling.romNativeSource?.kind !== "dot") return undefined;
const source = romNativePeriodicDamageBinding(scaling.romNativeSource);
if (!source) return undefined;
const mainHand = nativeTrainingAutoAttackProfile(caster, "main-hand");
const result = nativePeriodicDamageSnapshot({ source, caster, target,
sourceIsPlayer: true, magicLevel: romNativeMagicLevel(level), casterLevel: level, targetLevel,
mainDamage: mainHand?.damage, mainAttackSpeed: mainHand?.attackSpeed,
effectiveMagicPower, magicResistance,
});
return result.status === "supported" ? result.value : undefined;
}