184 lines
6.4 KiB
JavaScript
184 lines
6.4 KiB
JavaScript
/**
|
||
* 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 l’ancienne 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
|
||
};
|