Update Healer Man server
This commit is contained in:
1 parent
f8314e7de1
commit
1fedb9368d
76 files changed
+20556
-2523
No files matched your search
+81
-60
@@ -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),
|
||||
});
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user