Add equipment planner toolbox module
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-08-09 10:03:55 +02:00
parent ac33a55c8b
commit d53d4160af
33 changed files with 3522 additions and 20 deletions

View file

@ -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: "" },