Release Healer Man 0.1.45

This commit is contained in:
phenom committed 2026-09-15 14:43:15 -04:00
1 parent 756ee4f64a
commit f82daf1c13
242 files changed
+1440 -101

No files matched your search

+128 -5
View File
@@ -179,6 +179,7 @@ import {
} from "./combatResults";
import {
DEFAULT_ENEMY_ATTACK,
enemyControlPreventsActions,
maximumEnemyAttackRange,
normalizeEnemyAttacks,
type EnemyAttackAnimation,
@@ -301,6 +302,7 @@ export interface MobAura {
readonly sourceAbilityId: string;
readonly endsAt: number;
readonly magnitude?: number;
readonly lifeStealPercent?: number;
/** Persistent driver for a content-authored main-hand auto-attack proc. */
readonly meleeAutoCriticalProc?: EnemyMeleeAutoCriticalProc;
/** A charged modifier consumed by a successful main-hand auto attack. */
@@ -4285,7 +4287,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
else if (ability.effects.every((effect) => effect.kind === "scripted" || effect.kind === "unsupported")) failure = "unsupported";
else if (!isAbilityUnlocked(ability, state.level)) failure = "level-locked";
else if (state.health <= 0) failure = "player-dead";
else if (state.controlledUntil > now) failure = "player-controlled";
else if (
state.controlledUntil > now
&& enemyControlPreventsActions(state.controlMechanic)
) failure = "player-controlled";
else if (
effectiveTarget === "hostile"
&& abilityAttackKind(state.classId, ability) === "melee"
@@ -6904,7 +6909,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|| !enemySpatial
|| isPositionInFacingArc(enemySpatial, candidate, Math.PI);
});
const presentationTargetId = targets[0] ?? resolution.targetActorId;
const presentationTargetId = attack.target === "self"
? id
: targets[0] ?? resolution.targetActorId;
const presentationTargetPosition = attack.target === "self"
? position
: actorPosition(runtimeState, presentationTargetId);
const instantSourceCast = !resolvedEnemyCast && attack.spellId !== undefined;
emitEnemyAttackPresentation(
mob,
@@ -6913,10 +6923,14 @@ export const useCombatStore = create<CombatState>((set, get) => ({
now,
position,
presentationTargetId,
actorPosition(runtimeState, presentationTargetId),
presentationTargetPosition,
instantSourceCast,
);
const effects = effect.kind === "composite" ? effect.effects : [effect];
let hasDamageResolution = false;
for (const effect of effects) {
if (effect.kind === "damage") {
hasDamageResolution = true;
const baselineDamage = Math.max(0, Math.round(mob.attackDamage * effect.multiplier));
const attackDamage = attack.runewakerFormula
? adaptRunewakerEnemyDamage({
@@ -6934,6 +6948,25 @@ export const useCombatStore = create<CombatState>((set, get) => ({
instantCast: instantSourceCast,
});
}
} else if (effect.kind === "leech") {
hasDamageResolution = true;
const attackDamage = Math.max(0, Math.round(mob.attackDamage * effect.damageMultiplier));
for (const actorId of targets) {
incomingHits.push({
actorId,
amount: attackDamage,
mob,
attack,
attackRevision,
instantCast: instantSourceCast,
});
}
if (targets.length) {
enemyHealing.push({
targetId: id,
amount: Math.max(1, Math.round(mob.attackDamage * effect.healMultiplier)),
});
}
} else if (effect.kind === "heal") {
const requiredMissingHealth = attack.condition?.kind === "friendly-missing-health"
? attack.condition.amount
@@ -6975,6 +7008,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
sourceAbilityId: attack.id,
endsAt,
...(effect.magnitude === undefined ? {} : { magnitude: effect.magnitude }),
...(effect.lifeStealPercent === undefined
? {}
: { lifeStealPercent: effect.lifeStealPercent }),
...(effect.meleeAutoCriticalProc === undefined
? {}
: { meleeAutoCriticalProc: effect.meleeAutoCriticalProc }),
@@ -6987,6 +7023,29 @@ export const useCombatStore = create<CombatState>((set, get) => ({
disposition: "buff",
durationMs: effect.durationMs > 0 ? effect.durationMs : null,
dispelCategory: effect.aura === "enrage" ? "enrage" : "magic",
...(effect.damageTakenMultiplier === undefined
? {}
: {
modifiers: [{
kind: "damage" as const,
direction: "taken" as const,
operation: "percent" as const,
value: effect.damageTakenMultiplier - 1,
...(effect.damageSchool ? { school: effect.damageSchool } : {}),
}],
}),
...(effect.absorbMultiplier === undefined
? {}
: {
absorbs: [{
id: `${attack.id}:absorb`,
amount: Math.max(1, Math.round(mob.attackDamage * effect.absorbMultiplier)),
...(effect.damageSchool ? { school: effect.damageSchool } : {}),
}],
}),
...(effect.reflectSpellChance === undefined
? {}
: { reflectSpellChance: effect.reflectSpellChance }),
},
sourceId: id,
targetId: id,
@@ -7001,6 +7060,17 @@ export const useCombatStore = create<CombatState>((set, get) => ({
disposition: "debuff",
durationMs: effect.durationMs > 0 ? effect.durationMs : null,
dispelCategory: "magic",
...(effect.damageTakenMultiplier === undefined
? {}
: {
modifiers: [{
kind: "damage" as const,
direction: "taken" as const,
operation: "percent" as const,
value: effect.damageTakenMultiplier - 1,
...(effect.damageSchool ? { school: effect.damageSchool } : {}),
}],
}),
},
sourceId: id,
targetId: actorId,
@@ -7009,6 +7079,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
}
} else if (effect.kind === "control") {
if ((effect.damageMultiplier ?? 0) > 0) {
hasDamageResolution = true;
const baselineDamage = Math.max(0, Math.round(mob.attackDamage * effect.damageMultiplier!));
const attackDamage = attack.runewakerFormula
? adaptRunewakerEnemyDamage({
@@ -7036,7 +7107,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
definition: {
id: `enemy-control:${id}:${attack.id}:${effect.mechanic}`,
name: `${attack.name}: ${effect.mechanic}`,
description: `${attack.name} inflicts ${effect.mechanic}, preventing normal actions until the effect ends.`,
description: effect.mechanic === "slow"
? `${attack.name} reduces movement speed until the effect ends.`
: effect.mechanic === "root"
? `${attack.name} prevents movement until the effect ends.`
: `${attack.name} inflicts ${effect.mechanic}, preventing normal actions until the effect ends.`,
disposition: "debuff",
durationMs: effect.durationMs,
dispelCategory: "magic",
@@ -7055,6 +7130,20 @@ export const useCombatStore = create<CombatState>((set, get) => ({
endsAt: 0,
},
];
} else if (effect.kind === "pull") {
const pulledPosition: CombatPosition = [position[0], position[1], position[2]];
for (const actorId of targets) {
if (actorId === PLAYER_AGGRO_ID) {
set({ playerPosition: pulledPosition });
} else {
updatePartyRuntimePosition(
actorId,
pulledPosition[0],
pulledPosition[1],
pulledPosition[2],
);
}
}
} else if (effect.kind === "call-for-help") {
for (const helperId of nearbyMobRuntimeIds(position, effect.radius)) {
const helper = mobs[helperId];
@@ -7096,6 +7185,37 @@ export const useCombatStore = create<CombatState>((set, get) => ({
} else if (effect.kind === "encounter-event") {
dispatchDungeonEncounterSignal(effect.eventId, now);
}
}
if (hasDamageResolution && targets.length) {
const lifeStealPercent = activeAuras.reduce(
(maximum, aura) => Math.max(maximum, aura.lifeStealPercent ?? 0),
0,
);
if (lifeStealPercent > 0) {
enemyHealing.push({
targetId: id,
amount: Math.max(1, Math.round(mob.attackDamage * lifeStealPercent)),
});
}
}
if (!hasDamageResolution) {
const auraResolution = effects.some((entry) => (
entry.kind === "aura"
|| entry.kind === "target-aura"
|| entry.kind === "control"
|| entry.kind === "transform"
));
emitEnemyAttackPresentation(
mob,
attack,
auraResolution ? "aura-start" : "impact",
now,
position,
presentationTargetId,
presentationTargetPosition,
instantSourceCast,
);
}
attackRevision += 1;
abilityCastCounts[attack.id] = (abilityCastCounts[attack.id] ?? 0) + 1;
attackAnimation = attack.animation;
@@ -7795,7 +7915,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const autoAttackSource = combatSpatialActor(PLAYER_AGGRO_ID);
const canAutoAttack = (
!state.activeCast
&& state.controlledUntil <= now
&& (
state.controlledUntil <= now
|| !enemyControlPreventsActions(state.controlMechanic)
)
&& Math.sqrt(mobRuntimeDistanceSquared(autoAttackTargetId!, state.playerPosition)) <= PLAYER_AUTO_ATTACK_RANGE
&& Boolean(autoAttackTargetPosition)
&& combatHasLineOfSight(state.playerPosition, autoAttackTargetPosition ?? state.playerPosition)