// 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; }