Release Healer Man 0.1.4
This commit is contained in:
+310
-27
@@ -153,6 +153,16 @@ import {
|
||||
type ThreatSource,
|
||||
type ThreatTable,
|
||||
} from "./aggro";
|
||||
import { distanceSquared, planarDistanceSquared } from "./mobAi";
|
||||
import {
|
||||
emitCombatPresentation,
|
||||
presentationDeliveryForAbility,
|
||||
presentationSchoolForAbility,
|
||||
presentationSourceForAbility,
|
||||
presentationSourceForSpellId,
|
||||
resetCombatPresentationSession,
|
||||
type CombatPresentationPhase,
|
||||
} from "./combatPresentation";
|
||||
|
||||
/** Stable target id used by named player auras and HUD consumers. */
|
||||
export const PLAYER_AURA_ENTITY_ID = PLAYER_AGGRO_ID;
|
||||
@@ -166,6 +176,7 @@ export type MinimapRotation = "north-up" | "player-up";
|
||||
|
||||
export interface GameplaySettings {
|
||||
readonly showMobHealthBars: boolean;
|
||||
readonly showMobAggroRanges: boolean;
|
||||
readonly showTargetFrame: boolean;
|
||||
readonly showThreatMeter: boolean;
|
||||
readonly threatMeterPosition: NormalizedHudPosition;
|
||||
@@ -179,6 +190,7 @@ export interface GameplaySettings {
|
||||
|
||||
export const DEFAULT_GAMEPLAY_SETTINGS: GameplaySettings = Object.freeze({
|
||||
showMobHealthBars: true,
|
||||
showMobAggroRanges: false,
|
||||
showTargetFrame: true,
|
||||
showThreatMeter: false,
|
||||
threatMeterPosition: DEFAULT_THREAT_METER_POSITION,
|
||||
@@ -196,6 +208,9 @@ export const MOB_WOUND_ANIMATION_COOLDOWN_MS = 650;
|
||||
export const EMPTY_CORPSE_DESPAWN_MS = 10_000;
|
||||
/** Looting cannot hide a corpse before its one-shot death animation has had time to land. */
|
||||
export const MINIMUM_CORPSE_VISIBLE_MS = 1_500;
|
||||
/** Default proximity pull radii for dungeon enemies, expressed in world units. */
|
||||
export const DEFAULT_MOB_AGGRO_RANGE = 16;
|
||||
export const DEFAULT_BOSS_AGGRO_RANGE = 20;
|
||||
|
||||
export interface MobStatus {
|
||||
readonly kind: MobStatusKind;
|
||||
@@ -276,6 +291,7 @@ export interface MobCombatState {
|
||||
readonly attackRange: number;
|
||||
readonly attackDamage: number;
|
||||
readonly swingMs: number;
|
||||
readonly aggroRange: number;
|
||||
readonly leashRange: number;
|
||||
readonly moveSpeed: number;
|
||||
readonly attackRevision: number;
|
||||
@@ -397,6 +413,7 @@ export interface MobRegistration {
|
||||
readonly damageMultiplier?: number;
|
||||
readonly bonusLootChance?: number;
|
||||
readonly moveSpeed?: number;
|
||||
readonly aggroRange?: number;
|
||||
readonly leashRange?: number;
|
||||
readonly attacks?: readonly EnemyAttackDefinition[];
|
||||
readonly mechanicImmunities?: readonly AzerothCoreMechanic[];
|
||||
@@ -504,6 +521,7 @@ export interface CombatState {
|
||||
damagePlayer: (amount: number, school?: DamageSchool, attackerLevel?: number) => number;
|
||||
healPlayer: (amount: number) => number;
|
||||
cancelCast: (message?: string) => boolean;
|
||||
engageNearbyMobs: (now?: number, playerPosition?: CombatPosition) => number;
|
||||
advanceMobCombat: (now?: number, playerPosition?: CombatPosition) => number;
|
||||
tick: (deltaSeconds: number, now?: number) => void;
|
||||
resetEncounter: () => void;
|
||||
@@ -576,6 +594,85 @@ function nextAnimationEvent(
|
||||
return { revision: animationSequence, kind, ...(abilityId ? { abilityId } : {}) };
|
||||
}
|
||||
|
||||
function emitPlayerAbilityPresentation(
|
||||
ability: AbilityDefinition,
|
||||
phase: CombatPresentationPhase,
|
||||
occurredAt: number,
|
||||
origin: CombatPosition,
|
||||
targetId: string | null,
|
||||
targetPosition: CombatPosition | null,
|
||||
): void {
|
||||
emitCombatPresentation({
|
||||
occurredAt,
|
||||
phase,
|
||||
actorGroup: "player",
|
||||
sourceActorId: PLAYER_AGGRO_ID,
|
||||
...(targetId ? { targetActorId: targetId } : {}),
|
||||
abilityId: ability.id,
|
||||
sourceSpellId: ability.dbcSpellId,
|
||||
name: ability.name,
|
||||
source: presentationSourceForAbility(ability),
|
||||
school: presentationSchoolForAbility(ability),
|
||||
delivery: presentationDeliveryForAbility(ability),
|
||||
origin,
|
||||
targetPosition: targetPosition ?? origin,
|
||||
...(ability.radius === undefined ? {} : { radius: ability.radius }),
|
||||
...(ability.castTimeMs > 0 ? { durationMs: ability.castTimeMs } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function emitPlayerAbilityCancellation(abilityId: string, occurredAt: number, origin: CombatPosition): void {
|
||||
const ability = abilityById(abilityId);
|
||||
if (!ability) return;
|
||||
emitPlayerAbilityPresentation(ability, "cast-cancel", occurredAt, origin, null, origin);
|
||||
}
|
||||
|
||||
function emitPlayerAbilityResolutionPresentation(
|
||||
ability: AbilityDefinition,
|
||||
occurredAt: number,
|
||||
origin: CombatPosition,
|
||||
targetId: string | null,
|
||||
targetPosition: CombatPosition | null,
|
||||
): void {
|
||||
emitPlayerAbilityPresentation(ability, "release", occurredAt, origin, targetId, targetPosition);
|
||||
emitPlayerAbilityPresentation(
|
||||
ability,
|
||||
presentationDeliveryForAbility(ability) === "aura" ? "aura-start" : "impact",
|
||||
occurredAt,
|
||||
origin,
|
||||
targetId,
|
||||
targetPosition,
|
||||
);
|
||||
}
|
||||
|
||||
function emitEnemyAttackPresentation(
|
||||
mob: MobCombatState,
|
||||
attack: EnemyAttackDefinition,
|
||||
phase: CombatPresentationPhase,
|
||||
occurredAt: number,
|
||||
origin: CombatPosition,
|
||||
targetActorId: AggroActorId | null,
|
||||
targetPosition: CombatPosition | null,
|
||||
): void {
|
||||
emitCombatPresentation({
|
||||
occurredAt,
|
||||
phase,
|
||||
actorGroup: "enemy",
|
||||
sourceActorId: mob.id,
|
||||
...(targetActorId ? { targetActorId } : {}),
|
||||
abilityId: attack.id,
|
||||
...(attack.spellId === undefined ? {} : { sourceSpellId: attack.spellId }),
|
||||
name: attack.name,
|
||||
source: presentationSourceForSpellId(attack.spellId),
|
||||
school: attack.school,
|
||||
delivery: attack.delivery,
|
||||
origin,
|
||||
targetPosition: targetPosition ?? origin,
|
||||
...(attack.radius === undefined ? {} : { radius: attack.radius }),
|
||||
...((attack.castTimeMs ?? 0) > 0 ? { durationMs: attack.castTimeMs } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function emptyCastResult(reason?: CastFailureReason, abilityId?: string): CastResult {
|
||||
return { ok: !reason, abilityId, reason, affectedMobIds: [], damage: 0, healing: 0, levelsGained: 0 };
|
||||
}
|
||||
@@ -721,10 +818,7 @@ function eligibleAggroActorIds(
|
||||
const withinLeash = (position: CombatPosition | null) => {
|
||||
if (!position) return false;
|
||||
if (!home) return true;
|
||||
const dx = position[0] - home[0];
|
||||
const dy = position[1] - home[1];
|
||||
const dz = position[2] - home[2];
|
||||
return dx * dx + dy * dy + dz * dz <= mob.leashRange * mob.leashRange;
|
||||
return planarDistanceSquared(position, home) <= mob.leashRange * mob.leashRange;
|
||||
};
|
||||
if (state.health > 0 && withinLeash(state.playerPosition)) eligible.add(PLAYER_AGGRO_ID);
|
||||
for (const member of usePartyStore.getState().members) {
|
||||
@@ -1266,6 +1360,7 @@ function normalizedSettings(
|
||||
): GameplaySettings {
|
||||
return {
|
||||
showMobHealthBars: patch.showMobHealthBars ?? current.showMobHealthBars,
|
||||
showMobAggroRanges: patch.showMobAggroRanges ?? current.showMobAggroRanges,
|
||||
showTargetFrame: patch.showTargetFrame ?? current.showTargetFrame,
|
||||
showThreatMeter: patch.showThreatMeter ?? current.showThreatMeter,
|
||||
threatMeterPosition: normalizeHudPosition(
|
||||
@@ -1444,6 +1539,7 @@ function summonedMobCombatState(
|
||||
attackRange: attack.range,
|
||||
attackDamage: stats.attackDamage,
|
||||
swingMs: stats.swingMs,
|
||||
aggroRange: 0,
|
||||
leashRange: summoner.leashRange,
|
||||
moveSpeed: summoner.moveSpeed,
|
||||
attackRevision: 0,
|
||||
@@ -1749,34 +1845,76 @@ function mobGroupId(id: string): string {
|
||||
return separator > 0 ? id.slice(0, separator) : id;
|
||||
}
|
||||
|
||||
function engageMobGroup(
|
||||
mobs: Record<string, MobCombatState>,
|
||||
sourceId: string,
|
||||
now: number,
|
||||
sourceActorId: AggroActorId,
|
||||
): boolean {
|
||||
const groupId = mobGroupId(sourceId);
|
||||
const groupWasEngaged = Object.entries(mobs).some(([id, mob]) => (
|
||||
mobGroupId(id) === groupId && mob.engaged && !mob.dead
|
||||
));
|
||||
let changed = false;
|
||||
for (const [id, mob] of Object.entries(mobs)) {
|
||||
if (mob.dead || mob.despawned || mobGroupId(id) !== groupId) continue;
|
||||
const threatByActor = groupWasEngaged
|
||||
? mob.threatByActor
|
||||
: seedActorThreat(mob.threatByActor, sourceActorId);
|
||||
mobs[id] = {
|
||||
...mob,
|
||||
threatByActor,
|
||||
targetActorId: mob.targetActorId ?? (groupWasEngaged ? null : sourceActorId),
|
||||
engaged: true,
|
||||
combatPhase: "chasing",
|
||||
homePosition: groupWasEngaged
|
||||
? (mob.homePosition ?? getMobPosition(id))
|
||||
: (getMobPosition(id) ?? mob.homePosition),
|
||||
abilityReadyAt: Object.keys(mob.abilityReadyAt).length
|
||||
? mob.abilityReadyAt
|
||||
: initialEnemyAbilityReadyAt(mob, now),
|
||||
nextAttackAt: mob.nextAttackAt > now ? mob.nextAttackAt : now + 450,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function engageMobGroupInWork(
|
||||
work: MutableCastWork,
|
||||
sourceId: string,
|
||||
now: number,
|
||||
sourceActorId: AggroActorId,
|
||||
): void {
|
||||
const groupId = mobGroupId(sourceId);
|
||||
const groupWasEngaged = Object.entries(work.mobs).some(([id, mob]) => (
|
||||
mobGroupId(id) === groupId && mob.engaged && !mob.dead
|
||||
));
|
||||
for (const [id, mob] of Object.entries(work.mobs)) {
|
||||
if (mob.dead || mobGroupId(id) !== groupId) continue;
|
||||
const threatByActor = groupWasEngaged
|
||||
? mob.threatByActor
|
||||
: seedActorThreat(mob.threatByActor, sourceActorId);
|
||||
work.mobs[id] = {
|
||||
...mob,
|
||||
threatByActor,
|
||||
targetActorId: mob.targetActorId ?? (groupWasEngaged ? null : sourceActorId),
|
||||
engaged: true,
|
||||
combatPhase: "chasing",
|
||||
homePosition: mob.homePosition ?? getMobPosition(id),
|
||||
abilityReadyAt: Object.keys(mob.abilityReadyAt).length
|
||||
? mob.abilityReadyAt
|
||||
: initialEnemyAbilityReadyAt(mob, now),
|
||||
nextAttackAt: mob.nextAttackAt > now ? mob.nextAttackAt : now + 450,
|
||||
};
|
||||
engageMobGroup(work.mobs, sourceId, now, sourceActorId);
|
||||
}
|
||||
|
||||
function nearestAggroActorId(
|
||||
state: Pick<CombatState, "health" | "playerPosition">,
|
||||
mobPosition: CombatPosition,
|
||||
aggroRange: number,
|
||||
): AggroActorId | null {
|
||||
if (!Number.isFinite(aggroRange) || aggroRange <= 0) return null;
|
||||
let nearestId: AggroActorId | null = null;
|
||||
let nearestDistanceSquared = aggroRange * aggroRange;
|
||||
if (state.health > 0) {
|
||||
const playerDistanceSquared = distanceSquared(mobPosition, state.playerPosition);
|
||||
if (playerDistanceSquared <= nearestDistanceSquared) {
|
||||
nearestId = PLAYER_AGGRO_ID;
|
||||
nearestDistanceSquared = playerDistanceSquared;
|
||||
}
|
||||
}
|
||||
for (const member of usePartyStore.getState().members) {
|
||||
if (member.health <= 0) continue;
|
||||
const position = getPartyRuntimePosition(member.id);
|
||||
if (!position) continue;
|
||||
const memberDistanceSquared = distanceSquared(mobPosition, position);
|
||||
if (memberDistanceSquared < nearestDistanceSquared) {
|
||||
nearestId = member.id;
|
||||
nearestDistanceSquared = memberDistanceSquared;
|
||||
}
|
||||
}
|
||||
return nearestId;
|
||||
}
|
||||
|
||||
function mobResistanceForSchool(
|
||||
@@ -1893,7 +2031,7 @@ function damageMobInWork(
|
||||
forcedTarget: killed ? null : engagedMob.forcedTarget,
|
||||
engaged: !killed,
|
||||
combatPhase: killed ? "idle" : "chasing",
|
||||
homePosition: mob.homePosition ?? getMobPosition(id),
|
||||
homePosition: engagedMob.homePosition ?? getMobPosition(id),
|
||||
nextAttackAt: killed ? 0 : (mob.nextAttackAt > now ? mob.nextAttackAt : now + 450),
|
||||
activeCast: killed ? null : engagedMob.activeCast,
|
||||
woundRevision: canPlayWoundAnimation ? mob.woundRevision + 1 : mob.woundRevision,
|
||||
@@ -2266,6 +2404,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
...initialState(),
|
||||
|
||||
initializeCharacter: (profile) => {
|
||||
resetCombatPresentationSession();
|
||||
const classId = profile.classId;
|
||||
const secondaryClassId = profile.secondaryClassId ?? null;
|
||||
const raceId = profile.raceId ?? "human";
|
||||
@@ -2371,6 +2510,15 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
attackRange + 1,
|
||||
registration.leashRange ?? existing?.leashRange ?? (boss ? 55 : 42),
|
||||
);
|
||||
const aggroRange = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
leashRange,
|
||||
registration.aggroRange
|
||||
?? existing?.aggroRange
|
||||
?? (boss ? DEFAULT_BOSS_AGGRO_RANGE : DEFAULT_MOB_AGGRO_RANGE),
|
||||
),
|
||||
);
|
||||
const moveSpeed = Math.max(
|
||||
0,
|
||||
registration.moveSpeed ?? existing?.moveSpeed ?? (boss ? 3.15 : 3.5),
|
||||
@@ -2403,6 +2551,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
attacks,
|
||||
attackRange,
|
||||
swingMs,
|
||||
aggroRange,
|
||||
leashRange,
|
||||
moveSpeed,
|
||||
attackDamage,
|
||||
@@ -2454,6 +2603,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
attackRange,
|
||||
attackDamage,
|
||||
swingMs,
|
||||
aggroRange,
|
||||
leashRange,
|
||||
moveSpeed,
|
||||
attackRevision: 0,
|
||||
@@ -2690,6 +2840,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
const movedWhileCasting = Boolean(cast && (
|
||||
(next[0] - cast.origin[0]) ** 2 + (next[2] - cast.origin[2]) ** 2 > 0.04
|
||||
));
|
||||
if (movedWhileCasting) emitPlayerAbilityCancellation(cast!.abilityId, Date.now(), next);
|
||||
return {
|
||||
playerPosition: next,
|
||||
activeCast: movedWhileCasting ? null : cast,
|
||||
@@ -2814,6 +2965,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
? { playerAnimationEvent: nextAnimationEvent("ability-cancel", abilityId) }
|
||||
: {}),
|
||||
});
|
||||
if (completingCast) emitPlayerAbilityCancellation(abilityId, now, playerOrigin);
|
||||
return emptyCastResult(reason, abilityId);
|
||||
}
|
||||
|
||||
@@ -2885,6 +3037,14 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
{ abilityId: ability.id, targetId: targetId ?? undefined },
|
||||
),
|
||||
});
|
||||
emitPlayerAbilityPresentation(
|
||||
ability,
|
||||
"cast-start",
|
||||
now,
|
||||
playerOrigin,
|
||||
targetId,
|
||||
targetPosition,
|
||||
);
|
||||
return { ...emptyCastResult(), ok: true, abilityId: ability.id };
|
||||
}
|
||||
|
||||
@@ -3452,6 +3612,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
{ abilityId: ability.id, targetId: targetId ?? undefined, amount: work.totalDamage || work.totalHealing },
|
||||
),
|
||||
});
|
||||
emitPlayerAbilityResolutionPresentation(
|
||||
ability,
|
||||
now,
|
||||
playerOrigin,
|
||||
targetId,
|
||||
targetPosition,
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -3611,7 +3778,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: mob.forcedTarget,
|
||||
engaged: true,
|
||||
combatPhase: "chasing",
|
||||
homePosition: mob.homePosition ?? getMobPosition(mobId),
|
||||
homePosition: groupWasEngaged
|
||||
? (mob.homePosition ?? getMobPosition(mobId))
|
||||
: (getMobPosition(mobId) ?? mob.homePosition),
|
||||
abilityReadyAt: Object.keys(mob.abilityReadyAt).length
|
||||
? mob.abilityReadyAt
|
||||
: initialEnemyAbilityReadyAt(mob, now),
|
||||
@@ -3706,9 +3875,30 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
playerAnimationEvent: nextAnimationEvent("ability-cancel", cast.abilityId),
|
||||
feedback: nextFeedback("error", message, { abilityId: cast.abilityId, targetId: cast.targetId ?? undefined }),
|
||||
});
|
||||
emitPlayerAbilityCancellation(cast.abilityId, Date.now(), get().playerPosition);
|
||||
return true;
|
||||
},
|
||||
|
||||
engageNearbyMobs: (now = Date.now(), playerPosition) => {
|
||||
const state = get();
|
||||
const runtimeState = playerPosition
|
||||
? { ...state, playerPosition }
|
||||
: state;
|
||||
const mobs: Record<string, MobCombatState> = { ...state.mobs };
|
||||
let engagedGroups = 0;
|
||||
for (const id of Object.keys(mobs)) {
|
||||
const mob = mobs[id];
|
||||
if (mob.dead || mob.despawned || mob.engaged || mob.aggroRange <= 0) continue;
|
||||
const position = getMobPosition(id);
|
||||
if (!position) continue;
|
||||
const actorId = nearestAggroActorId(runtimeState, position, mob.aggroRange);
|
||||
if (!actorId) continue;
|
||||
if (engageMobGroup(mobs, id, now, actorId)) engagedGroups += 1;
|
||||
}
|
||||
if (engagedGroups > 0) set({ mobs });
|
||||
return engagedGroups;
|
||||
},
|
||||
|
||||
advanceMobCombat: (now = Date.now(), playerPosition) => {
|
||||
const state = get();
|
||||
const runtimeState = playerPosition
|
||||
@@ -3852,6 +4042,15 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
attackAnimation = "cast";
|
||||
lastAttackId = attack.id;
|
||||
nextAttackAt = enemyActiveCast.completesAt;
|
||||
emitEnemyAttackPresentation(
|
||||
mob,
|
||||
attack,
|
||||
"cast-start",
|
||||
now,
|
||||
position,
|
||||
resolution.targetActorId,
|
||||
targetPosition,
|
||||
);
|
||||
} else if (selectedAttack && !cannotAttack) {
|
||||
const attack = selectedAttack!;
|
||||
enemyActiveCast = null;
|
||||
@@ -3881,6 +4080,16 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
: attack.target === "primary"
|
||||
? [resolution.targetActorId]
|
||||
: [];
|
||||
const presentationTargetId = targets[0] ?? resolution.targetActorId;
|
||||
emitEnemyAttackPresentation(
|
||||
mob,
|
||||
attack,
|
||||
"release",
|
||||
now,
|
||||
position,
|
||||
presentationTargetId,
|
||||
actorPosition(runtimeState, presentationTargetId),
|
||||
);
|
||||
if (effect.kind === "damage") {
|
||||
const attackDamage = Math.max(0, Math.round(mob.attackDamage * effect.multiplier));
|
||||
for (const actorId of targets) {
|
||||
@@ -4102,6 +4311,19 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
}
|
||||
let appliedDamage = 0;
|
||||
for (const hit of incomingHits) {
|
||||
const impactOrigin = getMobPosition(hit.mob.id);
|
||||
const impactTarget = actorPosition(get(), hit.actorId);
|
||||
if (impactOrigin && impactTarget) {
|
||||
emitEnemyAttackPresentation(
|
||||
hit.mob,
|
||||
hit.attack,
|
||||
"impact",
|
||||
now,
|
||||
impactOrigin,
|
||||
hit.actorId,
|
||||
impactTarget,
|
||||
);
|
||||
}
|
||||
if (hit.actorId === PLAYER_AGGRO_ID) {
|
||||
const current = get();
|
||||
const defender = applyAuraStats(
|
||||
@@ -4437,6 +4659,23 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
work.resourcePools = rage.resourcePools;
|
||||
}
|
||||
if (result.amount > 0) autoAttackEvent = nextAnimationEvent("attack");
|
||||
if (result.amount > 0) {
|
||||
const targetPosition = getMobPosition(autoAttackTargetId!);
|
||||
emitCombatPresentation({
|
||||
occurredAt: now,
|
||||
phase: "release",
|
||||
actorGroup: "player",
|
||||
sourceActorId: PLAYER_AGGRO_ID,
|
||||
targetActorId: autoAttackTargetId!,
|
||||
abilityId: "player-auto-attack",
|
||||
name: "Auto Attack",
|
||||
source: "fallback",
|
||||
school: autoAttackSchool,
|
||||
delivery: "melee",
|
||||
origin: state.playerPosition,
|
||||
targetPosition: targetPosition ?? state.playerPosition,
|
||||
});
|
||||
}
|
||||
nextAutoAttackAt = now + profile.intervalMs;
|
||||
if (result.killed) {
|
||||
autoAttackTargetId = null;
|
||||
@@ -4530,6 +4769,31 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
amount,
|
||||
...(effect.damageSchool ? { school: effect.damageSchool } : {}),
|
||||
}, nextTickAt);
|
||||
if (ability) {
|
||||
const sourceActorId = effect.sourceActorId;
|
||||
const actorGroup = sourceActorId === PLAYER_AGGRO_ID ? "player" : "party";
|
||||
const origin = sourceActorId === PLAYER_AGGRO_ID
|
||||
? state.playerPosition
|
||||
: getPartyRuntimePosition(sourceActorId) ?? state.playerPosition;
|
||||
const targetPosition = effect.targetId
|
||||
? getMobPosition(effect.targetId) ?? getPartyRuntimePosition(effect.targetId)
|
||||
: state.playerPosition;
|
||||
emitCombatPresentation({
|
||||
occurredAt: nextTickAt,
|
||||
phase: "tick",
|
||||
actorGroup,
|
||||
sourceActorId,
|
||||
...(effect.targetId ? { targetActorId: effect.targetId } : {}),
|
||||
abilityId: ability.id,
|
||||
sourceSpellId: ability.dbcSpellId,
|
||||
name: ability.name,
|
||||
source: presentationSourceForAbility(ability),
|
||||
school: presentationSchoolForAbility(ability),
|
||||
delivery: presentationDeliveryForAbility(ability),
|
||||
origin,
|
||||
targetPosition: targetPosition ?? origin,
|
||||
});
|
||||
}
|
||||
remainingTicks -= 1;
|
||||
nextTickAt += effect.intervalMs;
|
||||
}
|
||||
@@ -4711,6 +4975,14 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
get().addHealingThreat(playerThreatSource(get(), ability.id, true), healed);
|
||||
}
|
||||
}
|
||||
emitPlayerAbilityPresentation(
|
||||
ability,
|
||||
"tick",
|
||||
nextTickAt,
|
||||
get().playerPosition,
|
||||
cast.targetId,
|
||||
cast.targetId ? getMobPosition(cast.targetId) : get().playerPosition,
|
||||
);
|
||||
ticksCompleted += 1;
|
||||
nextTickAt += (cast.completesAt - cast.startedAt) / cast.totalTicks;
|
||||
}
|
||||
@@ -4724,6 +4996,16 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
targetId: cast.targetId ?? undefined,
|
||||
}),
|
||||
});
|
||||
if (ability) {
|
||||
emitPlayerAbilityPresentation(
|
||||
ability,
|
||||
"release",
|
||||
now,
|
||||
get().playerPosition,
|
||||
cast.targetId,
|
||||
cast.targetId ? getMobPosition(cast.targetId) : get().playerPosition,
|
||||
);
|
||||
}
|
||||
} else if (ticksCompleted !== cast.ticksCompleted) {
|
||||
set({ activeCast: { ...cast, ticksCompleted, nextTickAt } });
|
||||
}
|
||||
@@ -4736,6 +5018,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|
||||
},
|
||||
|
||||
resetEncounter: () => set((state) => {
|
||||
resetCombatPresentationSession();
|
||||
for (const mob of Object.values(state.mobs)) {
|
||||
if (mob.summonedBy) removeMobPosition(mob.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user