import type { ClassId } from "../app/characterCatalog"; import generated from "./generated/partyTalentRuntimePlans.json"; type TalentRanks = Readonly>; 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 = {}; for (const nodeId of plan.slice(0, points)) ranks[nodeId] = (ranks[nodeId] ?? 0) + 1; return Object.freeze(ranks); }