Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+49 -26
View File
@@ -26,7 +26,9 @@ import {
damageAfterEquipmentMitigation,
deriveCharacterStats,
} from "./wotlkStats";
import { clampPlayerLevel } from "./progression";
import type { InventoryItem } from "./lootTypes";
import type { RomNativeRecoveryClock } from "./romNativeRecovery";
import {
runtimeDefaultPartyTalentProfile,
runtimePartyTalentRanksForLevel,
@@ -91,6 +93,7 @@ export interface PartyMember {
readonly appearance: CharacterAppearance;
readonly classId: ClassId;
readonly statRuleset?: CharacterStatRuleset;
readonly romNativeRecoveryClock?: RomNativeRecoveryClock;
readonly secondaryClassId?: ClassId | null;
readonly specialization: string;
readonly specializationId: string | null;
@@ -202,20 +205,15 @@ function wowTemplates(
}
const ROM_TANK_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ name: "Aldric", classId: "rom-warrior", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ ...romFiveIdentity("rom-knight", "tank")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ name: "Maerwen", classId: "rom-warden", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ name: "Bronn", classId: "rom-champion", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
]);
const ROM_HEALER_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ ...romFiveIdentity("rom-priest", "healer")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ name: "Sylva", classId: "rom-druid", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
]);
const ROM_DAMAGE_TEMPLATES: readonly PartyTemplate[] = Object.freeze([
{ ...romFiveIdentity("rom-scout", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ ...romFiveIdentity("rom-rogue", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ ...romFiveIdentity("rom-mage", "damage")!, statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
{ name: "Morgha", classId: "rom-warlock", statRuleset: "rom", specialization: "Primary", talentTreeId: "" },
]);
export const TANK_SPECIALIZATIONS = Object.freeze(new Set(
@@ -264,7 +262,7 @@ function memberMaxHealth(
secondaryClassId?: ClassId | null,
gender: GenderId = "male",
): number {
const safeLevel = Math.max(1, Math.trunc(level));
const safeLevel = clampPlayerLevel(level);
if (statRuleset === "rom") {
return deriveCharacterStats(classId, safeLevel, gearStats, raceId, {}, secondaryClassId, gender, statRuleset).health;
}
@@ -285,9 +283,10 @@ function memberMaxResource(
secondaryClassId?: ClassId | null,
gender: GenderId = "male",
): number {
const safeLevel = clampPlayerLevel(level);
const profile = resourceProfileForClass(classId);
return profile.type === "mana"
? deriveCharacterStats(classId, level, gearStats, raceId, {}, secondaryClassId, gender, statRuleset).mana
? deriveCharacterStats(classId, safeLevel, gearStats, raceId, {}, secondaryClassId, gender, statRuleset).mana
: profile.maximum;
}
@@ -311,6 +310,19 @@ function initialSecondaryResource(type: ResourceType, maximum: number): number {
: maximum;
}
function rescaledMemberValue(
current: number,
currentMaximum: number,
nextMaximum: number,
preserveNativeCap: boolean,
minimum: number,
): number {
if (current <= 0) return 0;
const fraction = Math.max(0, Math.min(1, current / Math.max(1, currentMaximum)));
if (preserveNativeCap && fraction >= 1) return nextMaximum;
return Math.max(minimum, Math.min(nextMaximum, Math.round(fraction * nextMaximum)));
}
/** Keeps every resource used by the member's learned kit available to companion AI. */
function withMemberAbilityResourcePools(member: PartyMember): PartyMember {
const types = new Set<ResourceType>([member.resourceType]);
@@ -370,13 +382,13 @@ function withMemberGearStats(member: PartyMember, gearStats: ItemStats): PartyMe
);
const health = member.health <= 0
? 0
: Math.max(1, Math.round(member.health / Math.max(1, member.maxHealth) * maxHealth));
: rescaledMemberValue(member.health, member.maxHealth, maxHealth, member.statRuleset === "rom", 1);
const maxResource = memberMaxResource(
member.classId, member.level, member.raceId, gearStats, member.statRuleset, member.secondaryClassId, member.gender,
);
const resource = Math.max(0, Math.round(
member.resource / Math.max(1, member.maxResource) * maxResource,
));
const resource = rescaledMemberValue(
member.resource, member.maxResource, maxResource, member.statRuleset === "rom", 0,
);
return withMemberAbilityResourcePools({
...member,
resourceMaximums: undefined,
@@ -397,6 +409,7 @@ function createMember(
seed: string,
configuredTalentRanks?: TalentRanks,
): PartyMember {
const safeLevel = clampPlayerLevel(level);
// Appearance has its own keyed stream so adding customization fields later
// cannot reshuffle which class/spec templates make up this character's party.
const identityRandom = {
@@ -432,22 +445,28 @@ function createMember(
ownerId: id,
ownerKind: "party",
classId: template.classId,
secondaryClassId: level >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null,
secondaryClassId: safeLevel >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null,
specializationId,
role,
level,
level: safeLevel,
});
const secondaryClassId = level >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null;
const secondaryClassId = safeLevel >= ROM_SECONDARY_UNLOCK_LEVEL ? template.secondaryClassId ?? null : null;
const maxHealth = memberMaxHealth(
level, role, gearStats, template.classId, race.id, statRuleset, secondaryClassId, gender,
safeLevel, role, gearStats, template.classId, race.id, statRuleset, secondaryClassId, gender,
);
const resourceProfile = resourceProfileForClass(template.classId);
const maxResource = memberMaxResource(
template.classId, level, race.id, gearStats, statRuleset, secondaryClassId, gender,
);
const resource = Math.round(
maxResource * (resourceProfile.maximum > 0 ? resourceProfile.initial / resourceProfile.maximum : 0),
template.classId, safeLevel, race.id, gearStats, statRuleset, secondaryClassId, gender,
);
const resource = statRuleset === "rom"
? resourceProfile.initial === resourceProfile.maximum && resourceProfile.maximum > 0
? maxResource
: Math.max(0, Math.min(maxResource, Math.round(
maxResource * (resourceProfile.maximum > 0 ? resourceProfile.initial / resourceProfile.maximum : 0),
)))
: Math.round(
maxResource * (resourceProfile.maximum > 0 ? resourceProfile.initial / resourceProfile.maximum : 0),
);
return withMemberAbilityResourcePools({
id,
name: template.name,
@@ -463,9 +482,9 @@ function createMember(
talentProfileId: template.talentProfileId ?? null,
talentRanks: configuredTalentRanks ?? (isRomClassId(template.classId)
? Object.freeze({})
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, level, template.talentProfileId ?? undefined)),
: runtimePartyTalentRanksForLevel(template.classId, template.specialization, safeLevel, template.talentProfileId ?? undefined)),
role,
level: Math.max(1, Math.trunc(level)),
level: safeLevel,
health: maxHealth,
maxHealth,
resourceType: resourceProfile.type,
@@ -739,7 +758,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
}),
syncLevel: (level) => set((state) => {
const safeLevel = Math.max(1, Math.trunc(level));
const safeLevel = clampPlayerLevel(level);
if (state.members.every((member) => member.level === safeLevel)) return state;
return {
members: state.members.map((member) => {
@@ -761,7 +780,9 @@ export const usePartyStore = create<PartyState>((set, get) => ({
);
const health = member.health <= 0
? 0
: Math.max(1, Math.round(member.health / Math.max(1, member.maxHealth) * maxHealth));
: rescaledMemberValue(
member.health, member.maxHealth, maxHealth, member.statRuleset === "rom", 1,
);
const maxResource = memberMaxResource(
member.classId,
safeLevel,
@@ -771,9 +792,9 @@ export const usePartyStore = create<PartyState>((set, get) => ({
nextSecondaryClassId,
member.gender,
);
const resource = Math.max(0, Math.round(
member.resource / Math.max(1, member.maxResource) * maxResource,
));
const resource = rescaledMemberValue(
member.resource, member.maxResource, maxResource, member.statRuleset === "rom", 0,
);
return withMemberAbilityResourcePools({
...member,
resourceMaximums: undefined,
@@ -900,6 +921,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
return {
...member,
health: restored,
romNativeRecoveryClock: undefined,
status: "ready",
controlledUntil: 0,
controlMechanic: null,
@@ -928,6 +950,7 @@ export const usePartyStore = create<PartyState>((set, get) => ({
return {
...member,
health,
romNativeRecoveryClock: health <= 0 ? undefined : member.romNativeRecoveryClock,
status: health <= 0 ? "down" : member.status,
};
}),