Release Healer Man 0.1.58

This commit is contained in:
phenom committed 2026-09-20 16:46:17 -04:00
1 parent 14e105642c
commit 246b4832b0
38 files changed
+952 -93

No files matched your search

+49 -31
View File
@@ -124,6 +124,10 @@ export interface CompanionPartyGenerationOptions {
readonly readinessPolicyRevision?: number;
readonly includeBetaSpecs?: boolean;
readonly excludedPlayerSpecializationId?: string | null;
readonly excludedPlayerSpecializationIds?: readonly string[];
readonly allowedCompanionSpecializationIds?: readonly string[];
/** All real-player roles in a shared party, including the local player. */
readonly occupiedHumanRoles?: readonly PartyRole[];
}
function companionTemplateFor(specializationId: string): PartyTemplate {
@@ -146,9 +150,18 @@ function wowTemplates(
seats: number,
): readonly PartyTemplate[] {
const policy = options.readinessPolicy ?? DEFAULT_COMPANION_SPEC_READINESS_POLICY;
const definitions = companionSpecializationsForPolicy(policy, options.includeBetaSpecs === true, role);
const withoutPlayer = definitions.filter((definition) => definition.id !== options.excludedPlayerSpecializationId);
const candidates = withoutPlayer.length >= seats ? withoutPlayer : definitions;
const allowed = options.allowedCompanionSpecializationIds
? new Set(options.allowedCompanionSpecializationIds)
: null;
const definitions = allowed
? WOW_COMPANION_SPECIALIZATIONS.filter((definition) => definition.role === role && allowed.has(definition.id))
: companionSpecializationsForPolicy(policy, options.includeBetaSpecs === true, role);
const excluded = new Set([
...(options.excludedPlayerSpecializationIds ?? []),
...(options.excludedPlayerSpecializationId ? [options.excludedPlayerSpecializationId] : []),
]);
const withoutPlayers = definitions.filter((definition) => !excluded.has(definition.id));
const candidates = withoutPlayers.length >= seats ? withoutPlayers : definitions;
return candidates.map((definition) => companionTemplateFor(definition.id));
}
@@ -410,35 +423,40 @@ export function createDungeonParty(
options: CompanionPartyGenerationOptions = {},
): readonly PartyMember[] {
const random = { value: hashSeed(seed || "healer-man") };
const tankTemplates = categoryId === "rom" ? ROM_TANK_TEMPLATES : wowTemplates("tank", options, 1);
const healerTemplates = categoryId === "rom" ? ROM_HEALER_TEMPLATES : wowTemplates("healer", options, 1);
const damageSeats = playerRole === "damage" ? 2 : 3;
const damageTemplates = categoryId === "rom" ? ROM_DAMAGE_TEMPLATES : wowTemplates("damage", options, damageSeats);
const tank = tankTemplates[Math.floor(nextRandom(random) * tankTemplates.length)] ?? tankTemplates[0];
const damagePool = [...damageTemplates];
for (let index = damagePool.length - 1; index > 0; index -= 1) {
const swapIndex = Math.floor(nextRandom(random) * (index + 1));
[damagePool[index], damagePool[swapIndex]] = [damagePool[swapIndex], damagePool[index]];
const npcSeats = Math.max(0, partySize - 1);
const humanRoleCounts: Record<PartyRole, number> = { tank: 0, healer: 0, damage: 0 };
for (const role of options.occupiedHumanRoles?.length ? options.occupiedHumanRoles : [playerRole]) {
humanRoleCounts[role] += 1;
}
const healer = healerTemplates[Math.floor(nextRandom(random) * healerTemplates.length)]
?? healerTemplates[0];
if (playerRole === "tank") {
return [
createMember(healer, "healer", level, 0, seed),
...damagePool.slice(0, 3).map((template, index) => createMember(template, "damage", level, index + 1, seed)),
].slice(0, Math.max(0, partySize - 1));
}
if (playerRole === "damage") {
return [
createMember(tank, "tank", level, 0, seed),
createMember(healer, "healer", level, 1, seed),
...damagePool.slice(0, 2).map((template, index) => createMember(template, "damage", level, index + 2, seed)),
].slice(0, Math.max(0, partySize - 1));
}
return [
createMember(tank, "tank", level, 0, seed),
...damagePool.slice(0, 3).map((template, index) => createMember(template, "damage", level, index + 1, seed)),
].slice(0, Math.max(0, partySize - 1));
const npcRoles = (["tank", "healer", "damage", "damage", "damage"] as const).filter((role) => {
if (humanRoleCounts[role] <= 0) return true;
humanRoleCounts[role] -= 1;
return false;
}).slice(0, npcSeats);
const seatsByRole: Record<PartyRole, number> = { tank: 0, healer: 0, damage: 0 };
for (const role of npcRoles) seatsByRole[role] += 1;
const sourceForRole = (role: PartyRole): readonly PartyTemplate[] => {
if (categoryId === "rom") {
return role === "tank" ? ROM_TANK_TEMPLATES : role === "healer" ? ROM_HEALER_TEMPLATES : ROM_DAMAGE_TEMPLATES;
}
return wowTemplates(role, options, seatsByRole[role]);
};
const pools = Object.fromEntries((["tank", "healer", "damage"] as const).map((role) => {
const pool = [...sourceForRole(role)];
for (let index = pool.length - 1; index > 0; index -= 1) {
const swapIndex = Math.floor(nextRandom(random) * (index + 1));
[pool[index], pool[swapIndex]] = [pool[swapIndex], pool[index]];
}
return [role, pool] as const;
})) as Record<PartyRole, PartyTemplate[]>;
const usedByRole: Record<PartyRole, number> = { tank: 0, healer: 0, damage: 0 };
return npcRoles.map((role, index) => {
const pool = pools[role];
if (!pool.length) throw new Error(`No ${role} companion is available for this party.`);
const template = pool[usedByRole[role] % pool.length];
usedByRole[role] += 1;
return createMember(template, role, level, index, seed);
});
}
/** Builds a complete deterministic NPC group for GM spectator simulations. */