Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,310 @@
|
||||
import type { ClassId } from "../app/characterCatalog";
|
||||
import type { AbilityDefinition, AbilityEffect, MobStatusKind } from "./abilityCatalog";
|
||||
import {
|
||||
type ActiveAura,
|
||||
type AuraDefinition,
|
||||
type AuraModifier,
|
||||
} from "./combatAuras";
|
||||
|
||||
export const COA_GUARDIAN_TOWER_FORMATION_SPELL_ID = 800317;
|
||||
export const COA_GUARDIAN_LINE_FORMATION_SPELL_ID = 803130;
|
||||
export const COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID = 4019;
|
||||
|
||||
export const COA_GUARDIAN_TOWER_FORMATION_AURA_ID = "coa:800317:effect:0:aura:87";
|
||||
export const COA_GUARDIAN_LINE_FORMATION_AURA_ID = "coa:803130:effect:0:aura:232";
|
||||
export const COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP = "coa-guardian-formation";
|
||||
export const COA_GUARDIAN_FORMATION_DURATION_MS = null;
|
||||
export const COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION = 0.1;
|
||||
export const COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION = 0.25;
|
||||
export const COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER = 1.15;
|
||||
export const COA_GUARDIAN_TOWER_SHIELD_ARMOR_BONUS = 0.5;
|
||||
|
||||
const FOOTMANS_CALLING_RANK_KEY = `coa-entry-${COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID}`;
|
||||
|
||||
export interface CoaGuardianFormationActor {
|
||||
readonly id: string;
|
||||
readonly classId: ClassId;
|
||||
readonly talentRanks: Readonly<Record<string, number | undefined>>;
|
||||
/** Omitted health is treated as alive for call sites that only hold a combat snapshot. */
|
||||
readonly health?: number;
|
||||
}
|
||||
|
||||
export type CoaGuardianFormation = "tower" | "line";
|
||||
|
||||
const formationAuraId = (formation: CoaGuardianFormation): string => (
|
||||
formation === "tower"
|
||||
? COA_GUARDIAN_TOWER_FORMATION_AURA_ID
|
||||
: COA_GUARDIAN_LINE_FORMATION_AURA_ID
|
||||
);
|
||||
|
||||
const towerModifiers: readonly AuraModifier[] = [
|
||||
{
|
||||
kind: "stat",
|
||||
stat: "block",
|
||||
operation: "flat",
|
||||
value: 10,
|
||||
perStack: false,
|
||||
},
|
||||
{
|
||||
kind: "damage",
|
||||
direction: "dealt",
|
||||
operation: "percent",
|
||||
value: -0.1,
|
||||
},
|
||||
];
|
||||
|
||||
const lineModifiers: readonly AuraModifier[] = [
|
||||
{
|
||||
kind: "stat",
|
||||
stat: "movement-speed",
|
||||
operation: "percent",
|
||||
value: 0.15,
|
||||
},
|
||||
];
|
||||
|
||||
/** Source-specific Tower effects; shield armor is projected from equipped gear at runtime. */
|
||||
export const COA_GUARDIAN_TOWER_FORMATION_AURA: AuraDefinition = {
|
||||
id: COA_GUARDIAN_TOWER_FORMATION_AURA_ID,
|
||||
name: "Tower Formation",
|
||||
description: "Increases block chance and shield armor contribution, but reduces damage dealt.",
|
||||
disposition: "buff",
|
||||
durationMs: COA_GUARDIAN_FORMATION_DURATION_MS,
|
||||
stackBehavior: "replace",
|
||||
uniqueBySource: true,
|
||||
exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP,
|
||||
modifiers: towerModifiers,
|
||||
};
|
||||
|
||||
/** Source-specific Line effects; miscValue 7 on Aura 232 narrows this to root controls. */
|
||||
export const COA_GUARDIAN_LINE_FORMATION_AURA: AuraDefinition = {
|
||||
id: COA_GUARDIAN_LINE_FORMATION_AURA_ID,
|
||||
name: "Line Formation",
|
||||
description: "Reduces root duration and increases movement speed.",
|
||||
disposition: "buff",
|
||||
durationMs: COA_GUARDIAN_FORMATION_DURATION_MS,
|
||||
stackBehavior: "replace",
|
||||
uniqueBySource: true,
|
||||
exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP,
|
||||
modifiers: lineModifiers,
|
||||
};
|
||||
|
||||
function actorCanUseFormation(actor: CoaGuardianFormationActor): boolean {
|
||||
return actor.classId === "guardian"
|
||||
&& (actor.health === undefined || (Number.isFinite(actor.health) && actor.health > 0));
|
||||
}
|
||||
|
||||
function isActiveOwnedFormationAura(
|
||||
aura: ActiveAura,
|
||||
actor: CoaGuardianFormationActor,
|
||||
formation: CoaGuardianFormation,
|
||||
now: number,
|
||||
): boolean {
|
||||
return actorCanUseFormation(actor)
|
||||
&& aura.definition.id === formationAuraId(formation)
|
||||
&& aura.sourceId === actor.id
|
||||
&& aura.targetId === actor.id
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now);
|
||||
}
|
||||
|
||||
/** True only for the actor's own live, self-targeted Guardian formation marker. */
|
||||
export function hasCoaGuardianFormationAura(
|
||||
auras: readonly ActiveAura[],
|
||||
actor: CoaGuardianFormationActor,
|
||||
formation: CoaGuardianFormation,
|
||||
now: number,
|
||||
): boolean {
|
||||
return auras.some((aura) => isActiveOwnedFormationAura(aura, actor, formation, now));
|
||||
}
|
||||
|
||||
export function hasCoaGuardianFootmansCalling(actor: CoaGuardianFormationActor): boolean {
|
||||
return actor.classId === "guardian"
|
||||
&& Math.max(0, Math.trunc(actor.talentRanks[FOOTMANS_CALLING_RANK_KEY] ?? 0)) > 0;
|
||||
}
|
||||
|
||||
function isProjectedFootmansCallingModifier(modifier: AuraModifier): boolean {
|
||||
return modifier.kind === "damage"
|
||||
&& modifier.direction === "taken"
|
||||
&& modifier.operation === "percent"
|
||||
&& modifier.value === -COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION
|
||||
&& modifier.school === undefined
|
||||
&& modifier.attackKind === undefined
|
||||
&& modifier.perStack === false;
|
||||
}
|
||||
|
||||
function isProjectedTowerShieldArmorModifier(modifier: AuraModifier): boolean {
|
||||
return modifier.kind === "stat"
|
||||
&& modifier.stat === "armor"
|
||||
&& modifier.operation === "flat"
|
||||
&& modifier.perStack === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return shield armor contributed by Tower Formation. The input is the equipped
|
||||
* shield's already-resolved armor value, not total armor or resistance.
|
||||
*/
|
||||
export function getCoaGuardianTowerShieldArmorBonus(
|
||||
actor: CoaGuardianFormationActor,
|
||||
auras: readonly ActiveAura[],
|
||||
now: number,
|
||||
shieldArmor: number,
|
||||
): number {
|
||||
if (!hasCoaGuardianFormationAura(auras, actor, "tower", now) || !Number.isFinite(shieldArmor)) return 0;
|
||||
return Math.max(0, shieldArmor) * COA_GUARDIAN_TOWER_SHIELD_ARMOR_BONUS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Project source-owned Guardian formation behavior into an aura view. Footman's
|
||||
* Calling is conditional on the actor's own live form and talent; Tower's shield
|
||||
* armor bonus uses only the equipped shield's resolved armor contribution.
|
||||
*/
|
||||
export function projectCoaGuardianFormationAuras(
|
||||
auras: readonly ActiveAura[],
|
||||
actor: CoaGuardianFormationActor,
|
||||
now: number,
|
||||
shieldArmor = 0,
|
||||
): readonly ActiveAura[] {
|
||||
if (!actorCanUseFormation(actor)) return auras;
|
||||
|
||||
const hasFootmansCalling = hasCoaGuardianFootmansCalling(actor);
|
||||
const towerShieldArmorBonus = getCoaGuardianTowerShieldArmorBonus(actor, auras, now, shieldArmor);
|
||||
let changed = false;
|
||||
const projected = auras.map((aura) => {
|
||||
const formation = aura.definition.id === COA_GUARDIAN_TOWER_FORMATION_AURA_ID
|
||||
? "tower"
|
||||
: aura.definition.id === COA_GUARDIAN_LINE_FORMATION_AURA_ID ? "line" : null;
|
||||
if (!formation || !isActiveOwnedFormationAura(aura, actor, formation, now)) return aura;
|
||||
|
||||
const currentModifiers = aura.definition.modifiers ?? [];
|
||||
const baseModifiers = currentModifiers.filter((modifier) => (
|
||||
!isProjectedFootmansCallingModifier(modifier)
|
||||
&& !(formation === "tower" && isProjectedTowerShieldArmorModifier(modifier))
|
||||
));
|
||||
const extraModifiers: AuraModifier[] = [];
|
||||
if (hasFootmansCalling) {
|
||||
extraModifiers.push({
|
||||
kind: "damage",
|
||||
direction: "taken",
|
||||
operation: "percent",
|
||||
value: -COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION,
|
||||
perStack: false,
|
||||
});
|
||||
}
|
||||
if (formation === "tower" && towerShieldArmorBonus > 0) {
|
||||
extraModifiers.push({
|
||||
kind: "stat",
|
||||
stat: "armor",
|
||||
operation: "flat",
|
||||
value: towerShieldArmorBonus,
|
||||
perStack: false,
|
||||
});
|
||||
}
|
||||
if (extraModifiers.length === 0 && baseModifiers.length === currentModifiers.length) return aura;
|
||||
|
||||
changed = true;
|
||||
return {
|
||||
...aura,
|
||||
definition: {
|
||||
...aura.definition,
|
||||
modifiers: [...baseModifiers, ...extraModifiers],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return changed ? projected : auras;
|
||||
}
|
||||
|
||||
/** Aura 232 carries miscValue 7 (Root in the project's source mechanic mapping). */
|
||||
export function resolveCoaGuardianControlDurationMs(
|
||||
durationMs: number,
|
||||
actor: CoaGuardianFormationActor,
|
||||
auras: readonly ActiveAura[],
|
||||
mechanic: MobStatusKind,
|
||||
now: number,
|
||||
): number {
|
||||
if (mechanic !== "root"
|
||||
|| !Number.isFinite(durationMs)
|
||||
|| durationMs <= 0
|
||||
|| !hasCoaGuardianFormationAura(auras, actor, "line", now)) return durationMs;
|
||||
return Math.max(0, Math.round(durationMs * (1 - COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION)));
|
||||
}
|
||||
|
||||
/** Movement multiplier for PlayerRig/party paths that do not read aura stat modifiers. */
|
||||
export function coaGuardianFormationMovementSpeedMultiplier(
|
||||
auras: readonly ActiveAura[],
|
||||
actor: CoaGuardianFormationActor,
|
||||
now: number,
|
||||
): number {
|
||||
return hasCoaGuardianFormationAura(auras, actor, "line", now)
|
||||
? COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER
|
||||
: 1;
|
||||
}
|
||||
|
||||
function formationAuraEffect(
|
||||
aura: AuraDefinition,
|
||||
sourceAuraType: number,
|
||||
miscValue: number,
|
||||
): AbilityEffect {
|
||||
return {
|
||||
kind: "apply-aura",
|
||||
aura,
|
||||
recipient: "caster",
|
||||
sourceEffectType: 6,
|
||||
sourceAuraType,
|
||||
miscValue,
|
||||
triggerSpellId: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/** Replace misconverted generic source effects with each formation's runtime-owned marker. */
|
||||
export function coaGuardianFormationAbilityForCast(ability: AbilityDefinition): AbilityDefinition {
|
||||
if (ability.classId !== "guardian") return ability;
|
||||
|
||||
if (ability.dbcSpellId === COA_GUARDIAN_TOWER_FORMATION_SPELL_ID) {
|
||||
return {
|
||||
...ability,
|
||||
target: "self",
|
||||
description: "Increases block chance by 10% and the armor contribution of your shield by 50%, but reduces your damage dealt by 10%. Replaces your current formation.",
|
||||
effects: [formationAuraEffect(COA_GUARDIAN_TOWER_FORMATION_AURA, 87, 0)],
|
||||
};
|
||||
}
|
||||
if (ability.dbcSpellId === COA_GUARDIAN_LINE_FORMATION_SPELL_ID) {
|
||||
return {
|
||||
...ability,
|
||||
target: "self",
|
||||
description: "Reduces root duration by 25% and increases movement speed by 15%. Replaces your current formation.",
|
||||
effects: [formationAuraEffect(COA_GUARDIAN_LINE_FORMATION_AURA, 232, 7)],
|
||||
};
|
||||
}
|
||||
return ability;
|
||||
}
|
||||
|
||||
export interface CoaGuardianCompanionFormationOptions {
|
||||
readonly actor: CoaGuardianFormationActor;
|
||||
/** The Vanguard tank role is authored to prefer Tower; other Guardian roles prefer Line. */
|
||||
readonly role: "vanguard-tank" | "other";
|
||||
readonly towerAvailable: boolean;
|
||||
readonly lineAvailable: boolean;
|
||||
readonly auras: readonly ActiveAura[];
|
||||
readonly now: number;
|
||||
}
|
||||
|
||||
/** Choose a preferred form only when no own active form is already present. */
|
||||
export function coaGuardianCompanionFormationSpellId(
|
||||
options: CoaGuardianCompanionFormationOptions,
|
||||
): number | null {
|
||||
const { actor, auras, now } = options;
|
||||
if (!actorCanUseFormation(actor)
|
||||
|| hasCoaGuardianFormationAura(auras, actor, "tower", now)
|
||||
|| hasCoaGuardianFormationAura(auras, actor, "line", now)) return null;
|
||||
|
||||
const candidates = options.role === "vanguard-tank"
|
||||
? [
|
||||
[options.towerAvailable, COA_GUARDIAN_TOWER_FORMATION_SPELL_ID],
|
||||
[options.lineAvailable, COA_GUARDIAN_LINE_FORMATION_SPELL_ID],
|
||||
] as const
|
||||
: [
|
||||
[options.lineAvailable, COA_GUARDIAN_LINE_FORMATION_SPELL_ID],
|
||||
[options.towerAvailable, COA_GUARDIAN_TOWER_FORMATION_SPELL_ID],
|
||||
] as const;
|
||||
return candidates.find(([available]) => available)?.[1] ?? null;
|
||||
}
|
||||
Reference in new issue
Block a user