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,228 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ClassId } from "../app/characterCatalog";
|
||||
import type { AbilityDefinition } from "./abilityCatalog";
|
||||
import { applyAura, resolveDamageValue, resolveStatValue, type ActiveAura } from "./combatAuras";
|
||||
import {
|
||||
coaGuardianCompanionFormationSpellId,
|
||||
coaGuardianFormationAbilityForCast,
|
||||
COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP,
|
||||
COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION,
|
||||
COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID,
|
||||
COA_GUARDIAN_LINE_FORMATION_AURA,
|
||||
COA_GUARDIAN_LINE_FORMATION_AURA_ID,
|
||||
COA_GUARDIAN_LINE_FORMATION_SPELL_ID,
|
||||
COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER,
|
||||
COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION,
|
||||
COA_GUARDIAN_TOWER_FORMATION_AURA,
|
||||
COA_GUARDIAN_TOWER_FORMATION_AURA_ID,
|
||||
COA_GUARDIAN_TOWER_FORMATION_SPELL_ID,
|
||||
coaGuardianFormationMovementSpeedMultiplier,
|
||||
getCoaGuardianTowerShieldArmorBonus,
|
||||
hasCoaGuardianFormationAura,
|
||||
projectCoaGuardianFormationAuras,
|
||||
resolveCoaGuardianControlDurationMs,
|
||||
type CoaGuardianFormationActor,
|
||||
} from "./coaGuardianFormationRuntime";
|
||||
|
||||
const actor = (
|
||||
id = "guardian-player",
|
||||
classId: ClassId = "guardian",
|
||||
footmansCalling = false,
|
||||
health = 100,
|
||||
): CoaGuardianFormationActor => ({
|
||||
id,
|
||||
classId,
|
||||
health,
|
||||
talentRanks: footmansCalling
|
||||
? { [`coa-entry-${COA_GUARDIAN_FOOTMANS_CALLING_TALENT_ENTRY_ID}`]: 1 }
|
||||
: {},
|
||||
});
|
||||
|
||||
const activeAura = (
|
||||
definition: typeof COA_GUARDIAN_TOWER_FORMATION_AURA | typeof COA_GUARDIAN_LINE_FORMATION_AURA,
|
||||
sourceId = "guardian-player",
|
||||
targetId = sourceId,
|
||||
now = 1_000,
|
||||
): ActiveAura => applyAura([], {
|
||||
definition,
|
||||
sourceId,
|
||||
targetId,
|
||||
now,
|
||||
}).aura;
|
||||
|
||||
const ability = (dbcSpellId: number, classId: ClassId = "guardian"): AbilityDefinition => ({
|
||||
id: `ability-${dbcSpellId}`,
|
||||
classId,
|
||||
dbcSpellId,
|
||||
name: "Formation",
|
||||
unlockLevel: 4,
|
||||
icon: "icon.png",
|
||||
sigil: "FO",
|
||||
description: "Source description",
|
||||
target: "hostile",
|
||||
range: { min: 0, max: 0 },
|
||||
castMode: "instant",
|
||||
castTimeMs: 0,
|
||||
cooldownMs: 0,
|
||||
gcdMs: 0,
|
||||
cost: { resource: "energy", amount: 20 },
|
||||
effects: [{
|
||||
kind: "apply-aura",
|
||||
aura: { id: "generic-import", name: "Generic import", disposition: "buff", durationMs: null },
|
||||
sourceEffectType: 6,
|
||||
sourceAuraType: 142,
|
||||
miscValue: 127,
|
||||
triggerSpellId: 0,
|
||||
}],
|
||||
passive: false,
|
||||
source: "coa-progression",
|
||||
});
|
||||
|
||||
describe("CoA Guardian formation runtime", () => {
|
||||
it("defines persistent exclusive forms with source-specific effects", () => {
|
||||
expect(COA_GUARDIAN_TOWER_FORMATION_AURA).toMatchObject({
|
||||
id: COA_GUARDIAN_TOWER_FORMATION_AURA_ID,
|
||||
durationMs: null,
|
||||
exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP,
|
||||
modifiers: [
|
||||
{ kind: "stat", stat: "block", operation: "flat", value: 10, perStack: false },
|
||||
{ kind: "damage", direction: "dealt", operation: "percent", value: -0.1 },
|
||||
],
|
||||
});
|
||||
expect(COA_GUARDIAN_LINE_FORMATION_AURA).toMatchObject({
|
||||
id: COA_GUARDIAN_LINE_FORMATION_AURA_ID,
|
||||
durationMs: null,
|
||||
exclusiveGroup: COA_GUARDIAN_FORMATION_EXCLUSIVE_GROUP,
|
||||
modifiers: [{ kind: "stat", stat: "movement-speed", operation: "percent", value: 0.15 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("replaces imported effects with one self-applied runtime form aura", () => {
|
||||
const tower = coaGuardianFormationAbilityForCast(ability(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID));
|
||||
expect(tower.cost).toEqual({ resource: "energy", amount: 20 });
|
||||
expect(tower.target).toBe("self");
|
||||
expect(tower.effects).toEqual([{
|
||||
kind: "apply-aura",
|
||||
aura: COA_GUARDIAN_TOWER_FORMATION_AURA,
|
||||
recipient: "caster",
|
||||
sourceEffectType: 6,
|
||||
sourceAuraType: 87,
|
||||
miscValue: 0,
|
||||
triggerSpellId: 0,
|
||||
}]);
|
||||
|
||||
const line = coaGuardianFormationAbilityForCast(ability(COA_GUARDIAN_LINE_FORMATION_SPELL_ID));
|
||||
expect(line.effects).toEqual([expect.objectContaining({
|
||||
kind: "apply-aura",
|
||||
aura: COA_GUARDIAN_LINE_FORMATION_AURA,
|
||||
recipient: "caster",
|
||||
sourceAuraType: 232,
|
||||
miscValue: 7,
|
||||
})]);
|
||||
const warriorAbility = ability(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID, "warrior");
|
||||
expect(coaGuardianFormationAbilityForCast(warriorAbility)).toBe(warriorAbility);
|
||||
const unrelated = ability(123456);
|
||||
expect(coaGuardianFormationAbilityForCast(unrelated)).toBe(unrelated);
|
||||
});
|
||||
|
||||
it("recognizes only the living actor's own active self marker", () => {
|
||||
const self = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA);
|
||||
const foreign = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "another-guardian", "guardian-player");
|
||||
const nonSelf = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "guardian-player", "another-guardian");
|
||||
const expired = { ...self, expiresAt: 1_000 };
|
||||
|
||||
expect(hasCoaGuardianFormationAura([self], actor(), "tower", 1_000)).toBe(true);
|
||||
expect(hasCoaGuardianFormationAura([foreign, nonSelf], actor(), "tower", 1_000)).toBe(false);
|
||||
expect(hasCoaGuardianFormationAura([expired], actor(), "tower", 1_000)).toBe(false);
|
||||
expect(hasCoaGuardianFormationAura([self], actor("guardian-player", "guardian", false, 0), "tower", 999))
|
||||
.toBe(false);
|
||||
expect(hasCoaGuardianFormationAura([self], actor("guardian-player", "warrior"), "tower", 999)).toBe(false);
|
||||
});
|
||||
|
||||
it("projects shield armor and Footman's Calling only on owned forms", () => {
|
||||
const tower = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA);
|
||||
const owner = actor("guardian-player", "guardian", true);
|
||||
const projected = projectCoaGuardianFormationAuras([tower], owner, 1_001, 120);
|
||||
expect(getCoaGuardianTowerShieldArmorBonus(owner, [tower], 1_001, 120)).toBe(60);
|
||||
expect(resolveStatValue(100, projected, "armor")).toBe(160);
|
||||
expect(resolveStatValue(5, projected, "block")).toBe(15);
|
||||
expect(resolveDamageValue(200, projected, { direction: "taken", school: "physical", attackKind: "melee" }))
|
||||
.toBe(180);
|
||||
expect(resolveDamageValue(200, projected, { direction: "dealt", school: "physical", attackKind: "melee" }))
|
||||
.toBe(180);
|
||||
|
||||
const repeatedProjection = projectCoaGuardianFormationAuras(projected, owner, 1_001, 120);
|
||||
expect(resolveStatValue(100, repeatedProjection, "armor")).toBe(160);
|
||||
expect(resolveDamageValue(200, repeatedProjection, { direction: "taken", school: "physical", attackKind: "melee" }))
|
||||
.toBe(180);
|
||||
|
||||
const withoutFootman = projectCoaGuardianFormationAuras([tower], actor(), 1_001, 120);
|
||||
expect(resolveStatValue(100, withoutFootman, "armor")).toBe(160);
|
||||
expect(resolveDamageValue(200, withoutFootman, { direction: "taken", school: "physical", attackKind: "melee" }))
|
||||
.toBe(200);
|
||||
const noLongerLearned = projectCoaGuardianFormationAuras(projected, actor(), 1_001, 120);
|
||||
expect(resolveDamageValue(200, noLongerLearned, { direction: "taken", school: "physical", attackKind: "melee" }))
|
||||
.toBe(200);
|
||||
expect(getCoaGuardianTowerShieldArmorBonus(actor(), [tower], 1_001, 120)).toBe(60);
|
||||
expect(getCoaGuardianTowerShieldArmorBonus(owner, [tower], 1_001, Number.NaN)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not project conditional mitigation or shield armor from foreign or Line forms", () => {
|
||||
const foreignTower = activeAura(COA_GUARDIAN_TOWER_FORMATION_AURA, "other", "guardian-player");
|
||||
const line = activeAura(COA_GUARDIAN_LINE_FORMATION_AURA);
|
||||
const owner = actor("guardian-player", "guardian", true);
|
||||
const foreignAuras = [foreignTower];
|
||||
|
||||
expect(projectCoaGuardianFormationAuras(foreignAuras, owner, 1_001, 120)).toBe(foreignAuras);
|
||||
expect(resolveStatValue(100, projectCoaGuardianFormationAuras(foreignAuras, owner, 1_001, 120), "armor"))
|
||||
.toBe(100);
|
||||
const projectedLine = projectCoaGuardianFormationAuras([line], owner, 1_001, 120);
|
||||
expect(resolveStatValue(100, projectedLine, "armor")).toBe(100);
|
||||
expect(resolveDamageValue(100, projectedLine, { direction: "taken", school: "fire", attackKind: "spell" }))
|
||||
.toBe(90);
|
||||
});
|
||||
|
||||
it("reduces only root duration while the actor's own Line form is active", () => {
|
||||
const line = activeAura(COA_GUARDIAN_LINE_FORMATION_AURA);
|
||||
expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [line], "root", 1_001))
|
||||
.toBe(8_000 * (1 - COA_GUARDIAN_LINE_ROOT_DURATION_REDUCTION));
|
||||
expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [line], "slow", 1_001)).toBe(8_000);
|
||||
expect(resolveCoaGuardianControlDurationMs(8_000, actor(), [], "root", 1_001)).toBe(8_000);
|
||||
expect(resolveCoaGuardianControlDurationMs(8_000, actor("guardian-player", "guardian", false, 0), [line], "root", 1_001))
|
||||
.toBe(8_000);
|
||||
expect(coaGuardianFormationMovementSpeedMultiplier([line], actor(), 1_001))
|
||||
.toBe(COA_GUARDIAN_LINE_MOVEMENT_SPEED_MULTIPLIER);
|
||||
expect(coaGuardianFormationMovementSpeedMultiplier([line], actor("guardian-player", "warrior"), 1_001)).toBe(1);
|
||||
expect(coaGuardianFormationMovementSpeedMultiplier([line], actor("guardian-player", "guardian", false, 0), 1_001))
|
||||
.toBe(1);
|
||||
expect(COA_GUARDIAN_FOOTMANS_CALLING_DAMAGE_REDUCTION).toBe(0.1);
|
||||
});
|
||||
|
||||
it("chooses the authored companion form once and waits while its own form persists", () => {
|
||||
const guardian = actor("guardian-companion");
|
||||
const options = {
|
||||
actor: guardian,
|
||||
role: "vanguard-tank" as const,
|
||||
towerAvailable: true,
|
||||
lineAvailable: true,
|
||||
auras: [] as readonly ActiveAura[],
|
||||
now: 1_000,
|
||||
};
|
||||
|
||||
expect(coaGuardianCompanionFormationSpellId(options)).toBe(COA_GUARDIAN_TOWER_FORMATION_SPELL_ID);
|
||||
expect(coaGuardianCompanionFormationSpellId({ ...options, towerAvailable: false }))
|
||||
.toBe(COA_GUARDIAN_LINE_FORMATION_SPELL_ID);
|
||||
expect(coaGuardianCompanionFormationSpellId({ ...options, role: "other" }))
|
||||
.toBe(COA_GUARDIAN_LINE_FORMATION_SPELL_ID);
|
||||
expect(coaGuardianCompanionFormationSpellId({ ...options, towerAvailable: false, lineAvailable: false }))
|
||||
.toBeNull();
|
||||
|
||||
const activeLine = activeAura(
|
||||
COA_GUARDIAN_LINE_FORMATION_AURA,
|
||||
"guardian-companion",
|
||||
"guardian-companion",
|
||||
options.now,
|
||||
);
|
||||
expect(coaGuardianCompanionFormationSpellId({ ...options, auras: [activeLine] })).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user