Add missing src/lib files and fix .gitignore
- Add codec.ts, transform.ts, and types.ts to src/lib/ - Fix .gitignore to only ignore Python lib/ in root, not nested lib/ directories - Make pointsSpent optional in AscTree type to match JSON data structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -52,8 +52,8 @@ dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
/lib/
|
||||
/lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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<string, AscTree>; // 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
|
||||
};
|
||||
Reference in New Issue
Block a user