diff --git a/.gitignore b/.gitignore index bbff391..17e657d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,8 +52,8 @@ dist/ downloads/ eggs/ .eggs/ -lib/ -lib64/ +/lib/ +/lib64/ parts/ sdist/ var/ diff --git a/talent-builder/src/lib/codec.ts b/talent-builder/src/lib/codec.ts new file mode 100644 index 0000000..a4b2887 --- /dev/null +++ b/talent-builder/src/lib/codec.ts @@ -0,0 +1,81 @@ +import type { GridSpec } from "./types"; + +// Build format: `${class}.${tree0}.${tree1}.${tree2}` +// Compact URL-safe format using base36: encode non-zero talents as "index_rank" pairs separated by underscores +// Example: "0_3_b_5" means slot 0 has rank 3, slot 11 (b in base36) has rank 5 +// For ranks: 1-9 are literal, 10+ use base36 (a=10, b=11, etc) +// For indices: use base36 notation + +const BASE36_CHARS = "0123456789abcdefghijklmnopqrstuvwxyz"; + +function encodeRank(rank: number): string { + if (rank < 10) return String(rank); + return BASE36_CHARS[rank] || "0"; +} + +function decodeRank(char: string): number { + const idx = BASE36_CHARS.indexOf(char.toLowerCase()); + return idx >= 0 ? idx : 0; +} + +export function encodeBuild(spec: GridSpec): string { + const parts = spec.trees.map((t) => { + const pairs: string[] = []; + t.slots.forEach((s, idx) => { + if (s.talent && s.talent.currentRank > 0) { + // Encode as index_rank using base36 for both + const idxStr = idx.toString(36); + const rankStr = encodeRank(s.talent.currentRank); + pairs.push(idxStr + rankStr); + } + }); + return pairs.join("_"); + }); + return `${spec.className}.${parts.join(".")}`; +} + +export function decodeBuild(spec: GridSpec, build: string): GridSpec { + const parts = build.split("."); + if (parts.length < 2) return spec; + + const cls = parts[0]; + const out = structuredClone(spec); + out.className = cls || spec.className; + + // Parse each tree (parts[1], parts[2], parts[3]) + out.trees.forEach((t, ti) => { + const treeStr = parts[ti + 1] || ""; + if (!treeStr) return; + + // Split by underscore to get index-rank pairs + const pairs = treeStr.split("_"); + for (const pair of pairs) { + if (pair.length < 2) continue; + + // Last character is rank, everything before is index + const rankChar = pair[pair.length - 1]; + const idxStr = pair.substring(0, pair.length - 1); + + const idx = parseInt(idxStr, 36); + const rank = decodeRank(rankChar); + + if (!Number.isFinite(idx) || !Number.isFinite(rank)) continue; + if (idx < 0 || idx >= t.slots.length) continue; + + const slot = t.slots[idx]; + if (slot.talent) { + slot.talent.currentRank = clamp(rank, 0, slot.talent.maxRank); + } + } + }); + return out; +} + +function clampDigit(n: number): number { + if (!Number.isFinite(n)) return 0; + return Math.max(0, Math.min(9, Math.trunc(n))); +} + +function clamp(n: number, min: number, max: number): number { + return Math.max(min, Math.min(max, n)); +} diff --git a/talent-builder/src/lib/transform.ts b/talent-builder/src/lib/transform.ts new file mode 100644 index 0000000..4a575d7 --- /dev/null +++ b/talent-builder/src/lib/transform.ts @@ -0,0 +1,66 @@ +import type { AscClassTalents, GridSpec, GridTree, GridSlot, AscTalent } from "./types"; + +function toIndex(tier: number, column: number): number { + return (tier - 1) * 4 + (column - 1); +} + +export function iconTextureToFilename(iconTexture: string): string { + // Input examples: + // - "Interface\\Icons\\Spell_Fire_SoulBurn" + // - "Interface/Icons/Spell_Fire_SoulBurn.blp" + // - " spell_fire_soulburn " + const normalized = (iconTexture || "").trim().replace(/\\/g, "/"); + const parts = normalized.split("/"); + let base = (parts[parts.length - 1] || normalized).trim(); + // Strip any extension if present + base = base.replace(/\.(blp|png|jpg|jpeg)$/i, ""); + const filename = base.toLowerCase() + ".png"; + + // Determine subdirectory based on filename prefix + // Icons are organized in: spells/, items/, professions/, classes/, talents/, ui/, misc/, raids/ + if (filename.startsWith("spell_") || filename.startsWith("ability_")) { + return "spells/" + filename; + } else if (filename.startsWith("inv_")) { + return "items/" + filename; + } else if (filename.startsWith("trade_")) { + return "professions/" + filename; + } else if (filename.startsWith("achievement_")) { + return "ui/" + filename; + } else { + // Default to spells for unknown prefixes + return "spells/" + filename; + } +} + +function buildEmptySlots(): GridSlot[] { + const slots: GridSlot[] = []; + for (let row = 0; row < 11; row++) { + for (let col = 0; col < 4; col++) { + const index = row * 4 + col; + slots.push({ index, row, col, talent: null }); + } + } + return slots; +} + +function placeTalents(slots: GridSlot[], talents: AscTalent[]): void { + for (const t of talents) { + const idx = toIndex(t.tier, t.column); + const slot = slots[idx]; + if (!slot) continue; + slot.talent = t; + } +} + +export function toGridSpec(className: string, data: AscClassTalents): GridSpec { + const trees: GridTree[] = Object.keys(data) + .sort((a, b) => (data[a].tabIndex ?? 0) - (data[b].tabIndex ?? 0)) + .map((name) => { + const tree = data[name]; + const slots = buildEmptySlots(); + placeTalents(slots, tree.talents); + return { name, slots } as GridTree; + }); + + return { className, trees }; +} diff --git a/talent-builder/src/lib/types.ts b/talent-builder/src/lib/types.ts new file mode 100644 index 0000000..713d869 --- /dev/null +++ b/talent-builder/src/lib/types.ts @@ -0,0 +1,36 @@ +export type AscTalent = { + name: string; + spellId: number; + iconTexture: string; // e.g., Interface\\Icons\\Spell_Fire_FlameBolt + tier: number; // 1..11 + column: number; // 1..4 + maxRank: number; + currentRank: number; + tooltip: string; + requires?: number | null; // optional: index or spellId of prerequisite +}; + +export type AscTree = { + tabIndex: number; + pointsSpent?: number; + talents: AscTalent[]; +}; + +export type AscClassTalents = Record; // keys: Fire/Frost/Arcane etc. + +export type GridSlot = { + index: number; // 0..43 in 4x11 + row: number; // 0..10 + col: number; // 0..3 + talent: AscTalent | null; +}; + +export type GridTree = { + name: string; // Fire/Frost/Arcane + slots: GridSlot[]; // 44 items +}; + +export type GridSpec = { + className: string; // mage, warrior, etc. + trees: GridTree[]; // length 3 +};