forked from phenom/healer-man
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,282 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||
import { applyAura } from "./combatAuras";
|
||||
import { abilitiesForClass } from "./abilityCatalog";
|
||||
import { useCombatStore, PLAYER_AURA_ENTITY_ID } from "./combatStore";
|
||||
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||
import { advanceDungeonPartyCombat, partyAbilityTargetIsValid, partyAttackDamage, resetPartyRuntimeClock } from "./partyRuntime";
|
||||
import { clearPartyRuntimeRegistry, registerPartyRuntimePosition, updatePartyRuntimeTargetVisibility } from "./partyRuntimeRegistry";
|
||||
import { createConfiguredPartyMember, usePartyStore } from "./partyStore";
|
||||
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||
import { enemyWeaponDebuffAuraDefinition, enemyWeaponDebuffContract } from "./enemyWeaponDebuffs";
|
||||
import { ROM_FIVE_IDENTITIES } from "./romFiveIdentities";
|
||||
|
||||
const START_AT = 1_000;
|
||||
|
||||
function weaponItem(
|
||||
instanceId: string,
|
||||
weaponType: "dagger" | "sword",
|
||||
damage: number,
|
||||
speedMs: number,
|
||||
) {
|
||||
return {
|
||||
instanceId,
|
||||
itemId: 1,
|
||||
itemKey: instanceId,
|
||||
name: `Source fixture ${weaponType}`,
|
||||
quality: "uncommon" as const,
|
||||
inventorySlot: "one-hand",
|
||||
armorType: null,
|
||||
weaponType,
|
||||
itemLevel: 1,
|
||||
sourceItemLevel: 1,
|
||||
stats: {
|
||||
...EMPTY_ITEM_STATS,
|
||||
meleeWeapon: { damageMin: damage, damageMax: damage, damageSchool: "physical" as const, speedMs },
|
||||
},
|
||||
quantity: 1,
|
||||
sourceBossKey: "source-disarm-fixture",
|
||||
sourceBossName: "Source Disarm Fixture",
|
||||
acquiredAt: START_AT,
|
||||
};
|
||||
}
|
||||
|
||||
function rangedItem(instanceId: string) {
|
||||
return {
|
||||
...weaponItem(instanceId, "sword", 500, 2_500),
|
||||
inventorySlot: "ranged",
|
||||
weaponType: "bow",
|
||||
stats: {
|
||||
...EMPTY_ITEM_STATS,
|
||||
rangedWeapon: { damageMin: 400, damageMax: 400, damageSchool: "physical" as const, speedMs: 2_500 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applySourceDisarm(targetId: string, now = START_AT): void {
|
||||
const definition = enemyWeaponDebuffAuraDefinition(enemyWeaponDebuffContract(6713, "ascension")!);
|
||||
useCombatStore.setState((state) => ({
|
||||
auras: [...applyAura(state.auras, { definition, sourceId: "source-disarm-caster", targetId, now }).auras],
|
||||
}));
|
||||
}
|
||||
|
||||
function registerTarget(id: string): void {
|
||||
useCombatStore.getState().registerMob(id, {
|
||||
name: "Source Disarm runtime target",
|
||||
level: 1,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(id, [2, 0, 0]);
|
||||
useCombatStore.getState().selectMob(id);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearMobRuntimeRegistry();
|
||||
clearPartyRuntimeRegistry();
|
||||
resetPartyRuntimeClock();
|
||||
useCombatStore.getState().initializeCharacter({ classId: "warrior", level: 30 });
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
usePartyStore.getState().initializeParty("enemy-disarm-runtime", 30, useCombatStore.getState().health, "damage", 1, "wow");
|
||||
});
|
||||
|
||||
describe("source Disarm character execution", () => {
|
||||
it("blocks an equipped-weapon cast and keeps the player's main-hand auto as a 1–2 / 2s punch", () => {
|
||||
const dagger = weaponItem("player-disarm-dagger", "dagger", 500, 4_000);
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rogue",
|
||||
level: 80,
|
||||
inventory: [dagger],
|
||||
equipment: { player: { "main-hand": dagger.instanceId } },
|
||||
});
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
const targetId = "player-disarm-runtime-target";
|
||||
registerTarget(targetId);
|
||||
applySourceDisarm(PLAYER_AURA_ENTITY_ID);
|
||||
|
||||
const sinisterStrike = abilitiesForClass("rogue").find((ability) => ability.name === "Sinister Strike")!;
|
||||
expect(sinisterStrike.wowWeaponRequirements?.equippedItemClass).toBe(2);
|
||||
expect(useCombatStore.getState().castAbility(sinisterStrike.id, [0, 0, 0], START_AT)).toMatchObject({
|
||||
ok: false,
|
||||
reason: "condition-not-met",
|
||||
});
|
||||
|
||||
useCombatStore.setState((state) => ({
|
||||
autoAttackTargetId: targetId,
|
||||
nextAutoAttackAt: START_AT,
|
||||
nextOffhandAutoAttackAt: 0,
|
||||
auras: [...state.auras],
|
||||
}));
|
||||
useCombatStore.getState().tick(0.05, START_AT);
|
||||
|
||||
const state = useCombatStore.getState();
|
||||
expect(state.nextAutoAttackAt).toBe(START_AT + 2_000);
|
||||
const auto = state.combatEvents.find((event) => event.kind === "damage"
|
||||
&& event.sourceActorId === PLAYER_AGGRO_ID
|
||||
&& event.targetActorId === targetId);
|
||||
expect(auto).toBeDefined();
|
||||
expect(auto?.rawAmount).toBeLessThan(500);
|
||||
expect(auto?.attackHand).toBe("mainhand");
|
||||
});
|
||||
|
||||
it("gates the companion's source weapon cast while retaining the ordinary unarmed party auto", () => {
|
||||
const targetId = "party-disarm-runtime-target";
|
||||
registerTarget(targetId);
|
||||
const configured = createConfiguredPartyMember({
|
||||
name: "Source fixture Protection Warrior",
|
||||
classId: "warrior",
|
||||
raceId: "human",
|
||||
specialization: "Protection",
|
||||
specializationId: "warrior:protection",
|
||||
talentTreeId: "warrior-protection",
|
||||
role: "tank",
|
||||
}, 30, 0, "party-source-disarm-runtime");
|
||||
const sword = weaponItem("party-disarm-sword", "sword", 500, 4_000);
|
||||
const member = {
|
||||
...configured,
|
||||
gearStats: sword.stats,
|
||||
nextActionAt: 10_000,
|
||||
};
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: targetId });
|
||||
useCombatStore.setState((state) => ({
|
||||
inventory: [...state.inventory, sword],
|
||||
equipment: { ...state.equipment, [member.id]: { "main-hand": sword.instanceId } },
|
||||
}));
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(member.id, targetId, true);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
applySourceDisarm(member.id);
|
||||
|
||||
const heroicStrike = abilitiesForClass("warrior").find((ability) => ability.name === "Heroic Strike")!;
|
||||
expect(heroicStrike.wowWeaponRequirements?.equippedItemClass).toBe(2);
|
||||
expect(partyAbilityTargetIsValid(member, heroicStrike, targetId, [0, 0, 0], START_AT)).toBe(false);
|
||||
const permittedPhysicalMeleeAbility = abilitiesForClass("warrior")
|
||||
.find((ability) => ability.name === "Devastate")!;
|
||||
expect(permittedPhysicalMeleeAbility.wowSpellDefense?.damageClass).toBe(2);
|
||||
expect(permittedPhysicalMeleeAbility.wowWeaponRequirements).toBeUndefined();
|
||||
expect(partyAbilityTargetIsValid(
|
||||
member, permittedPhysicalMeleeAbility!, targetId, [0, 0, 0], START_AT,
|
||||
)).toBe(true);
|
||||
|
||||
const expectedUnarmedAmount = partyAttackDamage(member, START_AT, {
|
||||
damageMin: 1,
|
||||
damageMax: 2,
|
||||
damageSchool: "physical",
|
||||
speedMs: 2_000,
|
||||
});
|
||||
advanceDungeonPartyCombat(START_AT);
|
||||
const auto = useCombatStore.getState().combatEvents.find((event) => event.kind === "damage"
|
||||
&& event.sourceActorId === member.id
|
||||
&& event.abilityId === "party-auto-attack"
|
||||
&& event.targetActorId === targetId);
|
||||
expect(auto).toBeDefined();
|
||||
expect(auto?.rawAmount).toBe(expectedUnarmedAmount);
|
||||
expect(auto?.attackHand).toBe("mainhand");
|
||||
});
|
||||
|
||||
it("uses only the unarmed main-hand contribution for a disarmed dual-wielding party shaman", () => {
|
||||
const targetId = "party-disarm-shaman-target";
|
||||
registerTarget(targetId);
|
||||
const configured = createConfiguredPartyMember({
|
||||
name: "Source fixture Enhancement Shaman",
|
||||
classId: "shaman",
|
||||
raceId: "orc",
|
||||
specialization: "Enhancement",
|
||||
specializationId: "shaman:enhancement",
|
||||
talentTreeId: "shaman-enhancement",
|
||||
role: "damage",
|
||||
}, 80, 0, "party-source-disarm-shaman");
|
||||
const mainHand = weaponItem("party-disarm-shaman-main", "sword", 500, 4_000);
|
||||
const offHand = { ...weaponItem("party-disarm-shaman-off", "sword", 500, 4_000), inventorySlot: "one-hand" };
|
||||
const member = { ...configured, gearStats: mainHand.stats, nextActionAt: START_AT + 100_000 };
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: targetId });
|
||||
useCombatStore.setState((state) => ({
|
||||
inventory: [...state.inventory, mainHand, offHand],
|
||||
equipment: {
|
||||
...state.equipment,
|
||||
[member.id]: { "main-hand": mainHand.instanceId, "off-hand": offHand.instanceId },
|
||||
},
|
||||
}));
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(member.id, targetId, true);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
applySourceDisarm(member.id);
|
||||
|
||||
advanceDungeonPartyCombat(START_AT);
|
||||
const events = useCombatStore.getState().combatEvents.filter((event) => (
|
||||
event.kind === "damage" && event.sourceActorId === member.id && event.targetActorId === targetId
|
||||
));
|
||||
const mainHandHit = events.find((event) => event.abilityId === "party-auto-attack");
|
||||
const offHandHit = events.find((event) => event.abilityId === "party-auto-attack-offhand");
|
||||
expect(mainHandHit).toBeDefined();
|
||||
expect(offHandHit).toBeDefined();
|
||||
expect(mainHandHit!.rawAmount).toBeLessThan(200);
|
||||
expect(offHandHit!.rawAmount).toBeGreaterThan(mainHandHit!.rawAmount + 10);
|
||||
expect(mainHandHit!.attackHand).toBe("mainhand");
|
||||
expect(offHandHit!.attackHand).toBe("offhand");
|
||||
});
|
||||
|
||||
it("uses the verified 3-second unarmed profile for a native party main-hand auto", () => {
|
||||
const targetId = "party-disarm-native-target";
|
||||
registerTarget(targetId);
|
||||
const identity = ROM_FIVE_IDENTITIES.find((candidate) => candidate.classId === "rom-knight")!;
|
||||
const configured = createConfiguredPartyMember({
|
||||
...identity,
|
||||
statRuleset: "rom",
|
||||
specialization: "Primary",
|
||||
talentTreeId: "",
|
||||
}, 30, 0, "party-native-disarm-runtime");
|
||||
const member = { ...configured, nextActionAt: START_AT + 100_000 };
|
||||
usePartyStore.setState({ members: [member], command: "attack", activeMobId: targetId });
|
||||
registerPartyRuntimePosition(member.id, [0, 0, 0]);
|
||||
updatePartyRuntimeTargetVisibility(member.id, targetId, true);
|
||||
useCombatStore.getState().synchronizeActors();
|
||||
applySourceDisarm(member.id);
|
||||
|
||||
advanceDungeonPartyCombat(START_AT);
|
||||
const autoEvents = () => useCombatStore.getState().combatEvents.filter((event) => (
|
||||
event.kind === "damage" && event.sourceActorId === member.id
|
||||
&& event.abilityId === "party-auto-attack" && event.targetActorId === targetId
|
||||
));
|
||||
expect(autoEvents()).toHaveLength(1);
|
||||
expect(autoEvents()[0].rawAmount).toBe(partyAttackDamage(member, START_AT, undefined, true));
|
||||
expect(autoEvents()[0].attackHand).toBe("mainhand");
|
||||
|
||||
advanceDungeonPartyCombat(START_AT + 2_999);
|
||||
expect(autoEvents()).toHaveLength(1);
|
||||
advanceDungeonPartyCombat(START_AT + 3_000);
|
||||
expect(autoEvents()).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("rechecks the Ascension ranged channel for a running Scout Auto Shot and resumes after expiry", () => {
|
||||
const bow = rangedItem("scout-disarm-bow");
|
||||
useCombatStore.getState().initializeCharacter({
|
||||
classId: "rom-scout",
|
||||
secondaryClassId: "rom-rogue",
|
||||
level: 80,
|
||||
statRuleset: "rom",
|
||||
inventory: [bow],
|
||||
equipment: { player: { ranged: bow.instanceId } },
|
||||
});
|
||||
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||
const targetId = "scout-disarm-ranged-target";
|
||||
useCombatStore.getState().registerMob(targetId, {
|
||||
name: "Scout ranged Disarm target",
|
||||
level: 1,
|
||||
maxHealth: 100_000,
|
||||
xpReward: 0,
|
||||
});
|
||||
setMobPosition(targetId, [4, 0, 0]);
|
||||
applySourceDisarm(PLAYER_AURA_ENTITY_ID);
|
||||
useCombatStore.setState({ autoShotTargetId: targetId, nextAutoShotAt: START_AT });
|
||||
|
||||
useCombatStore.getState().tick(0.05, START_AT);
|
||||
expect(useCombatStore.getState().mobs[targetId]?.health).toBe(100_000);
|
||||
expect(useCombatStore.getState().autoShotTargetId).toBe(targetId);
|
||||
|
||||
useCombatStore.getState().tick(0.05, START_AT + 5_000);
|
||||
expect(useCombatStore.getState().mobs[targetId]?.health).toBeLessThan(100_000);
|
||||
expect(useCombatStore.getState().combatEvents.find((event) => event.sourceActorId === PLAYER_AGGRO_ID
|
||||
&& event.targetActorId === targetId && event.kind === "damage")?.attackHand).toBe("ranged");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user