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>; 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>; 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>; 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`; }