fix: label player auto attack damage events (#15)
This commit is contained in:
1 parent
3687c423e2
commit
89b6f4f79d
2 files changed
+195
-1
No files matched your search
+14
-1
@@ -7682,6 +7682,7 @@ function damageMobInWork(
|
|||||||
primarySaintsBlessingMultiplierAlreadyApplied = false,
|
primarySaintsBlessingMultiplierAlreadyApplied = false,
|
||||||
directPhysicalArmorAttackerLevel?: number,
|
directPhysicalArmorAttackerLevel?: number,
|
||||||
attackHand?: import("./combatEvents").StructuredCombatEvent["attackHand"],
|
attackHand?: import("./combatEvents").StructuredCombatEvent["attackHand"],
|
||||||
|
reportedAbilityId?: string,
|
||||||
): DamageResult {
|
): DamageResult {
|
||||||
const mob = work.mobs[id];
|
const mob = work.mobs[id];
|
||||||
if (!mob || mob.dead || amount <= 0 || (mob.navigationEvadeUntil ?? 0) > now) {
|
if (!mob || mob.dead || amount <= 0 || (mob.navigationEvadeUntil ?? 0) > now) {
|
||||||
@@ -7771,12 +7772,14 @@ function damageMobInWork(
|
|||||||
const primaryApplied = secondaryDamage
|
const primaryApplied = secondaryDamage
|
||||||
? Math.min(applied, Math.max(0, Math.round(primaryRemainingDamage)))
|
? Math.min(applied, Math.max(0, Math.round(primaryRemainingDamage)))
|
||||||
: applied;
|
: applied;
|
||||||
|
// This override labels the primary event only; the ThreatSource stays authoritative for mechanics.
|
||||||
|
const combatEventAbilityId = reportedAbilityId ?? source.abilityId;
|
||||||
recordCombatEventInWork(work, {
|
recordCombatEventInWork(work, {
|
||||||
occurredAt: now,
|
occurredAt: now,
|
||||||
kind: "damage",
|
kind: "damage",
|
||||||
sourceActorId: source.actorId,
|
sourceActorId: source.actorId,
|
||||||
targetActorId: id,
|
targetActorId: id,
|
||||||
...(source.abilityId ? { abilityId: source.abilityId } : {}),
|
...(combatEventAbilityId ? { abilityId: combatEventAbilityId } : {}),
|
||||||
school,
|
school,
|
||||||
...(attackHand ? { attackHand } : {}),
|
...(attackHand ? { attackHand } : {}),
|
||||||
rawAmount: combatResult?.rawDamage !== undefined
|
rawAmount: combatResult?.rawDamage !== undefined
|
||||||
@@ -9465,6 +9468,12 @@ function maybeMirrorDeathKnightDancingRuneWeaponMeleeInWork(
|
|||||||
source,
|
source,
|
||||||
school,
|
school,
|
||||||
work.gearStats.spellPenetration,
|
work.gearStats.spellPenetration,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_ABILITY_ID,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9578,6 +9587,7 @@ function resolveAndDamageMobInWork(
|
|||||||
criticalChanceBonusPercent = 0,
|
criticalChanceBonusPercent = 0,
|
||||||
weaponSpeedMs = work.gearStats.meleeWeapon?.speedMs ?? WOW335_SHAMAN_MAELSTROM_WEAPON_DEFAULT_SPEED_MS,
|
weaponSpeedMs = work.gearStats.meleeWeapon?.speedMs ?? WOW335_SHAMAN_MAELSTROM_WEAPON_DEFAULT_SPEED_MS,
|
||||||
selectedDbcSpellId?: number,
|
selectedDbcSpellId?: number,
|
||||||
|
reportedAbilityId?: string,
|
||||||
): DamageResult {
|
): DamageResult {
|
||||||
const mob = work.mobs[id];
|
const mob = work.mobs[id];
|
||||||
if (!mob || mob.dead || amount <= 0 || (mob.navigationEvadeUntil ?? 0) > now) {
|
if (!mob || mob.dead || amount <= 0 || (mob.navigationEvadeUntil ?? 0) > now) {
|
||||||
@@ -9945,6 +9955,7 @@ function resolveAndDamageMobInWork(
|
|||||||
: sourceWeaponChannel === "off-hand" ? "offhand"
|
: sourceWeaponChannel === "off-hand" ? "offhand"
|
||||||
: sourceWeaponChannel === "ranged" ? "ranged" : undefined
|
: sourceWeaponChannel === "ranged" ? "ranged" : undefined
|
||||||
: undefined,
|
: undefined,
|
||||||
|
reportedAbilityId,
|
||||||
);
|
);
|
||||||
maybeProcShamanFlurryInWork(
|
maybeProcShamanFlurryInWork(
|
||||||
work,
|
work,
|
||||||
@@ -24460,6 +24471,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
|||||||
{ isAutoAttack: true, isDualWielding },
|
{ isAutoAttack: true, isDualWielding },
|
||||||
0,
|
0,
|
||||||
mainHandSpeedMs,
|
mainHandSpeedMs,
|
||||||
|
undefined,
|
||||||
|
"player-auto-attack",
|
||||||
);
|
);
|
||||||
const runeWeaponMainHand = state.classId === "death-knight"
|
const runeWeaponMainHand = state.classId === "death-knight"
|
||||||
&& result.combatResult?.landed
|
&& result.combatResult?.landed
|
||||||
|
|||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { PLAYER_AGGRO_ID } from "./aggro";
|
||||||
|
import { applyAura as applyCombatAura } from "./combatAuras";
|
||||||
|
import { useCombatStore } from "./combatStore";
|
||||||
|
import { subscribeCombatPresentation, type CombatPresentationEvent } from "./combatPresentation";
|
||||||
|
import { subscribeCombatEvents, type StructuredCombatEvent } from "./combatEvents";
|
||||||
|
import { PLAYER_EQUIPMENT_OWNER_ID } from "./equipment";
|
||||||
|
import { EMPTY_ITEM_STATS } from "./itemStats";
|
||||||
|
import { clearMobRuntimeRegistry, setMobPosition } from "./mobRuntimeRegistry";
|
||||||
|
import {
|
||||||
|
WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_ABILITY_ID,
|
||||||
|
WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_AURA,
|
||||||
|
} from "./wow335DeathKnightDancingRuneWeaponRuntime";
|
||||||
|
|
||||||
|
const TARGET_ID = "player-auto-event-lineage-target";
|
||||||
|
|
||||||
|
let presentationEvents: CombatPresentationEvent[] = [];
|
||||||
|
let publishedCombatEvents: StructuredCombatEvent[] = [];
|
||||||
|
let unsubscribePresentation = () => {};
|
||||||
|
let unsubscribeCombatEvents = () => {};
|
||||||
|
|
||||||
|
function weapon(instanceId: string, inventorySlot: "one-hand" | "off-hand") {
|
||||||
|
return {
|
||||||
|
instanceId,
|
||||||
|
itemId: 1,
|
||||||
|
itemKey: instanceId,
|
||||||
|
name: instanceId,
|
||||||
|
quality: "uncommon" as const,
|
||||||
|
inventorySlot,
|
||||||
|
armorType: null,
|
||||||
|
weaponType: "dagger",
|
||||||
|
itemLevel: 1,
|
||||||
|
sourceItemLevel: 1,
|
||||||
|
stats: {
|
||||||
|
...EMPTY_ITEM_STATS,
|
||||||
|
hitRating: 10_000,
|
||||||
|
meleeWeapon: { damageMin: 100, damageMax: 100, damageSchool: "physical" as const, speedMs: 2_000 },
|
||||||
|
},
|
||||||
|
quantity: 1,
|
||||||
|
sourceBossKey: "test",
|
||||||
|
sourceBossName: "Test",
|
||||||
|
acquiredAt: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clearMobRuntimeRegistry();
|
||||||
|
useCombatStore.getState().initializeCharacter({ classId: "rogue", level: 80 });
|
||||||
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||||
|
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||||
|
name: "Player auto event lineage target",
|
||||||
|
level: 80,
|
||||||
|
maxHealth: 1_000_000,
|
||||||
|
xpReward: 0,
|
||||||
|
});
|
||||||
|
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||||
|
useCombatStore.getState().selectMob(TARGET_ID);
|
||||||
|
presentationEvents = [];
|
||||||
|
publishedCombatEvents = [];
|
||||||
|
unsubscribePresentation = subscribeCombatPresentation((message) => {
|
||||||
|
if (message.kind === "event") presentationEvents.push(message.event);
|
||||||
|
});
|
||||||
|
unsubscribeCombatEvents = subscribeCombatEvents((event) => publishedCombatEvents.push(event));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unsubscribePresentation();
|
||||||
|
unsubscribeCombatEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("player auto-attack event lineage", () => {
|
||||||
|
it("joins both hand events to their releases without relabeling the triggering melee ability", () => {
|
||||||
|
const mainHand = weapon("event-lineage-main-hand", "one-hand");
|
||||||
|
const offHand = weapon("event-lineage-off-hand", "off-hand");
|
||||||
|
useCombatStore.getState().initializeCharacter({
|
||||||
|
classId: "rogue",
|
||||||
|
level: 80,
|
||||||
|
inventory: [mainHand, offHand],
|
||||||
|
equipment: { [PLAYER_EQUIPMENT_OWNER_ID]: {
|
||||||
|
"main-hand": mainHand.instanceId,
|
||||||
|
"off-hand": offHand.instanceId,
|
||||||
|
} },
|
||||||
|
});
|
||||||
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||||
|
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||||
|
name: "Player auto event lineage target",
|
||||||
|
level: 80,
|
||||||
|
maxHealth: 1_000_000,
|
||||||
|
xpReward: 0,
|
||||||
|
});
|
||||||
|
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||||
|
useCombatStore.getState().selectMob(TARGET_ID);
|
||||||
|
const triggeringAbility = useCombatStore.getState().abilities.find((ability) => ability.name === "Sinister Strike");
|
||||||
|
expect(triggeringAbility).toBeDefined();
|
||||||
|
expect(useCombatStore.getState().castAbility(triggeringAbility!.id, undefined, 1_000).ok).toBe(true);
|
||||||
|
const healthBeforeSwings = useCombatStore.getState().mobs[TARGET_ID]!.health;
|
||||||
|
|
||||||
|
useCombatStore.getState().tick(0, 1_000);
|
||||||
|
|
||||||
|
const swingEvents = useCombatStore.getState().combatEvents.filter((event) => (
|
||||||
|
event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID
|
||||||
|
&& event.targetActorId === TARGET_ID && event.occurredAt === 1_000
|
||||||
|
&& (event.attackHand === "mainhand" || event.attackHand === "offhand")
|
||||||
|
));
|
||||||
|
expect(swingEvents).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ abilityId: "player-auto-attack", attackHand: "mainhand" }),
|
||||||
|
expect.objectContaining({ abilityId: "player-auto-attack-offhand", attackHand: "offhand" }),
|
||||||
|
]));
|
||||||
|
expect(swingEvents).toHaveLength(2);
|
||||||
|
expect(swingEvents.reduce((sum, event) => sum + event.effectiveAmount, 0))
|
||||||
|
.toBe(healthBeforeSwings - useCombatStore.getState().mobs[TARGET_ID]!.health);
|
||||||
|
expect(useCombatStore.getState().combatEvents).toContainEqual(expect.objectContaining({
|
||||||
|
kind: "damage",
|
||||||
|
sourceActorId: PLAYER_AGGRO_ID,
|
||||||
|
targetActorId: TARGET_ID,
|
||||||
|
abilityId: triggeringAbility!.id,
|
||||||
|
}));
|
||||||
|
expect(presentationEvents).toContainEqual(expect.objectContaining({
|
||||||
|
occurredAt: 1_000,
|
||||||
|
phase: "release",
|
||||||
|
targetActorId: TARGET_ID,
|
||||||
|
abilityId: "player-auto-attack",
|
||||||
|
}));
|
||||||
|
expect(presentationEvents).toContainEqual(expect.objectContaining({
|
||||||
|
occurredAt: 1_000,
|
||||||
|
phase: "release",
|
||||||
|
targetActorId: TARGET_ID,
|
||||||
|
abilityId: "player-auto-attack-offhand",
|
||||||
|
}));
|
||||||
|
expect(publishedCombatEvents.filter((event) => event.kind === "damage"
|
||||||
|
&& event.sourceActorId === PLAYER_AGGRO_ID && event.targetActorId === TARGET_ID
|
||||||
|
&& (event.abilityId === "player-auto-attack" || event.abilityId === "player-auto-attack-offhand")))
|
||||||
|
.toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Dancing Rune Weapon's mirrored damage under its proc identity", () => {
|
||||||
|
useCombatStore.getState().initializeCharacter({ classId: "death-knight", level: 80 });
|
||||||
|
useCombatStore.getState().setPlayerPosition([0, 0, 0]);
|
||||||
|
useCombatStore.getState().registerMob(TARGET_ID, {
|
||||||
|
name: "Player auto event lineage target",
|
||||||
|
level: 80,
|
||||||
|
maxHealth: 1_000_000,
|
||||||
|
xpReward: 0,
|
||||||
|
});
|
||||||
|
setMobPosition(TARGET_ID, [2, 0, 0]);
|
||||||
|
const aura = applyCombatAura(useCombatStore.getState().auras, {
|
||||||
|
definition: WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_AURA,
|
||||||
|
sourceId: PLAYER_AGGRO_ID,
|
||||||
|
targetId: PLAYER_AGGRO_ID,
|
||||||
|
now: 1_000,
|
||||||
|
});
|
||||||
|
useCombatStore.setState({
|
||||||
|
auras: [...aura.auras],
|
||||||
|
autoAttackTargetId: TARGET_ID,
|
||||||
|
nextAutoAttackAt: 1_000,
|
||||||
|
});
|
||||||
|
const healthBeforeSwing = useCombatStore.getState().mobs[TARGET_ID]!.health;
|
||||||
|
|
||||||
|
useCombatStore.getState().tick(0, 1_000);
|
||||||
|
|
||||||
|
const playerDamage = useCombatStore.getState().combatEvents.filter((event) => (
|
||||||
|
event.kind === "damage" && event.sourceActorId === PLAYER_AGGRO_ID
|
||||||
|
&& event.targetActorId === TARGET_ID && event.occurredAt === 1_000
|
||||||
|
));
|
||||||
|
expect(playerDamage).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ abilityId: "player-auto-attack", attackHand: "mainhand" }),
|
||||||
|
expect.objectContaining({
|
||||||
|
abilityId: WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_ABILITY_ID,
|
||||||
|
}),
|
||||||
|
]));
|
||||||
|
expect(playerDamage.find((event) => event.abilityId
|
||||||
|
=== WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_ABILITY_ID)).not.toHaveProperty("attackHand");
|
||||||
|
expect(playerDamage).toHaveLength(2);
|
||||||
|
expect(playerDamage.reduce((sum, event) => sum + event.effectiveAmount, 0))
|
||||||
|
.toBe(healthBeforeSwing - useCombatStore.getState().mobs[TARGET_ID]!.health);
|
||||||
|
expect(presentationEvents.filter((event) => event.phase === "release"
|
||||||
|
&& event.abilityId === "player-auto-attack")).toHaveLength(1);
|
||||||
|
expect(presentationEvents.some((event) => event.phase === "release"
|
||||||
|
&& event.abilityId === WOW335_DEATH_KNIGHT_DANCING_RUNE_WEAPON_ABILITY_ID)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user