forked from phenom/healer-man
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
import type { ClassId } from "../app/characterCatalog";
|
|
import generated from "./generated/partyTalentRuntimePlans.json";
|
|
|
|
type TalentRanks = Readonly<Record<string, number>>;
|
|
|
|
export interface PartyTalentRuntimeMilestone {
|
|
readonly level: number;
|
|
readonly link: string;
|
|
}
|
|
|
|
export interface PartyTalentRuntimeLevelPlan {
|
|
readonly minimumLevel: number;
|
|
readonly sourceLevels: readonly number[];
|
|
readonly sourcePointCount: number;
|
|
readonly sourcePointFlags: readonly boolean[];
|
|
readonly plan: readonly string[];
|
|
}
|
|
|
|
export interface PartyTalentRuntimeProfile {
|
|
readonly id: string;
|
|
readonly classId: ClassId;
|
|
readonly specialization: string;
|
|
readonly name: string;
|
|
readonly characterName: string;
|
|
readonly source: "azerothcore-playerbots" | "healer-man-generated";
|
|
readonly sourceVersion: string;
|
|
readonly defaultForSpecialization: boolean;
|
|
readonly playerbotsSpecName: string | null;
|
|
readonly milestones: readonly PartyTalentRuntimeMilestone[];
|
|
readonly sourcePointCount: number | null;
|
|
readonly sourcePointsRetained: number | null;
|
|
readonly fallbackPointCount: number;
|
|
readonly plan: readonly string[];
|
|
readonly levelPlans: readonly PartyTalentRuntimeLevelPlan[];
|
|
}
|
|
|
|
export interface PartyTalentRuntimeProvenance {
|
|
readonly sourcePointCount: number | null;
|
|
readonly sourcePointsRetained: number | null;
|
|
readonly fallbackPointCount: number;
|
|
readonly sourceLevels: readonly number[];
|
|
}
|
|
|
|
interface PartyTalentRuntimeSnapshot {
|
|
readonly schemaVersion: 2;
|
|
readonly playerbotsCommit: string;
|
|
readonly profiles: readonly PartyTalentRuntimeProfile[];
|
|
}
|
|
|
|
const snapshot = generated as PartyTalentRuntimeSnapshot;
|
|
const profileById = new Map(snapshot.profiles.map((profile) => [profile.id, profile]));
|
|
|
|
function planKey(classId: ClassId, specialization: string): string {
|
|
return `${classId}:${specialization}`;
|
|
}
|
|
|
|
export function runtimePartyTalentPlan(
|
|
classId: ClassId,
|
|
specialization: string,
|
|
profileId?: string,
|
|
): readonly string[] {
|
|
return runtimePartyTalentProfile(classId, specialization, profileId).plan;
|
|
}
|
|
|
|
export function runtimePartyTalentProfiles(
|
|
classId: ClassId,
|
|
specialization: string,
|
|
): readonly PartyTalentRuntimeProfile[] {
|
|
const key = planKey(classId, specialization);
|
|
return snapshot.profiles.filter((profile) => planKey(profile.classId, profile.specialization) === key);
|
|
}
|
|
|
|
export function runtimePartyTalentProfile(
|
|
classId: ClassId,
|
|
specialization: string,
|
|
profileId?: string,
|
|
): PartyTalentRuntimeProfile {
|
|
const profiles = runtimePartyTalentProfiles(classId, specialization);
|
|
const profile = profileId
|
|
? profileById.get(profileId)
|
|
: profiles.find((candidate) => candidate.defaultForSpecialization);
|
|
if (!profile || profile.classId !== classId || profile.specialization !== specialization) {
|
|
throw new Error(`Unsupported party talent profile: ${classId}:${specialization}:${profileId ?? "default"}`);
|
|
}
|
|
return profile;
|
|
}
|
|
|
|
export function runtimeDefaultPartyTalentProfile(
|
|
classId: ClassId,
|
|
specialization: string,
|
|
): PartyTalentRuntimeProfile {
|
|
return runtimePartyTalentProfile(classId, specialization);
|
|
}
|
|
|
|
function levelPlanFor(
|
|
profile: PartyTalentRuntimeProfile,
|
|
level: number,
|
|
): PartyTalentRuntimeLevelPlan | null {
|
|
const safeLevel = Number.isFinite(level) ? Math.max(1, Math.min(80, Math.trunc(level))) : 1;
|
|
return [...profile.levelPlans]
|
|
.reverse()
|
|
.find((candidate) => candidate.minimumLevel <= safeLevel) ?? null;
|
|
}
|
|
|
|
export function runtimePartyTalentProvenanceForLevel(
|
|
profile: PartyTalentRuntimeProfile,
|
|
level: number,
|
|
): PartyTalentRuntimeProvenance {
|
|
const selected = levelPlanFor(profile, level);
|
|
if (!selected) {
|
|
return {
|
|
sourcePointCount: profile.sourcePointCount,
|
|
sourcePointsRetained: profile.sourcePointsRetained,
|
|
fallbackPointCount: profile.fallbackPointCount,
|
|
sourceLevels: [],
|
|
};
|
|
}
|
|
const safeLevel = Number.isFinite(level) ? Math.max(1, Math.min(80, Math.trunc(level))) : 1;
|
|
const points = Math.max(0, safeLevel - 9);
|
|
const sourcePointsRetained = selected.sourcePointFlags.slice(0, points).filter(Boolean).length;
|
|
return {
|
|
sourcePointCount: selected.sourcePointCount,
|
|
sourcePointsRetained,
|
|
fallbackPointCount: Math.max(0, points - sourcePointsRetained),
|
|
sourceLevels: selected.sourceLevels,
|
|
};
|
|
}
|
|
|
|
/** Runtime-only projection of the catalog-validated 71-point party plans. */
|
|
export function runtimePartyTalentRanksForLevel(
|
|
classId: ClassId,
|
|
specialization: string,
|
|
level: number,
|
|
profileId?: string,
|
|
): TalentRanks {
|
|
const safeLevel = Number.isFinite(level) ? Math.max(1, Math.min(80, Math.trunc(level))) : 1;
|
|
const profile = runtimePartyTalentProfile(classId, specialization, profileId);
|
|
const plan = levelPlanFor(profile, safeLevel)?.plan ?? profile.plan;
|
|
const points = Math.max(0, safeLevel - 9);
|
|
const ranks: Record<string, number> = {};
|
|
for (const nodeId of plan.slice(0, points)) ranks[nodeId] = (ranks[nodeId] ?? 0) + 1;
|
|
return Object.freeze(ranks);
|
|
}
|