Files
healer-man/src/game/coaLiveCatalog.ts
T
2026-08-14 15:56:39 -04:00

305 lines
9.9 KiB
TypeScript

import {
CLASSES,
type BaseClassId,
type ClassId,
type CoaClassId,
} from "../app/characterCatalog";
import liveData from "./coaLiveData.generated.json";
export type CoaEssenceType = "ability" | "talent";
export type CoaEntryType = "Ability" | "Talent";
export type CoaNodeType = "SpendCircle" | "SpendSquare" | "SpendHex";
export interface CoaLiveSource {
readonly builderUrl: string;
readonly databaseClassUrl: string;
readonly realmId: number;
readonly realmSlug: string;
readonly realmName: string;
readonly maximumLevel: number;
readonly schemaVersion: Readonly<Record<string, number>>;
readonly synchronizedAt: string;
readonly spellDbcPath: string;
readonly supportDbcDirectory: string;
}
export interface CoaLiveCounts {
readonly classes: number;
readonly tabs: number;
readonly entries: number;
readonly abilityEntries: number;
readonly talentEntries: number;
readonly rankedNodeSpells: number;
readonly classSpellRows: number;
readonly classicClassSpellRows: number;
readonly coaClassSpellRows: number;
readonly uniqueSpells: number;
readonly dbcMatchedSpells: number;
readonly progressionChains: number;
readonly classicProgressionChains: number;
readonly classicBaseProgressionChains: number;
readonly coaProgressionChains: number;
readonly progressionRanks: number;
readonly liveTooltips: number;
}
export interface CoaClassTab {
readonly tabId: number;
readonly tabName: string;
readonly sortOrder: number;
}
export interface CoaLiveClass {
readonly classId: number;
readonly className: string;
readonly maxTalentEssence: number;
readonly maxAbilityEssence: number;
readonly tabs: readonly CoaClassTab[];
}
export interface CoaRankDescription {
readonly rank: number;
readonly spellId: number;
readonly description: string;
readonly liveTooltip?: string;
}
export interface CoaTreeEntry {
readonly id: number;
readonly classId: number;
readonly tabId: number;
readonly x: number;
readonly y: number;
readonly name: string;
readonly iconBasename: string;
readonly nodeType: CoaNodeType;
readonly entryType: CoaEntryType;
readonly isPassive: boolean;
readonly maxPoints: number;
readonly aeCost: number;
readonly teCost: number;
readonly requiredClassPoints: number;
readonly requiredSpecPoints: number;
readonly requiredIds: readonly number[];
readonly connectedNodeIds: readonly number[];
readonly requiredLevel: number;
readonly isStartingNode: boolean;
readonly flags: number;
readonly group: number;
readonly sortOrder: number;
readonly description: string;
readonly rankDescriptions: readonly CoaRankDescription[];
}
export interface CoaDbcEffect {
readonly index: number;
readonly effectType: number;
readonly auraType: number;
readonly basePoints: number;
readonly dieSides: number;
readonly pointsPerLevel: number;
readonly mechanic: number;
readonly implicitTargetA: number;
readonly implicitTargetB: number;
readonly radiusIndex: number;
readonly radius: number;
readonly periodMs: number;
readonly valueMultiplier: number;
readonly chainTargets: number;
readonly itemType: number;
readonly miscValue: number;
readonly miscValueB: number;
readonly triggerSpellId: number;
readonly pointsPerCombo: number;
readonly coefficient: number;
}
export interface CoaDbcSpell {
readonly id: number;
readonly name: string;
readonly rankText: string;
readonly description: string;
readonly categoryId: number;
readonly dispelType: number;
readonly mechanic: number;
readonly attributes: readonly number[];
readonly castTimeIndex: number;
readonly castTimeMs: number;
readonly recoveryTimeMs: number;
readonly categoryRecoveryTimeMs: number;
readonly interruptFlags: number;
readonly auraInterruptFlags: number;
readonly channelInterruptFlags: number;
readonly procFlags: number;
readonly procChance: number;
readonly procCharges: number;
readonly maxLevel: number;
readonly baseLevel: number;
readonly spellLevel: number;
readonly durationIndex: number;
readonly durationMs: number;
readonly powerType: number;
readonly powerCost: number;
readonly powerCostPerLevel: number;
readonly powerCostPerSecond: number;
readonly powerCostPerSecondPerLevel: number;
readonly rangeIndex: number;
readonly rangeMin: number;
readonly rangeMax: number;
readonly projectileSpeed: number;
readonly stackAmount: number;
readonly equippedItemClass: number;
readonly equippedItemSubclassMask: number;
readonly equippedItemInventoryMask: number;
readonly iconId: number;
readonly powerCostPercentage: number;
readonly startRecoveryCategory: number;
readonly startRecoveryTimeMs: number;
readonly maximumTargets: number;
readonly damageClass: number;
readonly preventionType: number;
readonly schoolMask: number;
readonly runeCostId: number;
readonly powerDisplayId: number;
readonly effects: readonly CoaDbcEffect[];
}
export interface CoaLiveSpell {
readonly id: number;
readonly classIds: readonly number[];
readonly name: string;
readonly iconBasename: string;
readonly level: number;
readonly rank: string;
readonly description: string;
readonly liveTooltip?: string;
readonly sourceIds: readonly number[];
readonly advancementType: string;
readonly advancementTabId: number;
readonly talented: boolean;
readonly excluded: boolean;
readonly dbc?: CoaDbcSpell;
}
export interface CoaProgressionRank {
readonly spellId: number;
readonly rank: number;
readonly level: number;
}
export interface CoaProgressionChain {
readonly id: string;
readonly classId: number;
readonly name: string;
readonly iconBasename: string;
readonly talented: boolean;
readonly advancementType: string;
readonly trainer: boolean;
readonly ranks: readonly CoaProgressionRank[];
}
interface CoaLiveData {
readonly source: CoaLiveSource;
readonly counts: CoaLiveCounts;
readonly classes: readonly CoaLiveClass[];
readonly entries: readonly CoaTreeEntry[];
readonly progressionChains: readonly CoaProgressionChain[];
readonly spells: readonly CoaLiveSpell[];
}
const data = liveData as unknown as CoaLiveData;
const COA_CLASS_ID_BY_DBC_ID = Object.freeze(Object.fromEntries(
CLASSES
.filter((definition) => definition.mode === "conquest")
.map((definition) => [definition.dbcId, definition.id]),
)) as Readonly<Record<number, CoaClassId>>;
const CLASSIC_CLASS_ID_BY_DBC_ID = Object.freeze(Object.fromEntries(
CLASSES
.filter((definition) => definition.mode === "classic")
.map((definition) => [definition.dbcId, definition.id]),
)) as Readonly<Record<number, BaseClassId>>;
const SPELL_BY_ID = new Map(data.spells.map((spell) => [spell.id, spell]));
const ENTRY_BY_ID = new Map(data.entries.map((entry) => [entry.id, entry]));
const CLASS_BY_DBC_ID = new Map(data.classes.map((definition) => [definition.classId, definition]));
export const COA_LIVE_SOURCE = data.source;
export const COA_LIVE_COUNTS = data.counts;
export const COA_LIVE_CLASSES = data.classes;
export const COA_TREE_ENTRIES = data.entries;
export const COA_PROGRESSION_CHAINS = data.progressionChains.filter((chain) => (
COA_CLASS_ID_BY_DBC_ID[chain.classId] !== undefined
));
export const ASCENSION_CLASSIC_PROGRESSION_CHAINS = data.progressionChains.filter((chain) => (
CLASSIC_CLASS_ID_BY_DBC_ID[chain.classId] !== undefined
));
export function isCoaClassId(classId: ClassId): classId is CoaClassId {
return CLASSES.find((definition) => definition.id === classId)?.mode === "conquest";
}
export function coaClassIdFromDbcId(dbcId: number): CoaClassId | null {
return COA_CLASS_ID_BY_DBC_ID[dbcId] ?? null;
}
export function ascensionClassicClassIdFromDbcId(dbcId: number): BaseClassId | null {
return CLASSIC_CLASS_ID_BY_DBC_ID[dbcId] ?? null;
}
export function coaClassDefinition(classId: CoaClassId): CoaLiveClass {
const dbcId = CLASSES.find((definition) => definition.id === classId)?.dbcId ?? 0;
const definition = CLASS_BY_DBC_ID.get(dbcId);
if (!definition) throw new Error(`Missing live CoA definition for ${classId}`);
return definition;
}
export function coaTreeEntryById(entryId: number): CoaTreeEntry | null {
return ENTRY_BY_ID.get(entryId) ?? null;
}
export function coaEntriesForClass(classId: CoaClassId): readonly CoaTreeEntry[] {
const dbcId = CLASSES.find((definition) => definition.id === classId)?.dbcId ?? 0;
return data.entries.filter((entry) => entry.classId === dbcId);
}
export function coaEntriesForTab(classId: CoaClassId, tabId: number): readonly CoaTreeEntry[] {
return coaEntriesForClass(classId).filter((entry) => entry.tabId === tabId);
}
export function coaSpellById(spellId: number): CoaLiveSpell | null {
return SPELL_BY_ID.get(spellId) ?? null;
}
export function coaProgressionForClass(classId: CoaClassId): readonly CoaProgressionChain[] {
const dbcId = CLASSES.find((definition) => definition.id === classId)?.dbcId ?? 0;
return data.progressionChains.filter((chain) => chain.classId === dbcId);
}
export function ascensionProgressionForClass(classId: BaseClassId): readonly CoaProgressionChain[] {
const dbcId = CLASSES.find((definition) => definition.id === classId)?.dbcId ?? 0;
return ASCENSION_CLASSIC_PROGRESSION_CHAINS.filter((chain) => chain.classId === dbcId);
}
export function coaRankForLevel(
chain: CoaProgressionChain,
playerLevel: number,
): CoaProgressionRank {
return [...chain.ranks]
.filter((rank) => rank.level <= playerLevel)
.sort((left, right) => right.rank - left.rank)[0]
?? chain.ranks[0];
}
export function coaEntryRank(
entry: CoaTreeEntry,
rank: number,
): CoaRankDescription | null {
const normalized = Math.max(1, Math.min(entry.maxPoints, Math.trunc(rank)));
return entry.rankDescriptions.find((description) => description.rank === normalized)
?? entry.rankDescriptions[normalized - 1]
?? null;
}
export function coaIconUrl(icon: string): string {
return `/assets/ui/spells/${(icon || "inv_misc_questionmark").toLowerCase()}.png`;
}