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
+75
-16
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
abilitiesForClass,
|
||||
abilitiesForCharacter,
|
||||
abilityAtLevel,
|
||||
abilityById,
|
||||
learnedAbilityForBuild,
|
||||
isAbilityUnlocked,
|
||||
resourceProfileForClass,
|
||||
triggeredAbilityBySpellId,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
type ResourceType,
|
||||
} from "./abilityCatalog";
|
||||
import {
|
||||
aiHintsForAbility,
|
||||
canAbilityCastWhileMoving,
|
||||
targetingRulesForAbility,
|
||||
} from "./abilityRules";
|
||||
@@ -50,6 +53,7 @@ import {
|
||||
} from "./combatPresentation";
|
||||
import {
|
||||
choosePartyAbility,
|
||||
choosePartyRotationDecision,
|
||||
commitPartyAbilityUse,
|
||||
partyAbilityAmount,
|
||||
} from "./partyAbilityAi";
|
||||
@@ -231,9 +235,9 @@ export function partyUsesRangedAttacks(
|
||||
|
||||
function partyCatalogAttackRange(member: Pick<PartyMember, "classId"> & Partial<Pick<PartyMember, "level" | "talentRanks">>): number {
|
||||
return Math.max(0, ...abilitiesForClass(member.classId)
|
||||
.filter((ability) => !ability.passive && ability.unlockLevel <= (member.level ?? 80)
|
||||
&& (ability.talentEntryId === undefined || (member.talentRanks?.[`coa-entry-${ability.talentEntryId}`] ?? 0) > 0))
|
||||
.map((ability) => abilityAtLevel(ability, member.level ?? 80))
|
||||
.filter((ability) => !ability.passive)
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level ?? 80, member.talentRanks ?? {}))
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null)
|
||||
.filter((ability) => ability.effects.some((effect) => ["damage", "dot", "finisher-damage", "execute"].includes(effect.kind)))
|
||||
.map((ability) => ability.range.max));
|
||||
}
|
||||
@@ -367,11 +371,11 @@ function partyHealAmount(level: number, spellPower = 0): number {
|
||||
}
|
||||
|
||||
function partyResurrectionAbility(member: PartyMember): AbilityDefinition | null {
|
||||
const definition = abilitiesForClass(member.classId)
|
||||
return abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.filter((ability) => !ability.passive && (ability.target === "friendly" || ability.target === "any"))
|
||||
.filter((ability) => isAbilityUnlocked(ability, member.level))
|
||||
.find((ability) => ability.effects.some((effect) => effect.kind === "resurrection"));
|
||||
return definition ? abilityAtLevel(definition, member.level) : null;
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level, member.talentRanks))
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null)
|
||||
.find((ability) => ability.effects.some((effect) => effect.kind === "resurrection")) ?? null;
|
||||
}
|
||||
|
||||
function partyResurrectionPresentation(
|
||||
@@ -913,6 +917,11 @@ function choosePartySupportIntent(
|
||||
target: null,
|
||||
injuredAllies: injured.length,
|
||||
lowestHealthFraction: target.health / Math.max(1, target.maxHealth),
|
||||
friendlyTargets: injured.map((candidate) => ({
|
||||
id: candidate.id,
|
||||
health: candidate.health,
|
||||
maxHealth: candidate.maxHealth,
|
||||
})),
|
||||
canUseAbility: ability => ability.effects.some(effect => ["heal", "hot", "shield"].includes(effect.kind))
|
||||
&& (healingAbilityCanAidOthers(ability) || target.id === member.id)
|
||||
&& !(healingAbilityHasPendingHot(ability, member.id, target.id, combat.effects)
|
||||
@@ -975,6 +984,9 @@ function executePartyAbility(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (effect.kind === "finisher-damage") {
|
||||
runtimeMember = { ...runtimeMember, comboPoints: 0, comboTargetId: null };
|
||||
}
|
||||
} else if (effect.kind === "heal") {
|
||||
for (const friendlyTargetId of healingRecipientIds(effect, member.id, targetId, combat.actors,
|
||||
id => partyFriendlyPosition(id) ?? getMobPosition(id), friendlyTargets, combatHasLineOfSight, now)) {
|
||||
@@ -1074,6 +1086,14 @@ function executePartyAbility(
|
||||
now,
|
||||
);
|
||||
}
|
||||
} else if (effect.kind === "combo") {
|
||||
const current = runtimeMember.comboTargetId === targetId ? runtimeMember.comboPoints : 0;
|
||||
const comboPoints = Math.max(0, Math.min(5, current + effect.amount));
|
||||
runtimeMember = {
|
||||
...runtimeMember,
|
||||
comboPoints,
|
||||
comboTargetId: comboPoints > 0 ? targetId : null,
|
||||
};
|
||||
} else if (effect.kind === "summon") {
|
||||
useCombatStore.getState().summonActor(member.id, ability, effect, origin, targetId, now);
|
||||
} else if (effect.kind === "pet" && ["summon", "call", "resurrect", "tame"].includes(effect.action)) {
|
||||
@@ -1451,25 +1471,64 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
|
||||
nextMembers[index] = runtimeMember;
|
||||
continue;
|
||||
}
|
||||
const selectedAbility = choosePartyAbility(runtimeMember, {
|
||||
const selectedDecision = choosePartyRotationDecision(runtimeMember, {
|
||||
now,
|
||||
target: targetMob ?? null,
|
||||
targetIsLoose: member.role === "tank" && targetMob?.targetActorId !== member.id,
|
||||
canUseAbility: (ability) => partyAbilityTargetIsValid(runtimeMember, ability, memberTargetId)
|
||||
hasDangerousThreat: member.role !== "tank" && targetMob?.targetActorId === member.id,
|
||||
hostileCount: Object.values(currentCombat.mobs).filter((mob) => mob.engaged && !mob.dead).length,
|
||||
maintainedAbilityIds: new Set([
|
||||
...currentCombat.effects.filter((effect) => effect.sourceActorId === member.id && effect.remainingTicks > 0)
|
||||
.map((effect) => effect.sourceAbilityId),
|
||||
...currentCombat.auras.filter((aura) => aura.sourceId === member.id
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now))
|
||||
.flatMap((aura) => abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level, member.talentRanks))
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null)
|
||||
.filter((ability) => ability.effects.some((effect) => effect.kind === "apply-aura"
|
||||
&& effect.aura.id === aura.definition.id))
|
||||
.map((ability) => ability.id)),
|
||||
]),
|
||||
activeAuraExclusiveGroups: new Set(currentCombat.auras
|
||||
.filter((aura) => aura.sourceId === member.id && (aura.expiresAt === null || aura.expiresAt > now))
|
||||
.flatMap((aura) => aura.definition.exclusiveGroup ? [aura.definition.exclusiveGroup] : [])),
|
||||
activeSummonAbilityIds: new Set(currentCombat.summons
|
||||
.filter((summon) => summon.ownerActorId === member.id && summon.expiresAt > now)
|
||||
.map((summon) => summon.sourceAbilityId)),
|
||||
hasActivePet: currentCombat.summons.some((summon) => {
|
||||
if (summon.ownerActorId !== member.id || summon.expiresAt <= now) return false;
|
||||
const source = abilityById(summon.sourceAbilityId);
|
||||
return source?.effects.some((effect) => effect.kind === "pet"
|
||||
&& ["summon", "call", "resurrect", "tame"].includes(effect.action)) ?? false;
|
||||
}),
|
||||
canUseAbility: (ability) => {
|
||||
const hints = new Set(aiHintsForAbility(ability).categories);
|
||||
const hostileSelfArea = ability.target === "self" && (ability.radius ?? 0) > 0
|
||||
&& (["damage", "execute", "debuff", "control", "interrupt", "taunt"] as const)
|
||||
.some((category) => hints.has(category));
|
||||
const abilityTargetId = (ability.target === "self" && !hostileSelfArea) || ability.target === "friendly"
|
||||
? member.id
|
||||
: memberTargetId;
|
||||
return partyAbilityTargetIsValid(runtimeMember, ability, abilityTargetId)
|
||||
// Let maintained effects tick instead of resetting their first tick on every GCD.
|
||||
&& !currentCombat.effects.some((effect) => effect.sourceActorId === member.id
|
||||
&& effect.sourceAbilityId === ability.id && effect.targetId === memberTargetId && effect.remainingTicks > 0)
|
||||
&& effect.sourceAbilityId === ability.id && effect.targetId === abilityTargetId && effect.remainingTicks > 0)
|
||||
&& !ability.effects.some((effect) => effect.kind === "apply-aura"
|
||||
&& currentCombat.auras.some((aura) => aura.sourceId === member.id
|
||||
&& aura.definition.id === effect.aura.id && aura.targetId === memberTargetId)),
|
||||
&& aura.definition.id === effect.aura.id && aura.targetId === abilityTargetId));
|
||||
},
|
||||
});
|
||||
const selectedAbility = selectedDecision?.ability ?? null;
|
||||
if (!selectedAbility) {
|
||||
const inAttackRange = targetVisible && targetPosition
|
||||
&& distanceSquared(memberPosition, targetPosition) <= partyAttackRange(member) ** 2;
|
||||
nextMembers[index] = { ...runtimeMember, status: inAttackRange ? desiredStatus : "advancing", nextActionAt: now + 250 };
|
||||
continue;
|
||||
}
|
||||
const inRange = targetVisible && partyAbilityTargetIsValid(runtimeMember, selectedAbility, memberTargetId);
|
||||
const abilityTargetId = selectedDecision?.targetId
|
||||
?? (selectedAbility.target === "self" || selectedAbility.target === "friendly" ? member.id : memberTargetId);
|
||||
const inRange = (selectedAbility.target === "self" || targetVisible)
|
||||
&& partyAbilityTargetIsValid(runtimeMember, selectedAbility, abilityTargetId);
|
||||
if (!inRange) {
|
||||
nextMembers[index] = runtimeMember.status === "advancing"
|
||||
? runtimeMember
|
||||
@@ -1494,14 +1553,14 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
|
||||
originalDurationMs: selectedAbility.castTimeMs,
|
||||
pushbackHits: 0,
|
||||
origin: memberPosition,
|
||||
targetId: memberTargetId,
|
||||
targetId: abilityTargetId,
|
||||
},
|
||||
activeCastTargetId: memberTargetId,
|
||||
activeCastTargetId: abilityTargetId,
|
||||
nextActionAt: now + selectedAbility.castTimeMs,
|
||||
};
|
||||
emitPartyAbility(member, selectedAbility, memberTargetId, "cast-start", now);
|
||||
emitPartyAbility(member, selectedAbility, abilityTargetId, "cast-start", now);
|
||||
} else {
|
||||
const execution = executePartyAbility(committed, selectedAbility, memberTargetId, now, nextMembers);
|
||||
const execution = executePartyAbility(committed, selectedAbility, abilityTargetId, now, nextMembers);
|
||||
nextMembers[index] = {
|
||||
...execution.member,
|
||||
status: desiredStatus,
|
||||
|
||||
Reference in new issue
Block a user