Update Healer Man server

This commit is contained in:
phenom committed 2026-09-19 21:01:51 -04:00
1 parent f8314e7de1
commit 1fedb9368d
76 files changed
+20556 -2523

No files matched your search

+81 -60
View File
@@ -26,7 +26,16 @@ import {
deriveCharacterStats,
} from "./wotlkStats";
import type { InventoryItem } from "./lootTypes";
import { runtimePartyTalentRanksForLevel } from "./partyTalentRuntime";
import {
runtimeDefaultPartyTalentProfile,
runtimePartyTalentRanksForLevel,
} from "./partyTalentRuntime";
import {
DEFAULT_COMPANION_SPEC_READINESS_POLICY,
WOW_COMPANION_SPECIALIZATIONS,
companionSpecializationsForPolicy,
type CompanionSpecReadinessPolicy,
} from "./companionSpecReadiness";
import type { RuntimeTalentRanks as TalentRanks } from "./talentRuntimeCatalog";
import type { EnemyControlMechanic } from "./enemyAttacks";
import type { ContentCategoryId } from "../app/contentCategories";
@@ -62,6 +71,7 @@ export interface PartyMember {
readonly specialization: string;
readonly specializationId: string | null;
readonly talentTreeId: string;
readonly talentProfileId: string | null;
readonly talentRanks: TalentRanks;
readonly role: PartyRole;
readonly level: number;
@@ -94,6 +104,7 @@ interface PartyTemplate {
readonly classId: ClassId;
readonly specialization: string;
readonly talentTreeId: string;
readonly talentProfileId?: string | null;
}
export interface ConfiguredPartyMemberSeed {
@@ -103,45 +114,43 @@ export interface ConfiguredPartyMemberSeed {
readonly specialization: string;
readonly specializationId?: string | null;
readonly talentTreeId?: string;
readonly talentProfileId?: string | null;
readonly talentRanks?: TalentRanks;
readonly role: PartyRole;
}
const TANK_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Garrick", classId: "warrior", specialization: "Protection", talentTreeId: "warrior-protection" },
{ name: "Morvane", classId: "death-knight", specialization: "Blood", talentTreeId: "death-knight-blood" },
{ name: "Seraphin", classId: "paladin", specialization: "Protection", talentTreeId: "paladin-protection" },
]);
export interface CompanionPartyGenerationOptions {
readonly readinessPolicy?: CompanionSpecReadinessPolicy;
readonly readinessPolicyRevision?: number;
readonly includeBetaSpecs?: boolean;
readonly excludedPlayerSpecializationId?: string | null;
}
const HEALER_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Elowen", classId: "priest", specialization: "Holy", talentTreeId: "priest-holy" },
{ name: "Lucian", classId: "paladin", specialization: "Holy", talentTreeId: "paladin-holy" },
{ name: "Nym", classId: "druid", specialization: "Restoration", talentTreeId: "druid-restoration" },
{ name: "Tala", classId: "shaman", specialization: "Restoration", talentTreeId: "shaman-restoration" },
]);
function companionTemplateFor(specializationId: string): PartyTemplate {
const definition = specializationsForClass(specializationId.split(":", 1)[0] as ClassId)
.find((candidate) => candidate.id === specializationId);
if (!definition) throw new Error(`Unknown companion specialization ${specializationId}.`);
const profile = runtimeDefaultPartyTalentProfile(definition.classId, definition.displayName);
return Object.freeze({
name: profile.characterName,
classId: definition.classId,
specialization: definition.displayName,
talentTreeId: `${definition.classId}-${definition.talentTree.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
talentProfileId: profile.id,
});
}
/** Every entry is a Wrath damage specialization; tank and healer specs are excluded. */
const DAMAGE_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Brynn", classId: "warrior", specialization: "Arms", talentTreeId: "warrior-arms" },
{ name: "Kael", classId: "warrior", specialization: "Fury", talentTreeId: "warrior-fury" },
{ name: "Aurelia", classId: "paladin", specialization: "Retribution", talentTreeId: "paladin-retribution" },
{ name: "Tamsin", classId: "hunter", specialization: "Marksmanship", talentTreeId: "hunter-marksmanship" },
{ name: "Fen", classId: "hunter", specialization: "Survival", talentTreeId: "hunter-survival" },
{ name: "Vale", classId: "rogue", specialization: "Combat", talentTreeId: "rogue-combat" },
{ name: "Nyx", classId: "rogue", specialization: "Assassination", talentTreeId: "rogue-assassination" },
{ name: "Selene", classId: "priest", specialization: "Shadow", talentTreeId: "priest-shadow" },
{ name: "Varek", classId: "death-knight", specialization: "Frost", talentTreeId: "death-knight-frost" },
{ name: "Thorne", classId: "death-knight", specialization: "Unholy", talentTreeId: "death-knight-unholy" },
{ name: "Korra", classId: "shaman", specialization: "Enhancement", talentTreeId: "shaman-enhancement" },
{ name: "Orin", classId: "shaman", specialization: "Elemental", talentTreeId: "shaman-elemental" },
{ name: "Mira", classId: "mage", specialization: "Arcane", talentTreeId: "mage-arcane" },
{ name: "Ember", classId: "mage", specialization: "Fire", talentTreeId: "mage-fire" },
{ name: "Ilyra", classId: "mage", specialization: "Frost", talentTreeId: "mage-frost" },
{ name: "Malric", classId: "warlock", specialization: "Affliction", talentTreeId: "warlock-affliction" },
{ name: "Sable", classId: "warlock", specialization: "Demonology", talentTreeId: "warlock-demonology" },
{ name: "Rowan", classId: "druid", specialization: "Balance", talentTreeId: "druid-balance" },
{ name: "Ash", classId: "druid", specialization: "Feral (Cat)", talentTreeId: "druid-feral-combat" },
]);
function wowTemplates(
role: PartyRole,
options: CompanionPartyGenerationOptions,
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;
return candidates.map((definition) => companionTemplateFor(definition.id));
}
const ROM_TANK_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Aldric", classId: "rom-warrior", specialization: "Primary", talentTreeId: "" },
@@ -160,22 +169,19 @@ const ROM_DAMAGE_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Roran", classId: "rom-knight", specialization: "Primary", talentTreeId: "" },
]);
export const TANK_SPECIALIZATIONS = Object.freeze(new Set([
"warrior:Protection",
"death-knight:Blood",
"paladin:Protection",
]));
export const TANK_SPECIALIZATIONS = Object.freeze(new Set(
WOW_COMPANION_SPECIALIZATIONS.filter((definition) => definition.role === "tank")
.map((definition) => `${definition.classId}:${definition.displayName}`),
));
export function isTankSpecialization(classId: ClassId, specialization: string): boolean {
return TANK_SPECIALIZATIONS.has(`${classId}:${specialization}`);
}
export const HEALER_SPECIALIZATIONS = Object.freeze(new Set([
"priest:Holy",
"paladin:Holy",
"druid:Restoration",
"shaman:Restoration",
]));
export const HEALER_SPECIALIZATIONS = Object.freeze(new Set(
WOW_COMPANION_SPECIALIZATIONS.filter((definition) => definition.role === "healer")
.map((definition) => `${definition.classId}:${definition.displayName}`),
));
export function isHealerSpecialization(classId: ClassId, specialization: string): boolean {
return HEALER_SPECIALIZATIONS.has(`${classId}:${specialization}`);
@@ -338,9 +344,10 @@ function createMember(
specialization: template.specialization,
specializationId,
talentTreeId: template.talentTreeId,
talentProfileId: template.talentProfileId ?? null,
talentRanks: configuredTalentRanks ?? (isRomClassId(template.classId)
? Object.freeze({})
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, level)),
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, level, template.talentProfileId ?? undefined)),
role,
level: Math.max(1, Math.trunc(level)),
health: maxHealth,
@@ -380,6 +387,7 @@ export function createConfiguredPartyMember(
classId: configuration.classId,
specialization: configuration.specialization,
talentTreeId: configuration.talentTreeId ?? configuration.specializationId ?? "",
talentProfileId: configuration.talentProfileId ?? null,
}, configuration.role, level, index, seed, configuration.talentRanks);
return Object.freeze(withMemberAbilityResourcePools({
...member,
@@ -399,11 +407,13 @@ export function createDungeonParty(
playerRole: PartyRole = "healer",
partySize: PartySize = 5,
categoryId: ContentCategoryId = "wow",
options: CompanionPartyGenerationOptions = {},
): readonly PartyMember[] {
const random = { value: hashSeed(seed || "healer-man") };
const tankTemplates = categoryId === "rom" ? ROM_TANK_TEMPLATES : TANK_TEMPLATES;
const healerTemplates = categoryId === "rom" ? ROM_HEALER_TEMPLATES : HEALER_TEMPLATES;
const damageTemplates = categoryId === "rom" ? ROM_DAMAGE_TEMPLATES : DAMAGE_TEMPLATES;
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) {
@@ -436,11 +446,12 @@ export function createSpectatorDungeonParty(
seed: string,
level: number,
categoryId: ContentCategoryId = "wow",
options: CompanionPartyGenerationOptions = {},
): readonly PartyMember[] {
const random = { value: hashSeed(seed || "healer-man-spectator") };
const tankTemplates = categoryId === "rom" ? ROM_TANK_TEMPLATES : TANK_TEMPLATES;
const healerTemplates = categoryId === "rom" ? ROM_HEALER_TEMPLATES : HEALER_TEMPLATES;
const damagePool = [...(categoryId === "rom" ? ROM_DAMAGE_TEMPLATES : DAMAGE_TEMPLATES)];
const tankTemplates = categoryId === "rom" ? ROM_TANK_TEMPLATES : wowTemplates("tank", options, 1);
const healerTemplates = categoryId === "rom" ? ROM_HEALER_TEMPLATES : wowTemplates("healer", options, 1);
const damagePool = [...(categoryId === "rom" ? ROM_DAMAGE_TEMPLATES : wowTemplates("damage", options, 3))];
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]];
@@ -467,6 +478,7 @@ export interface PartyState {
playerRole: PartyRole;
partySize: PartySize;
categoryId: ContentCategoryId;
companionOptions: CompanionPartyGenerationOptions;
members: readonly PartyMember[];
command: PartyCommand;
selectedMemberId: string | null;
@@ -483,8 +495,14 @@ export interface PartyState {
playerRole?: PartyRole,
partySize?: PartySize,
categoryId?: ContentCategoryId,
companionOptions?: CompanionPartyGenerationOptions,
) => void;
initializeSpectatorParty: (
seed: string,
level: number,
categoryId?: ContentCategoryId,
companionOptions?: CompanionPartyGenerationOptions,
) => void;
initializeSpectatorParty: (seed: string, level: number, categoryId?: ContentCategoryId) => void;
resetCombat: (level: number, playerHealth: number) => void;
syncLevel: (level: number) => void;
syncEquipment: (equipment: EquipmentAssignments, inventory: readonly InventoryItem[]) => void;
@@ -532,6 +550,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
playerRole: "healer",
partySize: 5,
categoryId: "wow",
companionOptions: Object.freeze({}),
members: [],
command: "defend",
selectedMemberId: null,
@@ -542,13 +561,14 @@ export const usePartyStore = create<PartyState>((set, get) => ({
objectiveBossId: null,
memberTargetIds: {},
initializeParty: (seed, level, playerHealth, playerRole = "healer", partySize = 5, categoryId = "wow") => set({
initializeParty: (seed, level, playerHealth, playerRole = "healer", partySize = 5, categoryId = "wow", companionOptions = {}) => set({
seed,
spectatorParty: false,
playerRole,
partySize,
categoryId,
members: createDungeonParty(seed, level, playerRole, partySize, categoryId),
companionOptions,
members: createDungeonParty(seed, level, playerRole, partySize, categoryId, companionOptions),
command: "defend",
selectedMemberId: null,
selfSelected: false,
@@ -558,13 +578,14 @@ export const usePartyStore = create<PartyState>((set, get) => ({
memberTargetIds: {},
}),
initializeSpectatorParty: (seed, level, categoryId = "wow") => set({
initializeSpectatorParty: (seed, level, categoryId = "wow", companionOptions = {}) => set({
seed,
spectatorParty: true,
playerRole: "healer",
partySize: 5,
categoryId,
members: createSpectatorDungeonParty(seed, level, categoryId),
companionOptions,
members: createSpectatorDungeonParty(seed, level, categoryId, companionOptions),
command: "attack",
selectedMemberId: null,
selfSelected: false,
@@ -577,8 +598,8 @@ export const usePartyStore = create<PartyState>((set, get) => ({
resetCombat: (level, playerHealth) => set((state) => {
return {
members: state.spectatorParty
? createSpectatorDungeonParty(state.seed ?? "healer-man-spectator", level, state.categoryId)
: createDungeonParty(state.seed ?? "healer-man", level, state.playerRole, state.partySize, state.categoryId),
? createSpectatorDungeonParty(state.seed ?? "healer-man-spectator", level, state.categoryId, state.companionOptions)
: createDungeonParty(state.seed ?? "healer-man", level, state.playerRole, state.partySize, state.categoryId, state.companionOptions),
command: state.spectatorParty ? "attack" : "defend",
selectedMemberId: null,
selfSelected: false,
@@ -623,7 +644,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
resourcePools: Object.freeze({ ...member.resourcePools, [member.resourceType]: resource }),
talentRanks: isRomClassId(member.classId)
? Object.freeze({})
: runtimePartyTalentRanksForLevel(member.classId, member.specialization, safeLevel),
: runtimePartyTalentRanksForLevel(member.classId, member.specialization, safeLevel, member.talentProfileId ?? undefined),
});
}),
};