Release Healer Man 0.1.49
This commit is contained in:
1 parent
39425f2494
commit
612170afd0
91 files changed
+2278
-263
No files matched your search
+126
-18
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user