Release Healer Man 0.1.46

This commit is contained in:
phenom committed 2026-09-16 14:30:58 -04:00
1 parent f82daf1c13
commit 9ff574ac0b
73 files changed
+33486 -174

No files matched your search

+82 -4
View File
@@ -89,6 +89,17 @@ interface PartyTemplate {
readonly talentTreeId: string;
}
export interface ConfiguredPartyMemberSeed {
readonly name: string;
readonly classId: ClassId;
readonly secondaryClassId?: ClassId | null;
readonly specialization: string;
readonly specializationId?: string | null;
readonly talentTreeId?: string;
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" },
@@ -241,6 +252,7 @@ function createMember(
level: number,
index: number,
seed: string,
configuredTalentRanks?: TalentRanks,
): PartyMember {
// Appearance has its own keyed stream so adding customization fields later
// cannot reshuffle which class/spec templates make up this character's party.
@@ -282,9 +294,9 @@ function createMember(
specialization: template.specialization,
specializationId,
talentTreeId: template.talentTreeId,
talentRanks: isRomClassId(template.classId)
talentRanks: configuredTalentRanks ?? (isRomClassId(template.classId)
? Object.freeze({})
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, level),
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, level)),
role,
level: Math.max(1, Math.trunc(level)),
health: maxHealth,
@@ -310,6 +322,27 @@ function createMember(
};
}
/** Creates one exact companion while retaining the live party stat/equipment path. */
export function createConfiguredPartyMember(
configuration: ConfiguredPartyMemberSeed,
level: number,
index: number,
seed: string,
): PartyMember {
const member = createMember({
name: configuration.name,
classId: configuration.classId,
specialization: configuration.specialization,
talentTreeId: configuration.talentTreeId ?? configuration.specializationId ?? "",
}, configuration.role, level, index, seed, configuration.talentRanks);
return Object.freeze({
...member,
secondaryClassId: configuration.secondaryClassId ?? null,
specializationId: configuration.specializationId ?? member.specializationId,
talentRanks: configuration.talentRanks ?? member.talentRanks,
});
}
/**
* Builds the four AI slots around the player's selected role. The resulting
* group is always a classic five-player composition including the player.
@@ -352,6 +385,29 @@ export function createDungeonParty(
].slice(0, Math.max(0, partySize - 1));
}
/** Builds a complete deterministic NPC group for GM spectator simulations. */
export function createSpectatorDungeonParty(
seed: string,
level: number,
categoryId: ContentCategoryId = "wow",
): 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)];
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 tank = tankTemplates[Math.floor(nextRandom(random) * tankTemplates.length)] ?? tankTemplates[0];
const healer = healerTemplates[Math.floor(nextRandom(random) * healerTemplates.length)] ?? healerTemplates[0];
return [
createMember(tank, "tank", level, 0, seed),
createMember(healer, "healer", level, 1, seed),
...damagePool.slice(0, 3).map((template, index) => createMember(template, "damage", level, index + 2, seed)),
];
}
export interface PartyRuntimeCommit {
readonly members: readonly PartyMember[];
readonly activeMobId: string | null;
@@ -361,6 +417,7 @@ export interface PartyRuntimeCommit {
export interface PartyState {
seed: string | null;
spectatorParty: boolean;
playerRole: PartyRole;
partySize: PartySize;
categoryId: ContentCategoryId;
@@ -381,6 +438,7 @@ export interface PartyState {
partySize?: PartySize,
categoryId?: ContentCategoryId,
) => 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;
@@ -424,6 +482,7 @@ export interface PartyState {
export const usePartyStore = create<PartyState>((set, get) => ({
seed: null,
spectatorParty: false,
playerRole: "healer",
partySize: 5,
categoryId: "wow",
@@ -439,6 +498,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
initializeParty: (seed, level, playerHealth, playerRole = "healer", partySize = 5, categoryId = "wow") => set({
seed,
spectatorParty: false,
playerRole,
partySize,
categoryId,
@@ -452,10 +512,28 @@ export const usePartyStore = create<PartyState>((set, get) => ({
memberTargetIds: {},
}),
initializeSpectatorParty: (seed, level, categoryId = "wow") => set({
seed,
spectatorParty: true,
playerRole: "healer",
partySize: 5,
categoryId,
members: createSpectatorDungeonParty(seed, level, categoryId),
command: "attack",
selectedMemberId: null,
selfSelected: false,
routeBlocked: false,
activeMobId: null,
objectiveBossId: null,
memberTargetIds: {},
}),
resetCombat: (level, playerHealth) => set((state) => {
return {
members: createDungeonParty(state.seed ?? "healer-man", level, state.playerRole, state.partySize, state.categoryId),
command: "defend",
members: state.spectatorParty
? createSpectatorDungeonParty(state.seed ?? "healer-man-spectator", level, state.categoryId)
: createDungeonParty(state.seed ?? "healer-man", level, state.playerRole, state.partySize, state.categoryId),
command: state.spectatorParty ? "attack" : "defend",
selectedMemberId: null,
selfSelected: false,
routeBlocked: false,