Files
2026-07-21 16:50:58 +02:00

184 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Sprites ingrédients — parsing de `ingredient-sprites.bentext` et résolution
* alias → coordonnées pixel sur la planche. Pure, sans effet de bord.
*
* Format de `ingredient-sprites.bentext` :
* --- (séparateur de blocs)
* <row> <col> (coordonnées dans la grille, taille de cellule = INGREDIENT_SPRITE_CELL)
* alias 1 | alias 2 (synonymes séparés par `|`, lignes supplémentaires acceptées)
* ---
* ...
*
* Sortie de `parseIngredientSpritesBentext` :
* - byAlias : { [aliasLower]: { x, y, X, y: y } }
* - preferredAliases : Set<aliasLower> (alias des lignes « prioritaires »)
* - csv : rendu CSV (compat / rétro pour les outils existants)
*/
export const INGREDIENT_SPRITE_CELL = 32;
/** Lignes de tuiles « prioritaires » pour la carte variante (aligné sur lancienne grille CSV). */
export const PREFERRED_VARIANT_SPRITE_ROWS = new Set([8, 9, 10, 12, 13, 14, 15, 16]);
const normalizeIngredientAliasKey = (s) => s.trim().toLowerCase().replace(/\s+/g, " ");
const foldAccents = (s) => s.normalize("NFD").replace(/\p{M}+/gu, "").toLowerCase();
/**
* Parse `ingredient-sprites.bentext` : blocs `---` puis ligne `row col`, puis alias (`|` = synonymes).
*/
const parseIngredientSpritesBentext = (raw) => {
const byAlias = {};
const preferredAliases = new Set();
const grid = new Map();
const chunks = raw
.replace(/^\uFEFF/, "")
.trim()
.split(/\r?\n---\r?\n/);
for (const chunk of chunks) {
const trimmedChunk = chunk.trim();
if (!trimmedChunk) continue;
const lines = trimmedChunk
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0);
if (lines.length < 2) continue;
const coordMatch = lines[0].match(/^(\d+)\s+(\d+)/);
if (!coordMatch) {
if (lines[0] === "---") continue;
console.warn(`[build-recipes] Bloc sprite ignoré (coords invalides): ${lines[0].slice(0, 48)}`);
continue;
}
const row = Number(coordMatch[1]);
const col = Number(coordMatch[2]);
const x = col * INGREDIENT_SPRITE_CELL;
const y = row * INGREDIENT_SPRITE_CELL;
const entry = { x, y, X: x, Y: y };
const aliasLines = lines.slice(1);
const cellText = aliasLines.join("|");
const ck = `${row},${col}`;
grid.set(ck, grid.has(ck) ? `${grid.get(ck)}|${cellText}` : cellText);
const isPreferredRow = PREFERRED_VARIANT_SPRITE_ROWS.has(row);
for (const line of aliasLines) {
for (const part of line.split("|")) {
const key = normalizeIngredientAliasKey(part);
if (!key) continue;
const prev = byAlias[key];
if (!prev) {
byAlias[key] = entry;
} else if (prev.x !== x || prev.y !== y) {
console.warn(
`[build-recipes] Alias sprite « ${key} » en conflit (${prev.x},${prev.y}) vs (${x},${y}) — première définition conservée.`
);
continue;
}
if (isPreferredRow) preferredAliases.add(key);
}
}
}
let maxR = 0;
let maxC = 0;
for (const k of grid.keys()) {
const [r, c] = k.split(",").map(Number);
maxR = Math.max(maxR, r);
maxC = Math.max(maxC, c);
}
const csvLines = [];
for (let r = 0; r <= maxR; r++) {
const cols = [];
for (let c = 0; c <= maxC; c++) {
cols.push(grid.get(`${r},${c}`) ?? "");
}
csvLines.push(cols.join(","));
}
const csv = `${csvLines.join("\n")}\n`;
return { byAlias, preferredAliases, csv };
};
const resolveIconPxForIngredientName = (name, byAlias) => {
if (!name || typeof name !== "string") return null;
const n = normalizeIngredientAliasKey(name);
if (byAlias[n]) return aliasToIconPx(byAlias, n);
const nFold = foldAccents(n);
for (const key of Object.keys(byAlias)) {
if (foldAccents(key) === nFold) return aliasToIconPx(byAlias, key);
}
const segments = n.split(/[,;]|(?:\s+\/\s+)|(?:\s+et\s+)/i).map((s) => s.trim()).filter(Boolean);
for (const seg of segments) {
if (byAlias[seg]) return aliasToIconPx(byAlias, seg);
const sf = foldAccents(seg);
for (const key of Object.keys(byAlias)) {
if (foldAccents(key) === sf) return aliasToIconPx(byAlias, key);
}
}
const keysSorted = Object.keys(byAlias).sort((a, b) => b.length - a.length);
for (const k of keysSorted) {
if (k.length < 3) continue;
if (n.includes(k) || nFold.includes(foldAccents(k))) return aliasToIconPx(byAlias, k);
}
return null;
};
/** Lit les coordonnées pixel dans `byAlias` (API : `X`/`Y` ou `x`/`y`). */
const aliasToIconPx = (byAlias, alias) => {
if (!byAlias || typeof alias !== "string") return null;
const entry = byAlias[alias];
if (!entry || typeof entry !== "object") return null;
const x = typeof entry.x === "number" ? entry.x : typeof entry.X === "number" ? entry.X : NaN;
const y = typeof entry.y === "number" ? entry.y : typeof entry.Y === "number" ? entry.Y : NaN;
if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
return { x, y };
};
/** Alias manifeste correspondant à des coordonnées exactes sur la planche (pour filtrer les tuiles). */
const findAliasForIconPx = (byAlias, px) => {
if (!byAlias || !px || typeof px.x !== "number" || typeof px.y !== "number") return null;
for (const [alias, entry] of Object.entries(byAlias)) {
if (!entry || typeof entry !== "object") continue;
const x = typeof entry.x === "number" ? entry.x : typeof entry.X === "number" ? entry.X : NaN;
const y = typeof entry.y === "number" ? entry.y : typeof entry.Y === "number" ? entry.Y : NaN;
if (px.x === x && px.y === y) return alias;
}
return null;
};
const isPreferredVariantAlias = (preferredSet, manifestAlias) => {
if (!manifestAlias || preferredSet.size === 0) return false;
const lo = manifestAlias.trim().toLowerCase();
if (preferredSet.has(lo)) return true;
return [...preferredSet].some((p) => p.toLowerCase() === lo);
};
const buildIngredientCoverSprite = (iconPx, sheet) => {
if (!iconPx || typeof iconPx.x !== "number" || typeof iconPx.y !== "number" || !sheet) {
return null;
}
const col = Math.round(iconPx.x / INGREDIENT_SPRITE_CELL);
const row = Math.round(iconPx.y / INGREDIENT_SPRITE_CELL);
return {
sheet: "/ingredients.png",
col,
row,
cell: INGREDIENT_SPRITE_CELL,
sheetWidth: sheet.width,
sheetHeight: sheet.height
};
};
export {
parseIngredientSpritesBentext,
resolveIconPxForIngredientName,
aliasToIconPx,
findAliasForIconPx,
isPreferredVariantAlias,
buildIngredientCoverSprite,
normalizeIngredientAliasKey,
foldAccents
};