Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
@@ -0,0 +1,273 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { useCombatStore } from "./combatStore";
|
||||
import { resetCombatSpatialProvider, registerCombatSpatialProvider } from "./combatSpatial";
|
||||
import { useDungeonSimulationStore } from "./dungeonSimulationStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { commitPartyAbilityUse, partyAbilityIsReady } from "./partyAbilityAi";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition } from "./partyRuntimeRegistry";
|
||||
import { ROM_ALL_ABILITIES } from "./romAbilityCatalog";
|
||||
import {
|
||||
ROM_SCOUT_PRIEST_SCREEN_ATTACK_AURA,
|
||||
ROM_SCOUT_PRIEST_SCREEN_ATTACK_ID,
|
||||
} from "./romScoutPriestScreenAttack";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
|
||||
const SCREEN_ATTACK = ROM_ALL_ABILITIES.find((ability) => ability.dbcSpellId === ROM_SCOUT_PRIEST_SCREEN_ATTACK_ID);
|
||||
const SCOUT_PERIODIC_ABILITY = ROM_ALL_ABILITIES.find((ability) => ability.id === "rom-scout-primary-5");
|
||||
const SCREEN_ATTACK_ID = "rom-scout-rom-priest-elite-70";
|
||||
const DIRECT_TEST_ABILITY_ID = "rom-screen-attack-direct-fixture";
|
||||
const PERIODIC_TEST_ABILITY_ID = "rom-screen-attack-periodic-fixture";
|
||||
const DIRECT_TEST_TARGET = "screen-attack-direct-target";
|
||||
const DOT_TEST_TARGET = "screen-attack-dot-target";
|
||||
const EXPIRY_TEST_TARGET = "screen-attack-expiry-target";
|
||||
|
||||
function makePriest(name: string, seed: string, index: number, health?: number) {
|
||||
const member = createConfiguredPartyMember({
|
||||
name,
|
||||
classId: "rom-priest",
|
||||
specialization: "Primary",
|
||||
role: "damage",
|
||||
}, 80, index, seed);
|
||||
return health === undefined ? member : { ...member, health };
|
||||
}
|
||||
|
||||
function configurePlayerAndParty(seed: string, includeCompanionCaster = false) {
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-scout",
|
||||
secondaryClassId: "rom-priest",
|
||||
raceId: "rom-human",
|
||||
level: 80,
|
||||
role: "damage",
|
||||
});
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty(
|
||||
seed, 80, useCombatStore.getState().health, "damage", 5, "rom",
|
||||
);
|
||||
|
||||
const partyCaster = includeCompanionCaster
|
||||
? createConfiguredPartyMember({
|
||||
name: "Scout/Priest companion",
|
||||
classId: "rom-scout",
|
||||
secondaryClassId: "rom-priest",
|
||||
specialization: "Primary",
|
||||
role: "damage",
|
||||
}, 80, 0, seed)
|
||||
: null;
|
||||
const roster = [
|
||||
...(partyCaster ? [partyCaster] : []),
|
||||
makePriest("Inclusive-radius edge", seed, 1),
|
||||
makePriest("Outside-radius ally", seed, 2),
|
||||
makePriest("Fallen ally", seed, 3, 0),
|
||||
makePriest("Blocked ally", seed, 4),
|
||||
makePriest("Ally without position", seed, 5),
|
||||
];
|
||||
usePartyStore.setState({ members: roster });
|
||||
for (const member of roster) {
|
||||
if (member.name === "Inclusive-radius edge") registerPartyRuntimePosition(member.id, [9, 12, 0]);
|
||||
if (member.name === "Outside-radius ally") registerPartyRuntimePosition(member.id, [15.01, 0, 0]);
|
||||
if (member.name === "Fallen ally") registerPartyRuntimePosition(member.id, [1, 0, 0]);
|
||||
if (member.name === "Blocked ally") registerPartyRuntimePosition(member.id, [3, 0, 0]);
|
||||
if (member.name === "Scout/Priest companion") registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
}
|
||||
registerCombatSpatialProvider({
|
||||
actor: () => null,
|
||||
hasLineOfSight: (_source, target) => target[0] !== 3,
|
||||
projectGroundPoint: (position) => position,
|
||||
});
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
for (const id of [DIRECT_TEST_TARGET, DOT_TEST_TARGET, EXPIRY_TEST_TARGET]) {
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: id,
|
||||
level: 1,
|
||||
maxHealth: 1_000_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(id, [2, 0, 0]);
|
||||
}
|
||||
return {
|
||||
partyCaster,
|
||||
edge: roster.find((member) => member.name === "Inclusive-radius edge")!,
|
||||
outside: roster.find((member) => member.name === "Outside-radius ally")!,
|
||||
fallen: roster.find((member) => member.name === "Fallen ally")!,
|
||||
blocked: roster.find((member) => member.name === "Blocked ally")!,
|
||||
unpositioned: roster.find((member) => member.name === "Ally without position")!,
|
||||
};
|
||||
}
|
||||
|
||||
function applyScreenAttackFromCompanion(casterId: string, now: number): void {
|
||||
const member = usePartyStore.getState().members.find((candidate) => candidate.id === casterId);
|
||||
if (!member || !SCREEN_ATTACK) throw new Error("Missing Scout/Priest companion or Screen Attack");
|
||||
usePartyStore.setState((state) => ({
|
||||
members: state.members.map((candidate) => candidate.id === casterId
|
||||
? {
|
||||
...candidate,
|
||||
activeCast: {
|
||||
abilityId: SCREEN_ATTACK_ID,
|
||||
school: "physical",
|
||||
mode: "cast",
|
||||
startedAt: now,
|
||||
completesAt: now,
|
||||
originalDurationMs: 0,
|
||||
pushbackHits: 0,
|
||||
origin: [0, 0, 0],
|
||||
targetId: casterId,
|
||||
},
|
||||
activeCastTargetId: casterId,
|
||||
status: "attacking",
|
||||
}
|
||||
: candidate),
|
||||
}));
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
const syncedMember = usePartyStore.getState().members.find((candidate) => candidate.id === casterId);
|
||||
const committed = syncedMember ? commitPartyAbilityUse(syncedMember, SCREEN_ATTACK, now) : null;
|
||||
if (!committed) throw new Error("Companion could not commit Screen Attack before casting");
|
||||
usePartyStore.setState((state) => ({
|
||||
members: state.members.map((candidate) => candidate.id === casterId
|
||||
? {
|
||||
...committed,
|
||||
activeCast: candidate.activeCast,
|
||||
activeCastTargetId: candidate.activeCastTargetId,
|
||||
}
|
||||
: candidate),
|
||||
}));
|
||||
useCombatStore.getState().commitCompanionActors([committed]);
|
||||
advanceDungeonPartyCombat(now);
|
||||
}
|
||||
|
||||
function directDamage(sourceActorId: string, targetId: string, now: number): number {
|
||||
return useCombatStore.getState().damageMob(
|
||||
targetId,
|
||||
100,
|
||||
now,
|
||||
{ actorId: sourceActorId, role: "damage", abilityId: DIRECT_TEST_ABILITY_ID },
|
||||
"physical",
|
||||
{
|
||||
attackKind: "spell",
|
||||
seed: "rom-screen-attack-fixed-direct-hit",
|
||||
rules: { canMiss: false, canCrit: false, canDodge: false, canParry: false, canBlock: false },
|
||||
},
|
||||
).amount;
|
||||
}
|
||||
|
||||
function applyAndResolvePeriodicDamage(sourceActorId: string, targetId: string, now: number): number {
|
||||
if (!SCOUT_PERIODIC_ABILITY) throw new Error("Missing Scout Piercing Arrow test ability");
|
||||
const testAbility = { ...SCOUT_PERIODIC_ABILITY, id: PERIODIC_TEST_ABILITY_ID };
|
||||
const effect = {
|
||||
kind: "dot",
|
||||
coefficient: 0,
|
||||
basePoints: 100,
|
||||
school: "physical",
|
||||
ticks: 1,
|
||||
intervalMs: 1_000,
|
||||
tag: "screen-attack-integration-dot",
|
||||
} as const;
|
||||
if (!useCombatStore.getState().applyTimedActorEffect(
|
||||
sourceActorId, targetId, testAbility, effect, now,
|
||||
)) throw new Error("Could not apply the periodic damage test effect");
|
||||
const pending = useCombatStore.getState().effects.find((candidate) => candidate.kind === "dot"
|
||||
&& candidate.sourceAbilityId === PERIODIC_TEST_ABILITY_ID && candidate.targetId === targetId);
|
||||
if (!pending || pending.kind !== "dot") throw new Error("Periodic damage test effect was not scheduled");
|
||||
const tickAt = pending.nextTickAt;
|
||||
const seconds = Math.max(0, (tickAt - now) / 1_000);
|
||||
useCombatStore.getState().tick(seconds, tickAt);
|
||||
const matchingEvents = useCombatStore.getState().combatEvents.filter((candidate) => candidate.kind === "damage"
|
||||
&& candidate.sourceActorId === sourceActorId
|
||||
&& candidate.targetActorId === targetId
|
||||
&& candidate.abilityId === PERIODIC_TEST_ABILITY_ID);
|
||||
const event = matchingEvents[matchingEvents.length - 1];
|
||||
return event?.kind === "damage" ? event.effectiveAmount : 0;
|
||||
}
|
||||
|
||||
function screenAura(targetId: string) {
|
||||
return useCombatStore.getState().auras.find((aura) => aura.targetId === targetId
|
||||
&& aura.definition.id === ROM_SCOUT_PRIEST_SCREEN_ATTACK_AURA.id);
|
||||
}
|
||||
|
||||
describe("CoA Scout/Priest Screen Attack runtime integration", () => {
|
||||
beforeEach(() => {
|
||||
resetPartyRuntimeClock();
|
||||
resetCombatSpatialProvider();
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
useDungeonSimulationStore.setState({ sessionMode: "play" });
|
||||
});
|
||||
|
||||
afterEach(() => resetCombatSpatialProvider());
|
||||
|
||||
it("activates for the player, selects only living nearby visible party actors, scales direct and periodic damage, expires, and observes cooldown", () => {
|
||||
if (!SCREEN_ATTACK) throw new Error("Screen Attack (499555) is missing from the generated catalog");
|
||||
if (!SCOUT_PERIODIC_ABILITY) throw new Error("Scout periodic fixture ability is missing");
|
||||
const recipients = configurePlayerAndParty("screen-attack-player-runtime");
|
||||
|
||||
const baselineDirect = directDamage(PLAYER_AGGRO_ID, DIRECT_TEST_TARGET, 1_000);
|
||||
const baselinePeriodic = applyAndResolvePeriodicDamage(PLAYER_AGGRO_ID, DOT_TEST_TARGET, 1_000);
|
||||
expect(baselineDirect).toBeGreaterThan(0);
|
||||
expect(baselinePeriodic).toBeGreaterThan(0);
|
||||
|
||||
expect(useCombatStore.getState().castAbility(SCREEN_ATTACK.id, undefined, 3_000)).toMatchObject({ ok: true });
|
||||
expect(screenAura(PLAYER_AGGRO_ID)?.expiresAt).toBe(23_000);
|
||||
expect(screenAura(recipients.edge.id)?.expiresAt).toBe(23_000);
|
||||
expect(screenAura(recipients.outside.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.fallen.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.blocked.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.unpositioned.id)).toBeUndefined();
|
||||
|
||||
const buffedDirect = directDamage(PLAYER_AGGRO_ID, DIRECT_TEST_TARGET, 4_000);
|
||||
const buffedPeriodic = applyAndResolvePeriodicDamage(PLAYER_AGGRO_ID, DOT_TEST_TARGET, 4_000);
|
||||
expect(buffedDirect / baselineDirect).toBeCloseTo(1 + 0.174, 1);
|
||||
expect(buffedPeriodic / baselinePeriodic).toBeCloseTo(1 + 0.174, 1);
|
||||
|
||||
const beforeExpiry = directDamage(PLAYER_AGGRO_ID, EXPIRY_TEST_TARGET, 22_999);
|
||||
const atExpiry = directDamage(PLAYER_AGGRO_ID, EXPIRY_TEST_TARGET, 23_000);
|
||||
expect(beforeExpiry / baselineDirect).toBeCloseTo(1 + 0.174, 1);
|
||||
expect(atExpiry).toBe(baselineDirect);
|
||||
|
||||
expect(useCombatStore.getState().cooldowns[SCREEN_ATTACK.id]).toBe(63_000);
|
||||
expect(useCombatStore.getState().castAbility(SCREEN_ATTACK.id, undefined, 62_999)).toMatchObject({
|
||||
ok: false,
|
||||
reason: "cooldown",
|
||||
});
|
||||
expect(useCombatStore.getState().castAbility(SCREEN_ATTACK.id, undefined, 63_000)).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("activates for a generated Scout/Priest companion and applies its own direct and periodic damage bonuses to eligible recipients", () => {
|
||||
if (!SCOUT_PERIODIC_ABILITY) throw new Error("Scout periodic fixture ability is missing");
|
||||
const recipients = configurePlayerAndParty("screen-attack-companion-runtime", true);
|
||||
const casterId = recipients.partyCaster?.id;
|
||||
if (!casterId) throw new Error("Missing generated Scout/Priest caster");
|
||||
|
||||
const baselineDirect = directDamage(casterId, DIRECT_TEST_TARGET, 1_000);
|
||||
const baselinePeriodic = applyAndResolvePeriodicDamage(casterId, DOT_TEST_TARGET, 1_000);
|
||||
expect(baselineDirect).toBeGreaterThan(0);
|
||||
expect(baselinePeriodic).toBeGreaterThan(0);
|
||||
|
||||
applyScreenAttackFromCompanion(casterId, 3_000);
|
||||
expect(screenAura(casterId)?.expiresAt).toBe(23_000);
|
||||
expect(screenAura(PLAYER_AGGRO_ID)?.expiresAt).toBe(23_000);
|
||||
expect(screenAura(recipients.edge.id)?.expiresAt).toBe(23_000);
|
||||
expect(screenAura(recipients.outside.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.fallen.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.blocked.id)).toBeUndefined();
|
||||
expect(screenAura(recipients.unpositioned.id)).toBeUndefined();
|
||||
|
||||
const buffedDirect = directDamage(casterId, DIRECT_TEST_TARGET, 4_000);
|
||||
const buffedPeriodic = applyAndResolvePeriodicDamage(casterId, DOT_TEST_TARGET, 4_000);
|
||||
expect(buffedDirect / baselineDirect).toBeCloseTo(1 + 0.174, 1);
|
||||
expect(buffedPeriodic / baselinePeriodic).toBeCloseTo(1 + 0.174, 1);
|
||||
|
||||
const beforeExpiry = directDamage(casterId, EXPIRY_TEST_TARGET, 22_999);
|
||||
const atExpiry = directDamage(casterId, EXPIRY_TEST_TARGET, 23_000);
|
||||
expect(beforeExpiry / baselineDirect).toBeCloseTo(1 + 0.174, 1);
|
||||
expect(atExpiry).toBe(baselineDirect);
|
||||
const committedCaster = usePartyStore.getState().members.find((member) => member.id === casterId);
|
||||
expect(committedCaster?.cooldowns[SCREEN_ATTACK_ID]).toBe(63_000);
|
||||
if (!committedCaster || !SCREEN_ATTACK) throw new Error("Screen Attack companion cooldown was not committed");
|
||||
expect(partyAbilityIsReady(committedCaster, SCREEN_ATTACK, 62_999)).toBe(false);
|
||||
expect(commitPartyAbilityUse(committedCaster, SCREEN_ATTACK, 62_999)).toBeNull();
|
||||
expect(partyAbilityIsReady(committedCaster, SCREEN_ATTACK, 63_000)).toBe(true);
|
||||
expect(commitPartyAbilityUse(committedCaster, SCREEN_ATTACK, 63_000)?.cooldowns[SCREEN_ATTACK_ID])
|
||||
.toBe(123_000);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user