345 lines
14 KiB
JavaScript
345 lines
14 KiB
JavaScript
// Rôle : normalise, résume et compacte les données du planificateur d'équipements.
|
|
import { parsePositiveInt, uid } from "./shared.js";
|
|
|
|
const EQUIPMENT_SOCKET_SHAPES = new Set(["ball", "jewel"]);
|
|
const EQUIPMENT_SOCKET_COLORS = new Set(["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"]);
|
|
const EQUIPMENT_TYPE_ICONS = new Set(["boot", "chest-armor", "helmet", "glove", "shield", "sword", "gun", "earring", "necklace", "ring"]);
|
|
const EQUIPMENT_TRAIT_ICONS = new Set(["sword", "shield"]);
|
|
|
|
function normalizeEquipmentPlannerType(type) {
|
|
const title = String(type?.title || "").trim().slice(0, 80);
|
|
if (!title) return null;
|
|
const icon = EQUIPMENT_TYPE_ICONS.has(type?.icon) ? type.icon : "shield";
|
|
return {
|
|
id: type?.id || uid("equipmentType"),
|
|
title,
|
|
icon,
|
|
collapsed: type?.collapsed === true
|
|
};
|
|
}
|
|
|
|
function normalizeEquipmentPlannerValue(value) {
|
|
if (typeof value === "number") return Number.isFinite(value) ? Math.round(value * 1000) / 1000 : "";
|
|
const text = String(value ?? "").trim().slice(0, 80);
|
|
if (!text) return "";
|
|
const numeric = Number(text.replace(",", "."));
|
|
return Number.isFinite(numeric) && /^[-+]?\d+(?:[,.]\d+)?$/.test(text) ? Math.round(numeric * 1000) / 1000 : text;
|
|
}
|
|
|
|
function normalizeEquipmentPlannerTrait(trait, prefix = "trait") {
|
|
const name = String(trait?.name || "").trim().slice(0, 80);
|
|
if (!name) return null;
|
|
return {
|
|
id: trait?.id || uid(prefix),
|
|
category: String(trait?.category || "").trim().slice(0, 80),
|
|
icon: EQUIPMENT_TRAIT_ICONS.has(trait?.icon) ? trait.icon : "sword",
|
|
name,
|
|
value: normalizeEquipmentPlannerValue(trait?.value)
|
|
};
|
|
}
|
|
|
|
function normalizeEquipmentPlannerMaterial(material) {
|
|
const name = String(material?.name || "").trim().slice(0, 80);
|
|
if (!name) return null;
|
|
return {
|
|
id: material?.id || uid("material"),
|
|
name,
|
|
qty: Math.max(1, parsePositiveInt(material?.qty, 1))
|
|
};
|
|
}
|
|
|
|
function normalizeEquipmentPlannerSocketItem(socketItem) {
|
|
const name = String(socketItem?.name || "").trim().slice(0, 80);
|
|
if (!name) return null;
|
|
const shape = EQUIPMENT_SOCKET_SHAPES.has(socketItem?.shape) ? socketItem.shape : "jewel";
|
|
const color = EQUIPMENT_SOCKET_COLORS.has(socketItem?.color) ? socketItem.color : "yellow";
|
|
return {
|
|
id: socketItem?.id || uid("socketItem"),
|
|
name,
|
|
shape,
|
|
color,
|
|
bonuses: (Array.isArray(socketItem?.bonuses) ? socketItem.bonuses : [])
|
|
.map((bonus) => normalizeEquipmentPlannerTrait(bonus, "socketBonus"))
|
|
.filter(Boolean)
|
|
};
|
|
}
|
|
|
|
function normalizeEquipmentPlannerCategoryOrder(value, categories) {
|
|
const available = new Set(categories.filter(Boolean));
|
|
const ordered = [];
|
|
(Array.isArray(value) ? value : []).forEach((category) => {
|
|
const normalized = String(category || "").trim().slice(0, 80);
|
|
if (normalized && available.has(normalized) && !ordered.includes(normalized)) ordered.push(normalized);
|
|
});
|
|
categories.forEach((category) => {
|
|
if (category && !ordered.includes(category)) ordered.push(category);
|
|
});
|
|
return ordered;
|
|
}
|
|
|
|
function normalizeEquipmentPlannerEquipment(equipment, typeIds) {
|
|
const name = String(equipment?.name || "").trim().slice(0, 100);
|
|
const typeId = String(equipment?.typeId || "");
|
|
if (!name || !typeIds.has(typeId)) return null;
|
|
const characteristics = (Array.isArray(equipment?.characteristics) ? equipment.characteristics : [])
|
|
.map((trait) => normalizeEquipmentPlannerTrait(trait, "trait"))
|
|
.filter(Boolean);
|
|
const socketItems = (Array.isArray(equipment?.socketItems) ? equipment.socketItems : [])
|
|
.map(normalizeEquipmentPlannerSocketItem)
|
|
.filter(Boolean);
|
|
const socketItemIds = new Set(socketItems.map((socketItem) => socketItem.id));
|
|
const socketLinks = (Array.isArray(equipment?.socketLinks) ? equipment.socketLinks : [])
|
|
.map((link) => {
|
|
const fromSocketItemId = String(link?.fromSocketItemId || "");
|
|
const toSocketItemId = String(link?.toSocketItemId || "");
|
|
if (!socketItemIds.has(fromSocketItemId) || !socketItemIds.has(toSocketItemId) || fromSocketItemId === toSocketItemId) return null;
|
|
const pairKey = [fromSocketItemId, toSocketItemId].sort().join(":");
|
|
return {
|
|
id: link?.id || uid("socketLink"),
|
|
fromSocketItemId,
|
|
toSocketItemId,
|
|
pairKey
|
|
};
|
|
})
|
|
.filter(Boolean)
|
|
.filter((link, index, links) => links.findIndex((item) => item.pairKey === link.pairKey) === index)
|
|
.map(({ pairKey: _pairKey, ...link }) => link);
|
|
const categories = [
|
|
...characteristics.map((trait) => trait.category),
|
|
...socketItems.flatMap((socketItem) => socketItem.bonuses.map((bonus) => bonus.category))
|
|
].filter(Boolean);
|
|
const categoryOrder = normalizeEquipmentPlannerCategoryOrder(equipment?.categoryOrder, categories);
|
|
return {
|
|
id: equipment?.id || uid("equipment"),
|
|
typeId,
|
|
name,
|
|
icon: EQUIPMENT_TYPE_ICONS.has(equipment?.icon) ? equipment.icon : "shield",
|
|
active: equipment?.active !== false,
|
|
obtain: String(equipment?.obtain || "").trim().slice(0, 500),
|
|
characteristics,
|
|
socketItems,
|
|
socketLinks,
|
|
materials: (Array.isArray(equipment?.materials) ? equipment.materials : [])
|
|
.map(normalizeEquipmentPlannerMaterial)
|
|
.filter(Boolean),
|
|
categoryOrder,
|
|
collapsedCategories: (Array.isArray(equipment?.collapsedCategories) ? equipment.collapsedCategories : [])
|
|
.map((category) => String(category || "").trim().slice(0, 80))
|
|
.filter((category, index, collapsedCategories) => category && categoryOrder.includes(category) && collapsedCategories.indexOf(category) === index)
|
|
};
|
|
}
|
|
|
|
export function normalizeEquipmentPlannerData(data) {
|
|
const rawTypes = (Array.isArray(data?.types) ? data.types : []).map(normalizeEquipmentPlannerType).filter(Boolean);
|
|
const typeIds = new Set(rawTypes.map((type) => type.id));
|
|
const equipments = (Array.isArray(data?.equipments) ? data.equipments : [])
|
|
.map((equipment) => normalizeEquipmentPlannerEquipment(equipment, typeIds))
|
|
.filter(Boolean);
|
|
const usedTypeIds = new Set(equipments.map((equipment) => equipment.typeId));
|
|
const types = rawTypes.filter((type) => usedTypeIds.has(type.id));
|
|
const orderedTypeIds = (Array.isArray(data?.typeOrder) ? data.typeOrder : [])
|
|
.map((typeId) => String(typeId || ""))
|
|
.filter((typeId, index, order) => usedTypeIds.has(typeId) && order.indexOf(typeId) === index);
|
|
return {
|
|
types,
|
|
equipments,
|
|
typeOrder: [
|
|
...orderedTypeIds,
|
|
...types.map((type) => type.id).filter((typeId) => !orderedTypeIds.includes(typeId))
|
|
]
|
|
};
|
|
}
|
|
|
|
export function summarizeEquipmentPlannerData(data) {
|
|
const normalized = normalizeEquipmentPlannerData(data);
|
|
const totals = new Map();
|
|
const textBonuses = [];
|
|
const activeEquipments = normalized.equipments.filter((equipment) => equipment.active);
|
|
|
|
function incrementCount(counts, key) {
|
|
counts.set(key, (counts.get(key) || 0) + 1);
|
|
}
|
|
|
|
function pickMostUsedKey(counts) {
|
|
return [...counts.entries()].reduce((picked, [key, count]) => {
|
|
if (!picked) return { key, count };
|
|
if (count !== picked.count) return count > picked.count ? { key, count } : picked;
|
|
return key.localeCompare(picked.key) < 0 ? { key, count } : picked;
|
|
}, null);
|
|
}
|
|
|
|
function parseIconKey(key) {
|
|
const [icon, color = ""] = String(key || "").split(":");
|
|
return { icon: icon || "sword", color };
|
|
}
|
|
|
|
function pickSummaryIcon(iconSources) {
|
|
return parseIconKey(pickMostUsedKey(iconSources.direct)?.key || pickMostUsedKey(iconSources.socket)?.key || "sword:");
|
|
}
|
|
|
|
function getSummarySourceKind(sourceKinds) {
|
|
if (sourceKinds.size !== 1) return "mixed";
|
|
return sourceKinds.has("direct") ? "direct" : "socket";
|
|
}
|
|
|
|
function collectEntry(entry, contribution, icon, sourceKind, color = "") {
|
|
if (typeof entry.value === "number") {
|
|
const existing = totals.get(entry.name);
|
|
const iconKey = icon || "sword";
|
|
const colorKey = sourceKind === "socket" ? color || "gray" : "";
|
|
const iconSources = existing?.iconSources || { direct: new Map(), socket: new Map() };
|
|
const sourceKinds = existing?.sourceKinds || new Set();
|
|
const contributions = existing?.contributions || [];
|
|
sourceKinds.add(sourceKind);
|
|
incrementCount(iconSources[sourceKind], `${iconKey}:${colorKey}`);
|
|
const pickedIcon = pickSummaryIcon(iconSources);
|
|
totals.set(entry.name, {
|
|
name: entry.name,
|
|
icon: pickedIcon.icon,
|
|
color: pickedIcon.color,
|
|
sourceKind: getSummarySourceKind(sourceKinds),
|
|
iconSources,
|
|
sourceKinds,
|
|
contributions: [
|
|
...contributions,
|
|
{
|
|
id: entry.id,
|
|
sourceKind,
|
|
equipmentIcon: contribution.equipmentIcon,
|
|
equipmentName: contribution.equipmentName,
|
|
sourceName: contribution.sourceName,
|
|
value: entry.value
|
|
}
|
|
],
|
|
value: (existing?.value || 0) + entry.value
|
|
});
|
|
return;
|
|
}
|
|
textBonuses.push({
|
|
id: entry.id,
|
|
category: entry.category,
|
|
name: entry.name,
|
|
value: entry.value,
|
|
source: contribution.sourceName
|
|
});
|
|
}
|
|
|
|
activeEquipments.forEach((equipment) => {
|
|
equipment.characteristics.forEach((trait) => collectEntry(
|
|
trait,
|
|
{ equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: equipment.name },
|
|
trait.icon || "sword",
|
|
"direct"
|
|
));
|
|
equipment.socketItems.forEach((socketItem) => {
|
|
socketItem.bonuses.forEach((bonus) => collectEntry(
|
|
bonus,
|
|
{ equipmentIcon: equipment.icon || "shield", equipmentName: equipment.name, sourceName: `${equipment.name} · ${socketItem.name}` },
|
|
socketItem.shape || "jewel",
|
|
"socket",
|
|
socketItem.color
|
|
));
|
|
});
|
|
});
|
|
|
|
const numericTotals = [...totals.values()]
|
|
.map(({ iconSources: _iconSources, sourceKinds: _sourceKinds, color, sourceKind, ...total }) => ({
|
|
...total,
|
|
...(color ? { color } : {}),
|
|
sourceKind
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
return {
|
|
activeEquipmentCount: activeEquipments.length,
|
|
totals: numericTotals,
|
|
equipmentTotals: numericTotals.filter((total) => total.sourceKind === "direct"),
|
|
socketTotals: numericTotals.filter((total) => total.sourceKind === "socket"),
|
|
mixedTotals: numericTotals.filter((total) => total.sourceKind === "mixed"),
|
|
textBonuses,
|
|
socketItems: activeEquipments.flatMap((equipment) => equipment.socketItems.map((socketItem) => ({
|
|
...socketItem,
|
|
equipmentId: equipment.id,
|
|
equipmentName: equipment.name
|
|
}))),
|
|
socketLinks: activeEquipments.flatMap((equipment) => equipment.socketLinks.map((link) => ({
|
|
...link,
|
|
equipmentId: equipment.id,
|
|
equipmentName: equipment.name
|
|
})))
|
|
};
|
|
}
|
|
|
|
function compactEquipmentPlannerTraitForStorage(trait, prefix = "trait") {
|
|
const normalized = normalizeEquipmentPlannerTrait(trait, prefix);
|
|
if (!normalized) return null;
|
|
const compact = { id: normalized.id, name: normalized.name };
|
|
if (normalized.category) compact.category = normalized.category;
|
|
if (normalized.icon !== "sword") compact.icon = normalized.icon;
|
|
if (normalized.value !== "") compact.value = normalized.value;
|
|
return compact;
|
|
}
|
|
|
|
function compactEquipmentPlannerMaterialForStorage(material) {
|
|
const normalized = normalizeEquipmentPlannerMaterial(material);
|
|
return normalized ? normalized : null;
|
|
}
|
|
|
|
function compactEquipmentPlannerSocketItemForStorage(socketItem) {
|
|
const normalized = normalizeEquipmentPlannerSocketItem(socketItem);
|
|
if (!normalized) return null;
|
|
const compact = {
|
|
id: normalized.id,
|
|
name: normalized.name,
|
|
shape: normalized.shape,
|
|
color: normalized.color
|
|
};
|
|
if (normalized.bonuses.length) {
|
|
compact.bonuses = normalized.bonuses
|
|
.map((bonus) => compactEquipmentPlannerTraitForStorage(bonus, "socketBonus"))
|
|
.filter(Boolean);
|
|
}
|
|
return compact;
|
|
}
|
|
|
|
function compactEquipmentPlannerEquipmentForStorage(equipment) {
|
|
const compact = {
|
|
id: equipment.id,
|
|
typeId: equipment.typeId,
|
|
name: equipment.name
|
|
};
|
|
if (equipment.icon !== "shield") compact.icon = equipment.icon;
|
|
if (equipment.active === false) compact.active = false;
|
|
if (equipment.obtain) compact.obtain = equipment.obtain;
|
|
if (equipment.characteristics.length) {
|
|
compact.characteristics = equipment.characteristics
|
|
.map((trait) => compactEquipmentPlannerTraitForStorage(trait, "trait"))
|
|
.filter(Boolean);
|
|
}
|
|
if (equipment.socketItems.length) {
|
|
compact.socketItems = equipment.socketItems
|
|
.map(compactEquipmentPlannerSocketItemForStorage)
|
|
.filter(Boolean);
|
|
}
|
|
if (equipment.socketLinks.length) compact.socketLinks = equipment.socketLinks;
|
|
if (equipment.materials.length) compact.materials = equipment.materials.map(compactEquipmentPlannerMaterialForStorage).filter(Boolean);
|
|
if (equipment.categoryOrder.length) compact.categoryOrder = equipment.categoryOrder;
|
|
if (equipment.collapsedCategories.length) compact.collapsedCategories = equipment.collapsedCategories;
|
|
return compact;
|
|
}
|
|
|
|
export function compactEquipmentPlannerDataForStorage(value) {
|
|
const normalized = normalizeEquipmentPlannerData(value);
|
|
if (!normalized.equipments.length) return null;
|
|
const compact = {
|
|
types: normalized.types.map((type) => {
|
|
const compactType = { id: type.id, title: type.title };
|
|
if (type.icon !== "shield") compactType.icon = type.icon;
|
|
if (type.collapsed) compactType.collapsed = true;
|
|
return compactType;
|
|
}),
|
|
equipments: normalized.equipments.map(compactEquipmentPlannerEquipmentForStorage)
|
|
};
|
|
if (normalized.typeOrder.length) compact.typeOrder = normalized.typeOrder;
|
|
return compact;
|
|
}
|