// Rôle : fournit l'outil de planification d'équipements, sertissages, résumé et échange texte. import { useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Icon } from "../../../components/Icon.jsx"; import { Tooltip } from "../../../components/Tooltip.jsx"; import { applyGroupedReorderOperation, moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; const SOCKET_SHAPES = ["ball", "jewel"]; const SOCKET_COLORS = ["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"]; const SOCKET_COLOR_VALUES = { red: "#ef4444", orange: "#f97316", amber: "#f59e0b", yellow: "var(--color-accent-gold)", lime: "#84cc16", green: "#22c55e", teal: "#14b8a6", cyan: "var(--color-accent-cyan)", blue: "#3b82f6", indigo: "var(--color-accent-indigo)", violet: "#7c3aed", purple: "var(--color-primary)", pink: "var(--color-accent-pink)", rose: "#f43f5e", white: "#f8fafc", gray: "#64748b", black: "#020617" }; const EQUIPMENT_ICONS = [ { icon: "helmet", label: "Casque" }, { icon: "chest-armor", label: "Chest" }, { icon: "glove", label: "Gants" }, { icon: "boot", label: "Bottes" }, { icon: "necklace", label: "Collier" }, { icon: "earring", label: "Earring" }, { icon: "ring", label: "Anneau" }, { icon: "sword", label: "Épée" }, { icon: "gun", label: "Gun" }, { icon: "shield", label: "Bouclier" } ]; const TRAIT_ICONS = ["sword", "shield"]; const DETAIL_TABS = ["stats", "sockets", "craft"]; function getEquipmentIconLabel(icon) { return EQUIPMENT_ICONS.find((item) => item.icon === icon)?.label || "Équipement"; } function getCharacteristicCategories(equipment) { const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean); return [ ...equipment.categoryOrder.filter((category) => categories.includes(category)), ...categories.filter((category, index) => categories.indexOf(category) === index && !equipment.categoryOrder.includes(category)) ]; } function setItemCategory(item, category) { const nextItem = { ...item }; if (category) nextItem.category = category; else delete nextItem.category; return nextItem; } function formatValue(value) { if (typeof value === "number") return Number.isInteger(value) ? String(value) : String(Math.round(value * 1000) / 1000); return String(value || ""); } function createLocalId(prefix) { return globalThis.crypto?.randomUUID?.() || `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function getSocketItemName(equipment, socketItemId) { return equipment.socketItems.find((socketItem) => socketItem.id === socketItemId)?.name || ""; } function SocketShapeIcon({ shape, color = "gray", className = "" }) { return ( ); } function SummaryTotalIcon({ total }) { if (SOCKET_SHAPES.includes(total.icon)) return ; return ; } export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing }) { const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] })); const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]); const textContent = context.moduleText?.equipmentPlanner || {}; const [equipmentIcon, setEquipmentIcon] = useState("helmet"); const [equipmentName, setEquipmentName] = useState(""); const { itemReorder, groupReorder, getItemProps, getGroupProps, getGroupBoundaryProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, isBoundaryDropTarget, getDropPlacement, shouldShowGroupBoundaries } = useGroupedReorder({ namespace: `equipment-planner-${moduleId}`, items: data.equipments, getItemId: (equipment) => equipment.id, onItemMove: (operation) => save({ ...data, equipments: moveItem(data.equipments, operation.sourceId, operation.targetId, operation.placement) }) }); const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, getGroupBoundaryProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, isBoundaryDropTarget, getDropPlacement, shouldShowGroupBoundaries }; function save(nextData) { context.setModuleData(toolboxId, moduleId, nextData, "equipmentPlanner"); } function addEquipment(event) { event.preventDefault(); const cleanName = equipmentName.trim(); if (!cleanName) return; const type = data.types[0] || { id: context.uid("equipmentType"), title: "Équipements", icon: "shield", collapsed: false }; const nextData = { ...data, types: data.types.length ? data.types : [type], typeOrder: data.typeOrder.length ? data.typeOrder : [type.id], equipments: [ ...data.equipments, { id: context.uid("equipment"), typeId: type.id, name: cleanName, icon: equipmentIcon, active: true, obtain: "", characteristics: [], socketItems: [], socketLinks: [], materials: [], categoryOrder: [], collapsedCategories: [] } ] }; save(nextData); setEquipmentName(""); } function updateEquipment(equipmentId, updater) { save({ ...data, equipments: data.equipments.map((equipment) => equipment.id === equipmentId ? updater(equipment) : equipment) }); } function deleteEquipment(equipmentId) { const nextEquipments = data.equipments.filter((equipment) => equipment.id !== equipmentId); const usedTypeIds = new Set(nextEquipments.map((equipment) => equipment.typeId)); save({ ...data, equipments: nextEquipments, types: data.types.filter((type) => usedTypeIds.has(type.id)), typeOrder: data.typeOrder.filter((typeId) => usedTypeIds.has(typeId)) }); } return (
{editing && (
setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} /> save(nextData)} />
)}
    {data.equipments.length ? data.equipments.map((equipment) => ( updateEquipment(equipment.id, updater)} onDelete={() => deleteEquipment(equipment.id)} /> )) : (
  • {textContent.emptyEquipments || "Aucun équipement planifié."}
  • )}
); } function EquipmentPlannerSummary({ summary, textContent }) { const hasSourceTotals = summary.equipmentTotals.length || summary.socketTotals.length; const sourceColumnCount = [summary.equipmentTotals.length, summary.socketTotals.length].filter(Boolean).length; return (

{textContent.summaryTitle || "Résumé"}

{summary.activeEquipmentCount} {summary.activeEquipmentCount > 1 ? textContent.activeEquipmentsPlural || "équipements actifs" : textContent.activeEquipmentsSingular || "équipement actif"}
{hasSourceTotals ? (
{summary.equipmentTotals.length ? : null} {summary.socketTotals.length ? : null}
) : null} {summary.mixedTotals.length ? : null} {!summary.totals.length ? (
{textContent.numericTotalsTitle || "Totaux"}

{textContent.emptyTotals || "Aucun total numérique."}

) : null} {summary.textBonuses.length ? (
{textContent.textBonusesTitle || "Bonus"}
    {summary.textBonuses.map((bonus) => (
  • {bonus.category ? `${bonus.category} · ` : ""}{bonus.name} {bonus.source}{bonus.value ? ` · ${bonus.value}` : ""}
  • ))}
) : null}
); } function SummaryTotalsPanel({ title, totals, textContent }) { return (
{title}
    {totals.map((total) => ( } key={total.name} position="top-start" > {total.name} {formatValue(total.value)} ))}
); } function SummaryTotalTooltip({ total, textContent }) { return ( {textContent.summaryTooltipTitle || "Provenance"} {(total.contributions || []).map((contribution) => ( {contribution.sourceName || contribution.equipmentName} {formatValue(contribution.value)} ))} ); } function IconChoiceDropdown({ className = "", label, value, options, onChange }) { const selected = options.find((option) => option.icon === value) || options[0]; return ( ( )} > {({ close }) => (
{options.map((option) => ( ))}
)}
); } function ShapeChoiceDropdown({ label, value, onChange }) { return ( ( )} > {({ close }) => (
{SOCKET_SHAPES.map((shape) => ( ))}
)}
); } function ColorChoiceDropdown({ label, value, onChange }) { return ( ( )} > {({ close }) => (
{SOCKET_COLORS.map((color) => ( ))}
)}
); } function FloatingPicker({ className = "", label, columns = 5, renderTrigger, children }) { const [open, setOpen] = useState(false); const [menuStyle, setMenuStyle] = useState({}); const rootRef = useRef(null); const menuRef = useRef(null); useEffect(() => { if (!open) return undefined; function placeMenu() { const trigger = rootRef.current; if (!trigger) return; const rect = trigger.getBoundingClientRect(); const width = Math.max(0, Math.min(220, window.innerWidth - 16)); const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - width - 8)); setMenuStyle({ top: `${Math.min(rect.bottom + 6, window.innerHeight - 8)}px`, left: `${left}px`, maxWidth: `${width}px` }); } function handlePointerDown(event) { if (rootRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return; setOpen(false); } function handleKeyDown(event) { if (event.key === "Escape") setOpen(false); } placeMenu(); window.addEventListener("resize", placeMenu); window.addEventListener("scroll", placeMenu, true); document.addEventListener("pointerdown", handlePointerDown); document.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("resize", placeMenu); window.removeEventListener("scroll", placeMenu, true); document.removeEventListener("pointerdown", handlePointerDown); document.removeEventListener("keydown", handleKeyDown); }; }, [open]); function close() { setOpen(false); } function toggle() { setOpen((value) => !value); } return (
{renderTrigger({ open, toggle, close })} {open && createPortal(
{children({ close })}
, document.body )}
); } function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDelete }) { const [activeTab, setActiveTab] = useState("stats"); const [detailEditing, setDetailEditing] = useState(false); const nameEdit = useInlineEdit({ value: equipment.name, onCommit: (name) => onUpdate((current) => ({ ...current, name: name || current.name })), blurOnEscape: true }); const obtainEdit = useInlineEdit({ value: equipment.obtain || "", onCommit: (obtain) => onUpdate((current) => ({ ...current, obtain })), commitOnEnter: false }); const className = [ "equipment-planner-item", equipment.active ? "is-active" : "is-inactive", reorder.isItemDragging(equipment.id) ? "is-dragging" : "", reorder.isItemDropTarget(equipment.id) ? "is-drop-target" : "", reorder.getDropPlacement("item", equipment.id) === "after" ? "drop-after" : "" ].filter(Boolean).join(" "); return (
  • {equipment.name}
    {detailEditing ? ( <>
    {DETAIL_TABS.map((tab) => ( ))}
    {activeTab === "stats" && } {activeTab === "sockets" && } {activeTab === "craft" && (