-
+ {socketItem.bonuses.map((bonus) => (
+
- + {bonus.category ? `${bonus.category} · ` : ""}{bonus.name} + {formatValue(bonus.value)} + {editing && ( + + )} + + ))} +
diff --git a/AGENTS.md b/AGENTS.md index 2c76df4..dc50913 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,6 +77,12 @@ Privilégier : Éviter les styles isolés qui ne réutilisent pas le thème. +## Serveur de développement + +Si le port `5173` est déjà occupé, considérer qu'un `npm run dev` est probablement déjà en cours. Ne pas lancer automatiquement un nouveau serveur sur `5174` dans ce cas. + +Quand une vérification navigateur est nécessaire après des modifications, utiliser l'instance déjà ouverte sur `http://localhost:5173` si elle répond. + ## Validation Avant de terminer une modification significative, lancer : diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 6d21d9e..a933ac3 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -165,6 +165,91 @@ Stockage compact : - `collapsed` est omis si `false`. - les sections sans item sont ignorées. +## Outil Planificateur D'équipements + +Type : `equipmentPlanner` + +```json +{ + "types": [ + { + "id": "type1", + "title": "Armes", + "icon": "sword", + "collapsed": false + } + ], + "equipments": [ + { + "id": "equipment1", + "typeId": "type1", + "name": "Épée runique", + "icon": "sword", + "active": true, + "obtain": "Forge après le boss", + "characteristics": [ + { + "id": "trait1", + "category": "Stats", + "icon": "sword", + "name": "Force", + "value": 12 + } + ], + "socketItems": [ + { + "id": "socket1", + "name": "Joyau attaque", + "shape": "jewel", + "color": "yellow", + "bonuses": [ + { + "id": "bonus1", + "category": "Compétences", + "name": "Attaque", + "value": 1 + } + ] + } + ], + "socketLinks": [ + { + "id": "link1", + "fromSocketItemId": "socket1", + "toSocketItemId": "socket2" + } + ], + "materials": [ + { + "id": "material1", + "name": "Minerai rare", + "qty": 4 + } + ], + "categoryOrder": ["Stats", "Compétences"], + "collapsedCategories": [] + } + ], + "typeOrder": ["type1"] +} +``` + +Stockage compact : + +- les types sans équipement sont ignorés ; +- `icon` des types vaut `boot`, `chest-armor`, `helmet`, `glove`, `shield`, `sword`, `gun`, `earring`, `necklace` ou `ring` ; +- `icon` des équipements utilise la même liste et pilote l'icône affichée sur la ligne de l'équipement ; +- `active` est omis quand la valeur vaut `true` ; +- `obtain`, `characteristics`, `socketItems`, `socketLinks`, `materials`, `categoryOrder` et `collapsedCategories` sont omis si vides ; +- `collapsed` est omis si `false` ; +- `qty` des matériaux est normalisé à minimum `1` ; +- les caractéristiques et bonus sans nom sont ignorés ; +- `icon` des caractéristiques directes vaut `sword` ou `shield` ; +- `shape` vaut `ball` ou `jewel`, avec `jewel` par défaut ; +- `color` vaut `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `teal`, `cyan`, `blue`, `indigo`, `violet`, `purple`, `pink`, `rose`, `white`, `gray` ou `black` ; +- les liens de sertissage invalides, dupliqués ou hors équipement sont ignorés ; +- seuls les équipements actifs contribuent au résumé. + ## Outil Images Type : `images` diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index af5eef2..a039c9b 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -332,6 +332,73 @@ export function validateSiteContent(site) { "toolboxes.modules.taskPlanner.prerequisiteCheckboxLabel", "toolboxes.modules.taskPlanner.removeRelationTitle", "toolboxes.modules.taskPlanner.emptyTasks", + "toolboxes.modules.equipmentPlanner.typePlaceholder", + "toolboxes.modules.equipmentPlanner.equipmentIconLabel", + "toolboxes.modules.equipmentPlanner.equipmentPlaceholder", + "toolboxes.modules.equipmentPlanner.addEquipmentButton", + "toolboxes.modules.equipmentPlanner.summaryTitle", + "toolboxes.modules.equipmentPlanner.activeEquipmentsSingular", + "toolboxes.modules.equipmentPlanner.activeEquipmentsPlural", + "toolboxes.modules.equipmentPlanner.numericTotalsTitle", + "toolboxes.modules.equipmentPlanner.equipmentTotalsTitle", + "toolboxes.modules.equipmentPlanner.socketTotalsTitle", + "toolboxes.modules.equipmentPlanner.mixedTotalsTitle", + "toolboxes.modules.equipmentPlanner.summaryTooltipTitle", + "toolboxes.modules.equipmentPlanner.textBonusesTitle", + "toolboxes.modules.equipmentPlanner.emptyTotals", + "toolboxes.modules.equipmentPlanner.emptyTextBonuses", + "toolboxes.modules.equipmentPlanner.emptyEquipments", + "toolboxes.modules.equipmentPlanner.activeLabel", + "toolboxes.modules.equipmentPlanner.includeInSummaryLabel", + "toolboxes.modules.equipmentPlanner.reorderTypeTitle", + "toolboxes.modules.equipmentPlanner.renameTypeTitle", + "toolboxes.modules.equipmentPlanner.showTypeTitle", + "toolboxes.modules.equipmentPlanner.hideTypeTitle", + "toolboxes.modules.equipmentPlanner.reorderEquipmentTitle", + "toolboxes.modules.equipmentPlanner.renameEquipmentTitle", + "toolboxes.modules.equipmentPlanner.showDetailsTitle", + "toolboxes.modules.equipmentPlanner.hideDetailsTitle", + "toolboxes.modules.equipmentPlanner.deleteEquipmentTitle", + "toolboxes.modules.equipmentPlanner.tabsLabel", + "toolboxes.modules.equipmentPlanner.statsTab", + "toolboxes.modules.equipmentPlanner.socketsTab", + "toolboxes.modules.equipmentPlanner.craftTab", + "toolboxes.modules.equipmentPlanner.displayStatsTitle", + "toolboxes.modules.equipmentPlanner.displaySocketTitle", + "toolboxes.modules.equipmentPlanner.editModeTitle", + "toolboxes.modules.equipmentPlanner.viewModeTitle", + "toolboxes.modules.equipmentPlanner.editModeButton", + "toolboxes.modules.equipmentPlanner.viewModeButton", + "toolboxes.modules.equipmentPlanner.obtainLabel", + "toolboxes.modules.equipmentPlanner.obtainPlaceholder", + "toolboxes.modules.equipmentPlanner.emptyObtain", + "toolboxes.modules.equipmentPlanner.characteristicsTitle", + "toolboxes.modules.equipmentPlanner.traitIconLabel", + "toolboxes.modules.equipmentPlanner.categoryPlaceholder", + "toolboxes.modules.equipmentPlanner.traitNamePlaceholder", + "toolboxes.modules.equipmentPlanner.traitValuePlaceholder", + "toolboxes.modules.equipmentPlanner.addTraitButton", + "toolboxes.modules.equipmentPlanner.reorderTraitTitle", + "toolboxes.modules.equipmentPlanner.reorderCategoryTitle", + "toolboxes.modules.equipmentPlanner.deleteTraitTitle", + "toolboxes.modules.equipmentPlanner.emptyCharacteristics", + "toolboxes.modules.equipmentPlanner.materialsTitle", + "toolboxes.modules.equipmentPlanner.materialNamePlaceholder", + "toolboxes.modules.equipmentPlanner.materialQtyLabel", + "toolboxes.modules.equipmentPlanner.addMaterialButton", + "toolboxes.modules.equipmentPlanner.deleteMaterialTitle", + "toolboxes.modules.equipmentPlanner.socketItemsTitle", + "toolboxes.modules.equipmentPlanner.emptySocketItems", + "toolboxes.modules.equipmentPlanner.socketNamePlaceholder", + "toolboxes.modules.equipmentPlanner.socketShapeLabel", + "toolboxes.modules.equipmentPlanner.socketColorLabel", + "toolboxes.modules.equipmentPlanner.addSocketButton", + "toolboxes.modules.equipmentPlanner.bonusNamePlaceholder", + "toolboxes.modules.equipmentPlanner.addBonusButton", + "toolboxes.modules.equipmentPlanner.deleteSocketTitle", + "toolboxes.modules.equipmentPlanner.deleteBonusTitle", + "toolboxes.modules.equipmentPlanner.addSocketLinkLabel", + "toolboxes.modules.equipmentPlanner.deleteSocketLinkTitle", "toolboxes.modules.imageAnnotation.addImage", "toolboxes.modules.imageAnnotation.replaceImage", "toolboxes.modules.imageAnnotation.pastePlaceholder", @@ -432,7 +499,7 @@ export function validateLibraryData(payload) { assert.equal(typeof payload.modules[module.id], "object", `library.modules.${module.id} must be an object`); }); - ["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "imageAnnotation"].forEach((type) => { + ["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => { assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`); }); @@ -451,6 +518,12 @@ export function validateLibraryData(payload) { assert.ok(Array.isArray(taskData.tasks), "library task planner example must contain tasks"); assert.ok(Array.isArray(taskData.relations), "library task planner example must contain relations"); assert.ok(taskData.relations.length >= 1, "library task planner example must include a parent/child relation"); + + const equipmentModule = payload.toolbox.modules.find((module) => module.type === "equipmentPlanner"); + const equipmentData = payload.modules[equipmentModule.id]; + assert.ok(Array.isArray(equipmentData.types), "library equipment planner example must contain types"); + assert.ok(Array.isArray(equipmentData.equipments), "library equipment planner example must contain equipments"); + assert.ok(equipmentData.equipments.some((equipment) => Array.isArray(equipment.socketItems) && equipment.socketItems.length), "library equipment planner example must include socket items"); } function assertSnakeCase(value, path) { diff --git a/tests/static-app.test.mjs b/tests/static-app.test.mjs index 23852fa..a9bef9a 100644 --- a/tests/static-app.test.mjs +++ b/tests/static-app.test.mjs @@ -66,6 +66,9 @@ test("vite entrypoint and app shell are wired", async () => { assert.match(libraryPage, /export function LibraryPage/); assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/); assert.match(libraryPage, /TaskPlannerModule/); + assert.match(libraryPage, /EquipmentPlannerModule/); + assert.match(libraryPage, /normalizeEquipmentPlannerData/); + assert.match(libraryPage, /summarizeEquipmentPlannerData/); assert.match(bodyScrollLock, /modalLocks/); assert.match(bodyScrollLock, /is-modal-open/); }); diff --git a/tests/static-styles.test.mjs b/tests/static-styles.test.mjs index c0a368e..6d88837 100644 --- a/tests/static-styles.test.mjs +++ b/tests/static-styles.test.mjs @@ -78,6 +78,7 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styleIcons, /ui-icon-grab/); assert.match(styleIcons, /ui-icon-shield/); assert.match(styleIcons, /ui-icon-gun/); + assert.match(styleToolboxes, /module-icon-chest-armor/); assert.match(styleToolboxes, /\.combo-hold-controls/); assert.match(styleToolboxes, /\.combo-input-direction/); assert.match(styleToolboxes, /\.combo-device-n64\.combo-value-stick-up/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index c06fc7b..74ba301 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -43,6 +43,8 @@ test("toolbox storage, cards and pages are wired", async () => { assert.match(toolboxStorage, /export function normalizeTableData/); assert.match(toolboxStorage, /export function normalizeTimerData/); assert.match(toolboxStorage, /export function normalizeTaskPlannerData/); + assert.match(toolboxStorage, /export function normalizeEquipmentPlannerData/); + assert.match(toolboxStorage, /export function summarizeEquipmentPlannerData/); assert.match(toolboxStorage, /export function normalizeImageAnnotationData/); assert.match(toolboxStorage, /export function compactModuleDataForStorage\(type, value\)/); assert.match(toolboxStorage, /scrollResults/); @@ -108,6 +110,7 @@ test("toolbox module registry and modules expose expected behavior", async () => const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); + const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8"); const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8"); const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8"); @@ -133,6 +136,8 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(moduleRegistry, /table: \{ label: "Tableau", icon: "table", Component: TableModule, editable: false, scrollable: true \}/); assert.match(moduleRegistry, /timer:/); assert.match(moduleRegistry, /taskPlanner:/); + assert.match(moduleRegistry, /equipmentPlanner:/); + assert.match(moduleRegistry, /Planificateur d'équipements/); assert.match(moduleRegistry, /imageAnnotation:/); assert.match(moduleRegistry, /Annotation d'images/); assert.match(moduleRegistry, /editable: false/); @@ -245,6 +250,13 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(taskPlannerModule, /reorderFeatures/); assert.doesNotMatch(taskPlannerModule, /canMoveItem:/); assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/); + assert.match(equipmentPlannerModule, /export function EquipmentPlannerModule/); + assert.match(equipmentPlannerModule, /normalizeEquipmentPlannerData/); + assert.match(equipmentPlannerModule, /summarizeEquipmentPlannerData/); + assert.match(equipmentPlannerModule, /socketItems/); + assert.match(equipmentPlannerModule, /socketLinks/); + assert.match(equipmentPlannerModule, /useGroupedReorder/); + assert.match(equipmentPlannerModule, /reorderFeatures/); assert.match(checklistModule, /export function ChecklistModule/); assert.match(checklistModule, /TextImportModal/); assert.match(checklistModule, /Icon name="import"/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index ecad531..0752728 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -4,7 +4,7 @@ import assert from "node:assert/strict"; import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js"; import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js"; import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js"; -import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; +import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeEquipmentPlannerData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js"; test("colon text import keeps urls intact after the first separator", () => { @@ -96,6 +96,228 @@ test("table storage clamps dimensions and compacts non-empty cells", () => { assert.equal(compactModuleDataForStorage("table", { rows: 10, columns: 6, cells: {} }), null); }); +test("equipment planner storage normalizes sockets, materials and links", () => { + const normalized = normalizeEquipmentPlannerData({ + types: [ + { id: "type1", title: "Armes" }, + { id: "empty", title: "Vide" } + ], + equipments: [ + { + id: "eq1", + typeId: "type1", + name: "Épée", + active: false, + characteristics: [ + { id: "trait1", category: "Stats", name: "Force", value: "12" }, + { id: "ignored", category: "Stats", name: "", value: 4 } + ], + materials: [ + { id: "mat1", name: "Minerai", qty: 0 } + ], + socketItems: [ + { + id: "socket1", + name: "Materia feu", + shape: "triangle", + color: "invalid", + bonuses: [ + { id: "bonus1", category: "Stats", name: "Force", value: "3" }, + { id: "bonus2", category: "Compétences", name: "", value: 1 } + ] + }, + { + id: "socket2", + name: "Materia soin", + shape: "ball", + color: "cyan", + bonuses: [ + { id: "bonus3", category: "Sorts", name: "Soin", value: "" } + ] + } + ], + socketLinks: [ + { id: "link1", fromSocketItemId: "socket1", toSocketItemId: "socket2" }, + { id: "duplicate", fromSocketItemId: "socket2", toSocketItemId: "socket1" }, + { id: "invalid", fromSocketItemId: "socket1", toSocketItemId: "missing" } + ], + categoryOrder: ["Compétences", "Stats", "Missing"], + collapsedCategories: ["Stats", "Missing"] + } + ], + typeOrder: ["empty", "type1"] + }); + + assert.deepEqual(normalized.types.map((type) => type.id), ["type1"]); + assert.equal(normalized.equipments[0].active, false); + assert.equal(normalized.equipments[0].materials[0].qty, 1); + assert.equal(normalized.equipments[0].characteristics.length, 1); + assert.equal(normalized.equipments[0].socketItems[0].shape, "jewel"); + assert.equal(normalized.equipments[0].socketItems[0].color, "yellow"); + assert.equal(normalized.equipments[0].socketItems[0].bonuses.length, 1); + assert.equal(normalized.equipments[0].socketLinks.length, 1); + assert.deepEqual(normalized.equipments[0].categoryOrder, ["Stats", "Sorts"]); + assert.deepEqual(normalized.equipments[0].collapsedCategories, ["Stats"]); + + const compact = compactModuleDataForStorage("equipmentPlanner", normalized); + assert.equal(compact.equipments[0].active, false); + assert.equal(compact.equipments[0].socketItems[0].shape, "jewel"); + assert.equal(compact.equipments[0].socketLinks.length, 1); + assert.equal(compactModuleDataForStorage("equipmentPlanner", { types: [{ id: "empty", title: "Vide" }], equipments: [] }), null); +}); + +test("equipment planner summary only uses active equipments", () => { + const summary = summarizeEquipmentPlannerData({ + types: [{ id: "type1", title: "Build" }], + equipments: [ + { + id: "eq1", + typeId: "type1", + name: "Casque", + active: true, + characteristics: [{ id: "trait1", category: "Stats", name: "Force", value: 10 }], + socketItems: [{ + id: "socket1", + name: "Joyau attaque", + shape: "ball", + color: "yellow", + bonuses: [ + { id: "bonus1", category: "Stats", name: "Force", value: 2 }, + { id: "bonus2", category: "Affixes", name: "Brutalité", value: "" } + ] + }] + }, + { + id: "eq2", + typeId: "type1", + name: "Armure", + active: false, + characteristics: [{ id: "trait2", category: "Stats", name: "Force", value: 99 }] + } + ] + }); + + assert.equal(summary.activeEquipmentCount, 1); + assert.deepEqual(summary.totals, [{ + name: "Force", + icon: "sword", + sourceKind: "mixed", + contributions: [ + { id: "trait1", sourceKind: "direct", equipmentIcon: "shield", equipmentName: "Casque", sourceName: "Casque", value: 10 }, + { id: "bonus1", sourceKind: "socket", equipmentIcon: "shield", equipmentName: "Casque", sourceName: "Casque · Joyau attaque", value: 2 } + ], + value: 12 + }]); + assert.deepEqual(summary.equipmentTotals, []); + assert.deepEqual(summary.socketTotals, []); + assert.deepEqual(summary.mixedTotals, summary.totals); + assert.equal(summary.textBonuses.length, 1); + assert.equal(summary.textBonuses[0].name, "Brutalité"); +}); + +test("equipment planner summary prioritizes direct stat icons over socket icons", () => { + const summary = summarizeEquipmentPlannerData({ + types: [{ id: "type1", title: "Build" }], + equipments: [ + { + id: "eq1", + typeId: "type1", + name: "Épée", + characteristics: [{ id: "trait1", category: "Stats", icon: "shield", name: "Attaque", value: 100 }], + socketItems: [{ + id: "socket1", + name: "Joyau rouge", + shape: "jewel", + color: "red", + bonuses: [ + { id: "bonus1", category: "Stats", name: "Attaque", value: 10 }, + { id: "bonus2", category: "Compétences", name: "Attaque de feu", value: 1 } + ] + }] + }, + { + id: "eq2", + typeId: "type1", + name: "Fusil", + characteristics: [ + { id: "trait2", category: "Stats", icon: "sword", name: "Défense", value: 10 }, + { id: "trait3", category: "Stats", icon: "sword", name: "Défense", value: 20 }, + { id: "trait4", category: "Stats", icon: "shield", name: "Défense", value: 30 } + ] + } + ] + }); + + assert.deepEqual(summary.totals.find((total) => total.name === "Attaque"), { + name: "Attaque", + icon: "shield", + sourceKind: "mixed", + contributions: [ + { id: "trait1", sourceKind: "direct", equipmentIcon: "shield", equipmentName: "Épée", sourceName: "Épée", value: 100 }, + { id: "bonus1", sourceKind: "socket", equipmentIcon: "shield", equipmentName: "Épée", sourceName: "Épée · Joyau rouge", value: 10 } + ], + value: 110 + }); + assert.deepEqual(summary.totals.find((total) => total.name === "Attaque de feu"), { + name: "Attaque de feu", + icon: "jewel", + color: "red", + sourceKind: "socket", + contributions: [{ id: "bonus2", sourceKind: "socket", equipmentIcon: "shield", equipmentName: "Épée", sourceName: "Épée · Joyau rouge", value: 1 }], + value: 1 + }); + assert.deepEqual(summary.totals.find((total) => total.name === "Défense"), { + name: "Défense", + icon: "sword", + sourceKind: "direct", + contributions: [ + { id: "trait2", sourceKind: "direct", equipmentIcon: "shield", equipmentName: "Fusil", sourceName: "Fusil", value: 10 }, + { id: "trait3", sourceKind: "direct", equipmentIcon: "shield", equipmentName: "Fusil", sourceName: "Fusil", value: 20 }, + { id: "trait4", sourceKind: "direct", equipmentIcon: "shield", equipmentName: "Fusil", sourceName: "Fusil", value: 30 } + ], + value: 60 + }); + assert.deepEqual(summary.equipmentTotals.map((total) => total.name), ["Défense"]); + assert.deepEqual(summary.socketTotals.map((total) => total.name), ["Attaque de feu"]); + assert.deepEqual(summary.mixedTotals.map((total) => total.name), ["Attaque"]); +}); + +test("equipment planner export remaps nested ids and socket links", () => { + const payload = createToolboxExportPayload( + { + id: "toolbox1", + name: "Build", + modules: [{ id: "module1", type: "equipmentPlanner" }] + }, + { + "toolbox1:module1": { + types: [{ id: "type1", title: "Build" }], + equipments: [{ + id: "eq1", + typeId: "type1", + name: "Casque", + characteristics: [{ id: "trait1", name: "Force", value: 1 }], + materials: [{ id: "mat1", name: "Minerai", qty: 2 }], + socketItems: [ + { id: "socket1", name: "Joyau attaque", shape: "ball", color: "yellow", bonuses: [{ id: "bonus1", name: "Attaque", value: 1 }] }, + { id: "socket2", name: "Joyau expert", shape: "jewel", color: "cyan", bonuses: [] } + ], + socketLinks: [{ id: "link1", fromSocketItemId: "socket1", toSocketItemId: "socket2" }] + }], + typeOrder: ["type1"] + } + } + ); + const moduleId = payload.toolbox.modules[0].id; + const exported = payload.modules[moduleId]; + + assert.notEqual(exported.types[0].id, "type1"); + assert.equal(exported.equipments[0].typeId, exported.types[0].id); + assert.notEqual(exported.equipments[0].socketItems[0].id, "socket1"); + assert.equal(exported.equipments[0].socketLinks[0].fromSocketItemId, exported.equipments[0].socketItems[0].id); + assert.equal(exported.equipments[0].socketLinks[0].toSocketItemId, exported.equipments[0].socketItems[1].id); +}); + test("grouped reorder helpers group flat items and complete group order", () => { const items = [ { id: "a", category: "" }, diff --git a/website/public/data/library.json b/website/public/data/library.json index 8a6a573..25a53c0 100644 --- a/website/public/data/library.json +++ b/website/public/data/library.json @@ -47,15 +47,20 @@ { "id": "mb", "type": "imageAnnotation" + }, + { + "id": "mc", + "type": "equipmentPlanner" } ], - "updatedAt": "2026-08-03T08:04:53.696Z" + "updatedAt": "2026-08-09T07:34:31.425Z", + "moduleColumns": 1 }, "modules": { "m1": { - "html": "
{textContent.emptyTotals || "Aucun total numérique."}
+{textContent.emptyEquipmentDetails || "Aucun détail renseigné."}
; + } + + return ( +{equipment.obtain}
: null} + {equipment.materials.length ? ( +{textContent.emptyCharacteristics || "Aucune caractéristique."}
} +