Release Healer Man 0.1.49

This commit is contained in:
phenom committed 2026-09-17 09:13:15 -04:00
1 parent 39425f2494
commit 612170afd0
91 files changed
+2278 -263

No files matched your search

+126 -18
View File
@@ -15,9 +15,12 @@ import {
abilityAtLevel,
abilityById,
abilityRankForLevel,
abilityTalentNodeId,
defaultActionBarForClass,
defaultActionBarForCharacter,
isAbilityUnlocked,
learnedAbilityForBuild,
learnedAbilityRank,
resourceProfileForClass,
triggeredAbilityBySpellId,
type AbilityAmountScaling,
@@ -218,6 +221,11 @@ import {
onlinePlayerRuntimeActor,
onlinePlayerRuntimeActors,
} from "./onlineSessionRuntime";
import {
abilityIsPrecombatPreparation,
precombatPreparationEffects,
rotationProfileFor,
} from "./combatRotation";
import {
emitCombatPresentation,
presentationDeliveryForAbility,
@@ -709,6 +717,8 @@ export interface CombatState {
synchronizeActors: () => void;
/** Grants learned long-term class stat buffs to the full party without setup casts. */
applyDefaultPartyBuffs: (now?: number) => void;
/** Applies specialization maintenance buffs and permanent pets before the first pull. */
applyPrecombatPreparation: (now?: number) => void;
/** Commits companion simulation results to authoritative actors and mirrors party frames. */
commitCompanionActors: (members: readonly import("./partyStore").PartyMember[]) => void;
castSlot: (index: number, origin?: CombatPosition, now?: number) => CastResult;
@@ -1559,9 +1569,8 @@ function automaticClassPassiveUnlockLevel(ability: AbilityDefinition): number {
function isAutomaticClassPassive(ability: AbilityDefinition): boolean {
if (!ability.passive) return false;
if (ability.source !== "coa-tree") return true;
const node = ability.talentEntryId === undefined
? null
: talentNodeById(`coa-entry-${ability.talentEntryId}`);
const nodeId = abilityTalentNodeId(ability);
const node = nodeId ? talentNodeById(nodeId) : null;
return Boolean(
node?.isPassive
&& (node.abilityEssenceCost ?? 0) === 0
@@ -2161,12 +2170,9 @@ function syncPassiveAuras(
now: number,
): ActiveAura[] {
const definitions = abilities.flatMap((catalogAbility) => {
if (!catalogAbility.passive || !isAbilityUnlocked(catalogAbility, level)) return [];
const learnedRank = catalogAbility.talentEntryId === undefined
? undefined
: talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
if (catalogAbility.talentEntryId !== undefined && !learnedRank) return [];
const ability = abilityAtLevel(catalogAbility, level, learnedRank);
if (!catalogAbility.passive) return [];
const ability = learnedAbilityForBuild(catalogAbility, level, talentRanks);
if (!ability) return [];
return ability.effects
.filter((effect): effect is Extract<AbilityEffect, { kind: "apply-aura" }> => effect.kind === "apply-aura")
.map((effect) => auraDefinitionForAbility(effect.aura, ability));
@@ -3163,6 +3169,20 @@ function grantDamageResourceInWork(
abilityId, rawAmount: requested, effectiveAmount: restored, overheal: requested - restored, absorbed: 0, critical: false });
}
function withoutUnlearnedTalentBindings(
bindings: ActionBindings,
abilities: readonly AbilityDefinition[],
talentRanks: TalentRanks,
): ActionBindings {
const unlearned = new Set(abilities
.filter((ability) => abilityTalentNodeId(ability) && !(learnedAbilityRank(ability, talentRanks) ?? 0))
.map((ability) => ability.id));
return Object.fromEntries(Object.entries(bindings).map(([id, abilityId]) => [
id,
abilityId && unlearned.has(abilityId) ? null : abilityId,
])) as ActionBindings;
}
function timedEffect(
kind: "dot" | "hot",
abilityId: string,
@@ -3562,6 +3582,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
set((state) => ({
...initialState(),
classId,
specializationId,
gearRole,
secondaryClassId,
raceId,
level: progression.level,
@@ -3579,7 +3601,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
auras: syncPassiveAuras([], abilities, progression.level, talentRanks, combatNow()),
talentRanks,
talentModifiers,
actionBindings: normalizeActionBindings(profile.actionBindings, defaultActionBarForCharacter(classId, secondaryClassId, progression.level)),
actionBindings: withoutUnlearnedTalentBindings(
normalizeActionBindings(profile.actionBindings, defaultActionBarForCharacter(classId, secondaryClassId, progression.level)),
abilities,
talentRanks,
),
inventory,
bossCards: normalizeBossCardCollection(profile.bossCards),
equipment,
@@ -4222,6 +4248,90 @@ export const useCombatStore = create<CombatState>((set, get) => ({
}
get().synchronizeActors();
},
applyPrecombatPreparation: (now = combatNow()) => {
get().synchronizeActors();
const state = get();
const partyMembers = usePartyStore.getState().members;
const includePlayer = useDungeonSimulationStore.getState().sessionMode === "play";
const sources = [
...(includePlayer ? [{
id: PLAYER_AURA_ENTITY_ID,
classId: state.classId,
secondaryClassId: state.secondaryClassId,
specializationId: state.specializationId,
specialization: specializationById(state.specializationId)?.displayName ?? null,
role: state.gearRole,
level: state.level,
talentRanks: state.talentRanks,
abilities: state.abilities,
}] : []),
...partyMembers.filter((member) => member.health > 0).map((member) => ({
id: member.id,
classId: member.classId,
secondaryClassId: member.secondaryClassId ?? null,
specializationId: member.specializationId,
specialization: member.specialization,
role: member.role,
level: member.level,
talentRanks: member.talentRanks,
abilities: abilitiesForCharacter(member.classId, member.secondaryClassId),
})),
];
for (const source of sources) {
const rotation = rotationProfileFor(source.classId, source.specializationId, source.specialization);
const priority = (ability: AbilityDefinition) => {
const index = rotation?.abilityPriority.indexOf(ability.name) ?? -1;
return index < 0 ? Number.MAX_SAFE_INTEGER : index;
};
const abilities = source.abilities
.map((ability) => learnedAbilityForBuild(ability, source.level, source.talentRanks))
.filter((ability): ability is AbilityDefinition => ability !== null)
.filter(abilityIsPrecombatPreparation)
.filter((ability) => !rotation || priority(ability) < Number.MAX_SAFE_INTEGER)
.sort((left, right) => priority(left) - priority(right) || left.name.localeCompare(right.name));
for (const ability of abilities) {
const effects = precombatPreparationEffects(ability);
const auraEffects = effects.filter((effect): effect is Extract<AbilityEffect, { kind: "apply-aura" }> => (
effect.kind === "apply-aura"
));
const current = get();
const maintained = current.auras.some((aura) => (
aura.targetId === source.id
&& (aura.expiresAt === null || aura.expiresAt > now)
&& auraEffects.some((effect) => effect.aura.id === aura.definition.id)
));
const blockedByExclusiveGroup = current.auras.some((aura) => (
aura.targetId === source.id
&& (aura.expiresAt === null || aura.expiresAt > now)
&& Boolean(aura.definition.exclusiveGroup)
&& auraEffects.some((effect) => effect.aura.exclusiveGroup === aura.definition.exclusiveGroup)
));
if (!maintained && !blockedByExclusiveGroup) {
for (const effect of auraEffects) get().applyActorAura(source.id, source.id, ability, effect, now);
}
const petEffects = effects.filter((effect): effect is Extract<AbilityEffect, { kind: "pet" }> => (
effect.kind === "pet"
));
if (!petEffects.length) continue;
const hasActivePet = get().summons.some((summon) => {
if (summon.ownerActorId !== source.id || summon.expiresAt <= now) return false;
const summonAbility = abilityById(summon.sourceAbilityId);
return summonAbility?.effects.some((effect) => effect.kind === "pet"
&& ["summon", "call", "resurrect", "tame"].includes(effect.action)) ?? false;
});
if (hasActivePet) continue;
const position = actorPosition(get(), source.id) ?? get().playerPosition;
const petEffect = petEffects[0];
get().summonActor(source.id, ability, {
kind: "summon",
creatureId: petEffect.creatureId ?? 1,
coefficient: 0.35,
durationMs: Number.MAX_SAFE_INTEGER - now,
}, position, null, now);
}
}
get().synchronizeActors();
},
commitCompanionActors: (members) => {
let actors: Readonly<Record<string, CombatActorState>> = {};
set((state) => {
@@ -4254,9 +4364,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const state = get();
const party = usePartyStore.getState();
const catalogAbility = abilityById(abilityId);
const learnedTalentRank = catalogAbility?.talentEntryId === undefined
? undefined
: state.talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
const talentNodeId = catalogAbility ? abilityTalentNodeId(catalogAbility) : null;
const learnedTalentRank = catalogAbility ? learnedAbilityRank(catalogAbility, state.talentRanks) : undefined;
const ability = catalogAbility
? abilityAtLevel(catalogAbility, state.level, learnedTalentRank)
: null;
@@ -4287,7 +4396,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
let failure: CastFailureReason | undefined;
if (!ability) failure = "unknown-ability";
else if (!state.abilities.some((entry) => entry.id === ability.id)) failure = "wrong-class";
else if (ability.talentEntryId !== undefined && !learnedTalentRank) failure = "not-learned";
else if (talentNodeId && !learnedTalentRank) failure = "not-learned";
else if (ability.passive) failure = "passive";
else if (ability.effects.every((effect) => effect.kind === "scripted" || effect.kind === "unsupported")) failure = "unsupported";
else if (!isAbilityUnlocked(ability, state.level)) failure = "level-locked";
@@ -8412,11 +8521,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const cast = get().activeCast;
if (cast?.mode === "channel") {
const catalogAbility = abilityById(cast.abilityId);
const learnedTalentRank = catalogAbility?.talentEntryId === undefined
? undefined
: get().talentRanks[`coa-entry-${catalogAbility.talentEntryId}`] ?? 0;
const ability = catalogAbility
? abilityAtLevel(catalogAbility, get().level, learnedTalentRank)
? learnedAbilityForBuild(catalogAbility, get().level, get().talentRanks)
: null;
let ticksCompleted = cast.ticksCompleted;
let nextTickAt = cast.nextTickAt;
@@ -8775,6 +8881,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
maxResource,
resource: clamp(state.maxResource > 0 ? state.resource / state.maxResource * maxResource : 0, 0, maxResource),
auras,
actionBindings: withoutUnlearnedTalentBindings(state.actionBindings, state.abilities, talentRanks),
feedback: nextFeedback("talent", "Talent points reset."),
};
}),
@@ -8783,6 +8890,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const state = get();
if (abilityId !== null && !state.abilities.some((ability) => (
ability.id === abilityId && !ability.passive
&& (!abilityTalentNodeId(ability) || Boolean(learnedAbilityRank(ability, state.talentRanks)))
))) return false;
set({ actionBindings: withActionBinding(state.actionBindings, layer, control, abilityId) });
return true;