Release Healer Man 0.1.37

This commit is contained in:
phenom committed 2026-09-12 21:14:41 -04:00
1 parent 03a4e3f51c
commit 361734bc41
41 files changed
+1714 -110

No files matched your search

+62 -57
View File
@@ -2175,10 +2175,7 @@ function executeTriggeredAuraProcsInWork(
const maximum = work.maxResourcePools[resource] ?? defaultResourceMaximum(resource);
work.resourcePools[resource] = clamp((work.resourcePools[resource] ?? 0) + action.amount, 0, maximum);
} else if (action.kind === "heal" && actionTarget === PLAYER_AURA_ENTITY_ID) {
const healed = Math.min(Math.max(0, action.amount), work.maxHealth - work.health);
work.health += healed;
work.progression.health = work.health;
work.totalHealing += healed;
work.totalHealing += healPlayerInWork(work, action.amount);
} else if (action.kind === "heal" && actionTarget && !work.mobs[actionTarget]) {
work.totalHealing += healCompanionActorInWork(work, actionTarget, Math.max(0, action.amount));
} else if (action.kind === "damage" && actionTarget && work.mobs[actionTarget]) {
@@ -2379,7 +2376,8 @@ function grantMobExperience(
work.secondaryClassId,
work.auras,
);
progression.health = progression.maxHealth;
// A lingering DoT may grant experience after death, but leveling is not resurrection.
progression.health = work.health > 0 ? progression.maxHealth : 0;
work.health = progression.health;
work.maxHealth = progression.maxHealth;
work.maxResource = talentAdjustedMaxResource(
@@ -2877,10 +2875,7 @@ function executeTriggeredSpellInWork(
"taken",
));
if (recipient === PLAYER_AURA_ENTITY_ID) {
const healed = Math.min(amount, work.maxHealth - work.health);
work.health += healed;
work.progression.health = work.health;
work.totalHealing += healed;
work.totalHealing += healPlayerInWork(work, amount);
} else {
const healed = healCompanionActorInWork(work, recipient, amount);
work.totalHealing += healed;
@@ -3188,6 +3183,18 @@ function applyAuraHealthTransitionInWork(
mirrorCompanionActor(nextActor);
}
/** Ordinary healing never transitions a corpse back to a living actor. */
function healPlayerInWork(work: MutableCastWork, amount: number): number {
const requested = Math.max(0, Math.round(Number.isFinite(amount) ? amount : 0));
if (work.health <= 0 || requested <= 0) return 0;
const healed = Math.min(requested, Math.max(0, work.maxHealth - work.health));
work.health += healed;
work.progression.health = work.health;
const actor = work.actors[PLAYER_AURA_ENTITY_ID];
if (actor) work.actors[PLAYER_AURA_ENTITY_ID] = { ...actor, health: work.health, alive: true };
return healed;
}
function healCompanionActorInWork(work: MutableCastWork, id: string, amount: number): number {
const actor = ensureCompanionActorInWork(work, id);
const requested = Math.max(0, Math.round(Number.isFinite(amount) ? amount : 0));
@@ -4700,7 +4707,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
? Math.min(resolution.rawAmount, friendlySummon.maxHealth - friendlySummon.health)
: friendlyOnline
? Math.min(resolution.rawAmount, friendlyOnline.maxHealth - friendlyOnline.health)
: resolution.effectiveAmount;
: healPlayerInWork(work, resolution.rawAmount);
if (friendlySummon && healed > 0) {
work.summons = work.summons.map((summon) => summon.id === healingTargetId
? { ...summon, health: summon.health + healed }
@@ -4710,9 +4717,6 @@ export const useCombatStore = create<CombatState>((set, get) => ({
} else if (friendlyOnline && healed > 0) {
const actor = work.actors[healingTargetId] ?? friendlyOnline;
work.actors[healingTargetId] = { ...actor, health: actor.health + healed, alive: true };
} else if (!friendlyTarget) {
work.health += healed;
work.progression.health = work.health;
}
recordCombatEventInWork(work, {
occurredAt: now,
@@ -4744,10 +4748,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
&& aura.definition.utilityTags?.includes("beacon-of-light")
));
if (beacon?.targetId === PLAYER_AURA_ENTITY_ID) {
const beaconHealing = Math.min(directHealing, work.maxHealth - work.health);
work.health += beaconHealing;
work.progression.health = work.health;
work.totalHealing += beaconHealing;
work.totalHealing += healPlayerInWork(work, directHealing);
} else if (beacon) {
work.totalHealing += healCompanionActorInWork(work, beacon.targetId, directHealing);
}
@@ -5331,10 +5332,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
work.resource = clamp(work.resource + restored, 0, work.maxResource);
work.resourcePools[activeResourceType] = work.resource;
} else if (/food|refreshment|health|firestone/i.test(ability.name)) {
const healed = Math.min(Math.max(1, Math.round(work.maxHealth * 0.25)), work.maxHealth - work.health);
work.health += healed;
work.progression.health = work.health;
work.totalHealing += healed;
work.totalHealing += healPlayerInWork(work, Math.max(1, Math.round(work.maxHealth * 0.25)));
}
} else if (effect.action === "portal") {
summonSequence += 1;
@@ -5363,13 +5361,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
aura.targetId === PLAYER_AURA_ENTITY_ID
&& aura.definition.utilityTags?.includes("rom-revival-charge")
));
const restored = Math.min(
work.maxHealth - work.health,
Math.round(work.maxHealth * 0.05 * (revival?.stacks ?? 0)),
);
work.health += restored;
work.progression.health = work.health;
work.totalHealing += restored;
work.totalHealing += healPlayerInWork(work, Math.round(work.maxHealth * 0.05 * (revival?.stacks ?? 0)));
if (revival) work.auras = work.auras.filter((aura) => aura.instanceId !== revival.instanceId);
}
}
@@ -5412,9 +5404,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
// Judgement of Light restores 2% health when the marked target is struck.
if (work.totalDamage > 0 && targetId && work.mobs[targetId]?.statuses.some((entry) => entry.kind === "marked")) {
const healed = Math.min(talentAdjustedAmount(Math.round(work.maxHealth * 0.02), modifiers, "healing-percent", ability), work.maxHealth - work.health);
work.health += healed;
work.progression.health = work.health;
const healed = healPlayerInWork(work,
talentAdjustedAmount(Math.round(work.maxHealth * 0.02), modifiers, "healing-percent", ability));
work.totalHealing += healed;
addHealingThreatInWork(work, castThreatSource, healed);
}
@@ -5874,10 +5865,15 @@ export const useCombatStore = create<CombatState>((set, get) => ({
if (id === PLAYER_AURA_ENTITY_ID) {
const restored = state.health > 0 ? 0 : Math.max(1, Math.round(state.maxHealth * fraction));
if (restored <= 0) return 0;
const playerActor = state.actors[id];
set({
health: restored,
controlledUntil: 0,
controlMechanic: null,
playerAnimationEvent: nextAnimationEvent("ability-cancel"),
actors: playerActor ? { ...state.actors, [id]: {
...playerActor, health: restored, alive: true, controlledUntil: 0, cast: null,
} } : state.actors,
feedback: nextFeedback("heal", `Resurrected with ${restored} health.`, { amount: restored, abilityId }),
combatEvents: recordCombatEvent(state.combatEvents, {
occurredAt: now,
@@ -6101,7 +6097,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const state = get();
if (!state.actors[sourceActorId]) return false;
if (effect.kind === "dot" && (!state.mobs[targetActorId] || state.mobs[targetActorId].dead)) return false;
if (effect.kind === "hot" && !state.actors[targetActorId]) return false;
if (effect.kind === "hot") {
const targetHealth = targetActorId === PLAYER_AURA_ENTITY_ID ? state.health
: state.summons.find((summon) => summon.id === targetActorId)?.health
?? state.actors[targetActorId]?.health ?? 0;
if (!state.actors[targetActorId] || targetHealth <= 0) return false;
}
const sourceActor = state.actors[sourceActorId];
const sourceMember = usePartyStore.getState().members.find((member) => member.id === sourceActorId);
const amount = sourceMember
@@ -6402,11 +6403,15 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const restored = Math.max(1, Math.round(
state.maxHealth * Math.max(0.01, Math.min(1, percentMaxHealth)),
));
const playerActor = state.actors[PLAYER_AURA_ENTITY_ID];
set({
health: restored,
controlledUntil: 0,
controlMechanic: null,
playerAnimationEvent: nextAnimationEvent("ability-cancel"),
actors: playerActor ? { ...state.actors, [PLAYER_AURA_ENTITY_ID]: {
...playerActor, health: restored, alive: true, controlledUntil: 0, cast: null,
} } : state.actors,
feedback: nextFeedback("heal", `Resurrected with ${restored} health.`, { amount: restored }),
});
return restored;
@@ -7578,7 +7583,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
combatEvents: [...state.combatEvents],
actors: { ...state.actors },
cooldowns: { ...state.cooldowns },
health: Math.min(
health: state.health <= 0 ? 0 : Math.min(
state.maxHealth,
state.health + resolveAuraStatValue(
state.gearStats.healthPer5 / 5 + state.gearStats.spirit * 0.01,
@@ -7707,10 +7712,24 @@ export const useCombatStore = create<CombatState>((set, get) => ({
}
}
const timedEffectTargetAlive = (effect: TimedCombatEffect): boolean => {
if (effect.kind === "dot") {
const mob = effect.targetId ? work.mobs[effect.targetId] : null;
return Boolean(mob && !mob.dead && mob.health > 0);
}
const targetId = effect.targetId ?? PLAYER_AURA_ENTITY_ID;
if (targetId === PLAYER_AURA_ENTITY_ID) return work.health > 0;
const summon = work.summons.find((candidate) => candidate.id === targetId);
if (summon) return summon.health > 0;
const actor = ensureCompanionActorInWork(work, targetId) ?? work.actors[targetId];
return Boolean(actor?.alive && actor.health > 0);
};
for (const effect of state.effects) {
// Check before due ticks/procs, not only after applying their health changes.
if (!timedEffectTargetAlive(effect)) continue;
let remainingTicks = effect.remainingTicks;
let nextTickAt = effect.nextTickAt;
while (remainingTicks > 0 && nextTickAt <= now) {
while (remainingTicks > 0 && nextTickAt <= now && timedEffectTargetAlive(effect)) {
const ability = abilityById(effect.sourceAbilityId);
const baseAmount = effect.fixedAmount ?? (ability
? equipmentAdjustedCombatAmount(
@@ -7764,10 +7783,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
"taken",
));
if (effect.targetId) {
const target = work.actors[effect.targetId]
?? ensureCompanionActorInWork(work, effect.targetId)
?? (work.summons.some((summon) => summon.id === effect.targetId)
? combatActorFromSummon(work.summons.find((summon) => summon.id === effect.targetId)!, progression.level, work.auras)
const target = ensureCompanionActorInWork(work, hotTargetId)
?? work.actors[hotTargetId]
?? (work.summons.some((summon) => summon.id === hotTargetId)
? combatActorFromSummon(work.summons.find((summon) => summon.id === hotTargetId)!, progression.level, work.auras)
: null);
if (target?.alive) partyHotTicks.push({
memberId: target.id,
@@ -7776,9 +7795,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
sourceAbilityId: effect.sourceAbilityId,
});
} else {
const healed = Math.min(modifiedHealing, work.maxHealth - work.health);
work.health += healed;
work.progression.health = work.health;
const healed = healPlayerInWork(work, modifiedHealing);
work.totalHealing += healed;
addHealingThreatInWork(
work,
@@ -7823,12 +7840,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
remainingTicks -= 1;
nextTickAt += effect.intervalMs;
}
const targetAlive = effect.kind === "dot"
? Boolean(effect.targetId && !work.mobs[effect.targetId]?.dead)
: !effect.targetId || usePartyStore.getState().members.some(
(member) => member.id === effect.targetId && member.health > 0,
);
if (remainingTicks > 0 && targetAlive) {
if (remainingTicks > 0 && timedEffectTargetAlive(effect)) {
work.effects.push(
remainingTicks === effect.remainingTicks && nextTickAt === effect.nextTickAt
? effect
@@ -7884,13 +7896,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|| Math.sqrt(distanceSquared(area.position, position)) > area.radius
|| !combatHasLineOfSight(area.position, position)) continue;
const member = partyMembers.find((candidate) => candidate.id === friendlyId);
if (member ? member.health <= 0 : work.health <= 0) continue;
const healed = member
? healCompanionActorInWork(work, friendlyId, amount)
: Math.min(amount, work.maxHealth - work.health);
if (!member) {
work.health += healed;
work.progression.health = work.health;
}
: healPlayerInWork(work, amount);
work.totalHealing += healed;
recordCombatEventInWork(work, {
occurredAt: nextTickAt,
@@ -7941,12 +7950,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const target = injured[0];
if (target) {
const healed = target.id === PLAYER_AURA_ENTITY_ID
? Math.min(summon.attackDamage, work.maxHealth - work.health)
? healPlayerInWork(work, summon.attackDamage)
: healCompanionActorInWork(work, target.id, summon.attackDamage);
if (target.id === PLAYER_AURA_ENTITY_ID) {
work.health += healed;
work.progression.health = work.health;
}
work.totalHealing += healed;
addHealingThreatInWork(work, { actorId: summon.ownerActorId, role: "healer", abilityId: summon.sourceAbilityId }, healed);
recordCombatEventInWork(work, {