Release Healer Man 0.1.40

This commit is contained in:
phenom committed 2026-09-13 13:21:27 -04:00
1 parent ed0d2fea3d
commit 197a64117c
215 files changed
+5131 -181

No files matched your search

+70 -16
View File
@@ -128,6 +128,8 @@ import {
type TriggeredAuraProc,
} from "./combatAuras";
import { rollBossLoot } from "./lootCatalog";
import { bossCardForSource } from "./bossCardCatalog";
import { collectBossCardDrop, emptyBossCardCollection, normalizeBossCardCollection, rollBossCards, type BossCardCollection, type BossCardDrop } from "./bossCards";
import { adaptRunewakerEnemyDamage } from "./runewakerCombatFormula";
import { channelAmountEffects } from "./combatChannelAmounts";
import { normalizeInventory, type InventoryItem } from "./lootTypes";
@@ -357,6 +359,7 @@ export interface MobCombatState {
readonly dead: boolean;
readonly hasLoot: boolean;
readonly pendingLoot: readonly InventoryItem[];
readonly bossCardDrop?: BossCardDrop;
readonly looted: boolean;
readonly despawned: boolean;
readonly diedAt: number | null;
@@ -454,6 +457,7 @@ export interface BossLootReward {
readonly bossId: string;
readonly bossName: string;
readonly items: readonly InventoryItem[];
readonly cards?: BossCardDrop;
readonly acquiredAt: number;
}
@@ -525,6 +529,7 @@ export interface CharacterCombatSeed {
readonly talentRanks?: TalentRanks;
readonly actionBindings?: unknown;
readonly inventory?: unknown;
readonly bossCards?: unknown;
readonly equipment?: unknown;
readonly settings?: Partial<GameplaySettings>;
}
@@ -667,6 +672,8 @@ export interface CombatState {
talentModifiers: readonly TalentModifier[];
actionBindings: ActionBindings;
inventory: readonly InventoryItem[];
bossCards: BossCardCollection;
collectSharedBossCards: (drop: BossCardDrop, bossName: string, now: number) => void;
equipment: EquipmentAssignments;
gearStats: ItemStats;
settings: GameplaySettings;
@@ -785,6 +792,7 @@ interface MutableCastWork {
secondaryClassId: ClassId | null;
/** Active synchronous spell/source/target keys used to break proc cycles. */
triggeredSpellStack?: string[];
triggeredProcKeys?: Set<string>;
}
let feedbackSequence = 0;
@@ -1336,6 +1344,12 @@ function resolvePlayerDamageResult(
const attacker: CombatAttackerStats = {
...baseAttacker,
criticalChancePercent: (baseAttacker.criticalChancePercent ?? 0) + criticalChanceFromTalents,
criticalDamageMultiplier: applyTalentModifierValue(
2,
work.talentModifiers,
"critical-damage-percent",
ability ? abilityTalentContext(ability, periodic) : {},
),
};
const immune = mob.schoolImmunities.includes(school);
return resolveCombatResult({
@@ -2280,17 +2294,29 @@ function evaluateProcsInWork(
event: ProcEvent,
now: number,
): readonly TriggeredAuraProc[] {
const result = evaluateAuraProcs(
work.auras,
event,
now,
({ aura, proc }) => stableCombatUnit(
`${aura.instanceId}:${proc.id}:${event.trigger}:${event.abilityId ?? ""}:${event.otherId ?? ""}:${now}`,
),
);
work.auras = [...result.auras];
executeTriggeredAuraProcsInWork(work, result.triggered, now);
return result.triggered;
const root = work.triggeredProcKeys === undefined;
const triggeredKeys = work.triggeredProcKeys ?? new Set<string>();
work.triggeredProcKeys = triggeredKeys;
try {
const result = evaluateAuraProcs(
work.auras,
event,
now,
({ aura, proc }) => triggeredKeys.has(`${aura.instanceId}:${proc.id}`)
? 1
: stableCombatUnit(
`${aura.instanceId}:${proc.id}:${event.trigger}:${event.abilityId ?? ""}:${event.otherId ?? ""}:${now}`,
),
);
// A proc can activate once for the originating event. Its damage must not
// reactivate it through a different branch of the same triggered chain.
for (const proc of result.triggered) triggeredKeys.add(`${proc.auraInstanceId}:${proc.procId}`);
work.auras = [...result.auras];
executeTriggeredAuraProcsInWork(work, result.triggered, now);
return result.triggered;
} finally {
if (root) work.triggeredProcKeys = undefined;
}
}
function eligibleTargetIds(state: CombatState, origin: CombatPosition, maxRange: number): string[] {
@@ -2600,7 +2626,16 @@ function damageMobInWork(
bonusItemChance: mob.bonusLootChance,
})
: mob.pendingLoot;
const hasLoot = killed ? mob.hasLoot || pendingLoot.length > 0 : mob.hasLoot;
const card = killed && mob.boss
? bossCardForSource(mob.lootSourceId ?? mob.id, useGameStore.getState().activeDungeonId, mob.name)
: null;
const bossCardDrop = card ? {
bossKey: card.key,
spawnId: mob.id,
claimId: `${now}:${mob.deathRevision + 1}:${mob.id}`,
rarities: rollBossCards(),
} satisfies BossCardDrop : mob.bossCardDrop;
const hasLoot = killed ? mob.hasLoot || pendingLoot.length > 0 || Boolean(bossCardDrop) : mob.hasLoot;
const xpAwarded = canAward ? grantMobExperience(classId, work, mob.xpReward) : 0;
engageMobGroupInWork(work, id, now, source.actorId);
const engagedMob = work.mobs[id] ?? mob;
@@ -2618,6 +2653,7 @@ function damageMobInWork(
dead: killed,
hasLoot,
pendingLoot,
bossCardDrop,
looted: killed ? false : mob.looted,
despawned: false,
diedAt: killed ? now : mob.diedAt,
@@ -3025,7 +3061,7 @@ function executeTriggeredSpellInWork(
} else if (effect.kind === "execute") {
for (const mobId of mobTargets) {
const mob = work.mobs[mobId];
if (mob) damageMobInWork(
if (mob && !mob.boss) damageMobInWork(
work.classId,
work,
mobId,
@@ -3064,7 +3100,7 @@ function executeTriggeredSpellInWork(
work.cooldowns[candidate.id] = Math.max(now, readyAt - reduction);
}
}
} else if (effect.kind === "trigger-spell" && depth < 1 && effect.spellId !== spellId) {
} else if (effect.kind === "trigger-spell" && !effect.expanded && depth < 1 && effect.spellId !== spellId) {
executeTriggeredSpellInWork(work, effect.spellId, sourceId, targetId, now, depth + 1, allowCoaHidden);
}
}
@@ -3409,6 +3445,7 @@ function initialState() {
talentModifiers,
actionBindings: defaultActionBindings(defaultActionBarForClass(classId)),
inventory: [] as readonly InventoryItem[],
bossCards: emptyBossCardCollection(),
equipment: {} as EquipmentAssignments,
gearStats: EMPTY_ITEM_STATS,
settings: DEFAULT_GAMEPLAY_SETTINGS,
@@ -3481,6 +3518,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
talentModifiers,
actionBindings: normalizeActionBindings(profile.actionBindings, defaultActionBarForCharacter(classId, secondaryClassId, progression.level)),
inventory,
bossCards: normalizeBossCardCollection(profile.bossCards),
equipment,
gearStats,
settings: normalizedSettings(DEFAULT_GAMEPLAY_SETTINGS, profile.settings ?? {}),
@@ -3717,11 +3755,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
bossId: id,
bossName: mob.name,
items: mob.pendingLoot,
...(mob.bossCardDrop ? { cards: mob.bossCardDrop } : {}),
acquiredAt: now,
} satisfies BossLootReward
: null;
return {
inventory: [...state.inventory, ...mob.pendingLoot],
bossCards: mob.bossCardDrop ? collectBossCardDrop(state.bossCards, mob.bossCardDrop) : state.bossCards,
mobs: {
...state.mobs,
[id]: {
@@ -3745,6 +3785,17 @@ export const useCombatStore = create<CombatState>((set, get) => ({
: state.bossLootRewards.filter((reward) => reward.id !== id),
})),
collectSharedBossCards: (drop, bossName, now) => set((state) => {
const bossCards = collectBossCardDrop(state.bossCards, drop);
if (bossCards === state.bossCards) return state;
return {
bossCards,
bossLootRewards: [...state.bossLootRewards, {
id: ++bossLootRewardSequence, bossId: drop.spawnId, bossName, items: [], cards: drop, acquiredAt: now,
}],
};
}),
equipItem: (instanceId, ownerId = PLAYER_EQUIPMENT_OWNER_ID) => {
const state = get();
const ownerClassId = ownerId === PLAYER_EQUIPMENT_OWNER_ID
@@ -5166,7 +5217,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
} else if (effect.kind === "execute") {
for (const id of affectedMobIds) {
const mob = work.mobs[id];
if (!mob || mob.dead) continue;
// DBC instant-kill effects include pet sacrifices such as Death Pact.
// They must never bypass a boss encounter's health and mechanics.
if (!mob || mob.dead || mob.boss) continue;
damageMobInWork(
state.classId,
work,
@@ -5320,7 +5373,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
} else if (effect.kind === "cooldown-reset") {
cooldownAdjustments.push(effect);
} else if (effect.kind === "trigger-spell") {
if (effect.spellId !== ability.dbcSpellId) {
if (!effect.expanded && effect.spellId !== ability.dbcSpellId) {
executeTriggeredSpellInWork(
work,
effect.spellId,
@@ -8359,6 +8412,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
health: preserveDefeated ? 0 : mob.maxHealth,
dead: preserveDefeated,
pendingLoot: preserveDefeated ? mob.pendingLoot : [],
bossCardDrop: preserveDefeated ? mob.bossCardDrop : undefined,
looted: preserveDefeated ? mob.looted : false,
despawned: preserveDefeated ? mob.despawned : false,
diedAt: preserveDefeated ? mob.diedAt : null,