forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user