Release Healer Man 0.1.22

This commit is contained in:
phenom committed 2026-09-09 11:21:33 -04:00
1 parent f6f2375145
commit 7f6001723a
144 files changed
+54107 -690

No files matched your search

+58 -9
View File
@@ -1,4 +1,4 @@
import type { DungeonDefinition, DungeonExpansion } from "./dungeonTypes";
import type { DungeonBossObjective, DungeonDefinition, DungeonExpansion } from "./dungeonTypes";
import {
normalizeEnemyAttack,
type EnemyAbilityCondition,
@@ -114,6 +114,10 @@ function describeEffect(effect: EnemyAbilityEffect | undefined, attack: EnemyAtt
}
if (effect.kind === "heal") return `Heals ${target} for ${effect.multiplier}× boss power`;
if (effect.kind === "aura") {
if (effect.meleeAutoCriticalProc) {
const proc = effect.meleeAutoCriticalProc;
return `Applies ${titleCase(effect.aura)} to ${target}; successful main-hand autos have a ${Math.round(proc.chance * 100)}% chance to grant ${proc.charges} charge${proc.charges === 1 ? "" : "s"} of +${proc.criticalChancePercent}% critical chance to the next main-hand auto for ${seconds(proc.durationMs)}`;
}
const magnitude = effect.magnitude === undefined ? "" : ` at ${effect.magnitude} strength`;
return `Applies ${titleCase(effect.aura)} to ${target} for ${seconds(effect.durationMs)}${magnitude}`;
}
@@ -121,7 +125,10 @@ function describeEffect(effect: EnemyAbilityEffect | undefined, attack: EnemyAtt
return `Applies ${titleCase(effect.aura)} to ${target} for ${seconds(effect.durationMs)}`;
}
if (effect.kind === "control") {
return `${titleCase(effect.mechanic)} controls ${target} for ${seconds(effect.durationMs)}`;
const damage = effect.damageMultiplier === undefined
? ""
: `${titleCase(attack.school)} damage (${Number(effect.damageMultiplier.toFixed(2))}× boss power) and `;
return `${damage}${titleCase(effect.mechanic)} controls ${target} for ${seconds(effect.durationMs)}`;
}
if (effect.kind === "transform") return `Transforms ${target} into ${titleCase(effect.form)}`;
if (effect.kind === "summon") return `Summons ${effect.count} add${effect.count === 1 ? "" : "s"} (creature ${effect.creatureEntry})`;
@@ -271,18 +278,25 @@ function normalizedName(value: string): string {
function bossEntityForObjective(
dungeon: DungeonDefinition,
objectiveId: string,
objectiveName: string,
objective: DungeonBossObjective,
): readonly [string, PopulationEntityDefinition] | undefined {
const staticSpawn = dungeon.staticSpawns.find((spawn) => spawn.id === objectiveId);
const staticSpawn = dungeon.staticSpawns.find((spawn) => spawn.id === objective.id);
if (staticSpawn) {
const entity = dungeon.entities[staticSpawn.entityId];
if (entity) return [staticSpawn.entityId, entity];
}
for (const group of dungeon.encounterSpawnGroups ?? []) {
const spawn = group.spawns.find((candidate) => candidate.id === objective.id);
if (spawn) {
const entity = dungeon.entities[spawn.entityId];
if (entity) return [spawn.entityId, entity];
}
}
for (const pack of dungeon.roamingPacks) {
const member = pack.members.find((candidate) => (
`${pack.id}:${candidate.id}` === objectiveId || candidate.id === objectiveId
`${pack.id}:${candidate.id}` === objective.id || candidate.id === objective.id
));
if (member) {
const entity = dungeon.entities[member.entityId];
@@ -290,11 +304,46 @@ function bossEntityForObjective(
}
}
const wanted = normalizedName(objectiveName);
return Object.entries(dungeon.entities).find(([, entity]) => (
const wanted = normalizedName(objective.name);
const named = Object.entries(dungeon.entities).find(([, entity]) => (
entity.kind === "boss"
&& (normalizedName(entity.name) === wanted || normalizedName(entity.title) === wanted)
));
if (named) return named;
if (objective.eventOnly) {
const related = Object.entries(dungeon.entities)
.filter(([, entity]) => {
const entityName = normalizedName(entity.name);
return entityName.length >= 5 && (wanted.includes(entityName) || entityName.includes(wanted));
})
.sort((left, right) => {
const leftBoss = left[1].kind === "boss" ? 1 : 0;
const rightBoss = right[1].kind === "boss" ? 1 : 0;
if (leftBoss !== rightBoss) return rightBoss - leftBoss;
const leftMechanics = left[1].combat?.attacks.filter((attack) => attack.id !== "basic-attack").length ?? 0;
const rightMechanics = right[1].combat?.attacks.filter((attack) => attack.id !== "basic-attack").length ?? 0;
return rightMechanics - leftMechanics || left[0].localeCompare(right[0]);
});
if (related[0]) return related[0];
}
// Aggregate objectives such as Blackrock Depths' "The Seven" use a
// synthetic objective id but retain the final source actor's exact position.
// That representative may be typed as a mob because the objective covers a
// multi-actor event rather than one loot-bearing boss.
for (const group of dungeon.encounterSpawnGroups ?? []) {
const exact = group.spawns.filter((candidate) => {
const dx = candidate.position[0] - objective.position[0];
const dy = candidate.position[1] - objective.position[1];
const dz = candidate.position[2] - objective.position[2];
return dx * dx + dy * dy + dz * dz <= 0.25 * 0.25;
});
const spawn = exact.find((candidate) => dungeon.entities[candidate.entityId]?.kind === "boss")
?? exact.find((candidate) => dungeon.entities[candidate.entityId] !== undefined);
if (spawn) return [spawn.entityId, dungeon.entities[spawn.entityId]];
}
return undefined;
}
function bossGuide(
@@ -325,7 +374,7 @@ export function buildDungeonBossGuide(
): DungeonBossGuide {
const representedEntityIds = new Set<string>();
const objectiveBosses = dungeon.bosses.map((objective) => {
const resolved = bossEntityForObjective(dungeon, objective.id, objective.name);
const resolved = bossEntityForObjective(dungeon, objective);
if (resolved) representedEntityIds.add(resolved[0]);
return bossGuide(dungeon, objective.id, objective.name, resolved, overrides);
});