Advertisement
Guest User

Untitled

a guest
May 27th, 2025
2,335
1
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 61.59 KB | None | 1 0
  1. <!DOCTYPE html>
  2. <html lang="fr">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Visualiseur d'Arbre de Craft Interactif</title>
  7. <script src="https://d3js.org/d3.v7.min.js"></script>
  8. <style>
  9. body {
  10. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  11. margin: 0;
  12. padding: 0;
  13. background-color: #f4f7f6; /* Thème blanc cassé */
  14. color: #333; /* Texte principal sombre */
  15. overflow: hidden;
  16. display: flex;
  17. flex-direction: column;
  18. height: 100vh;
  19. }
  20.  
  21. .headbar {
  22. background-color: #ffffff; /* Header blanc */
  23. padding: 10px 20px;
  24. display: flex;
  25. align-items: center;
  26. justify-content: space-between; /* Pour espacer les éléments */
  27. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  28. z-index: 1000;
  29. flex-wrap: wrap;
  30. }
  31.  
  32. .header-left, .header-center, .header-right {
  33. display: flex;
  34. align-items: center;
  35. }
  36. .header-center {
  37. flex-grow: 1;
  38. justify-content: center;
  39. }
  40. .header-right button, .header-left button {
  41. background: none;
  42. border: none;
  43. font-size: 1.5em; /* Taille des icônes */
  44. cursor: pointer;
  45. color: #555;
  46. padding: 5px;
  47. margin-left: 10px;
  48. }
  49. .header-right button:hover, .header-left button:hover {
  50. color: #007bff; /* Bleu pour le survol */
  51. }
  52.  
  53. .search-container {
  54. position: relative;
  55. }
  56.  
  57. .headbar input[type="search"] {
  58. padding: 8px 12px;
  59. border-radius: 6px;
  60. border: 1px solid #ccc;
  61. background-color: #fff;
  62. color: #333;
  63. min-width: 250px;
  64. }
  65.  
  66. .headbar input[type="search"]::placeholder {
  67. color: #999;
  68. }
  69.  
  70. #search-suggestions {
  71. position: absolute;
  72. background-color: #fff;
  73. border: 1px solid #ccc;
  74. border-top: none;
  75. z-index: 999;
  76. max-height: 200px;
  77. overflow-y: auto;
  78. border-radius: 0 0 6px 6px;
  79. display: none;
  80. box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  81. }
  82.  
  83. #search-suggestions div {
  84. padding: 8px 12px;
  85. cursor: pointer;
  86. color: #333;
  87. }
  88.  
  89. #search-suggestions div:hover {
  90. background-color: #e9ecef;
  91. }
  92.  
  93. #progress-bar-container {
  94. width: 200px;
  95. background-color: #e9ecef;
  96. border-radius: 4px;
  97. margin: 0 15px;
  98. display: none;
  99. }
  100.  
  101. #progress-bar {
  102. width: 0%;
  103. height: 10px;
  104. background-color: #007bff;
  105. border-radius: 4px;
  106. text-align: center;
  107. line-height: 10px;
  108. color: white;
  109. font-size: 0.7em;
  110. transition: width 0.2s ease-in-out;
  111. }
  112.  
  113. #status-message {
  114. font-size: 0.9em;
  115. color: #555;
  116. white-space: nowrap;
  117. margin-left: 15px;
  118. }
  119.  
  120. #summary-info {
  121. position: absolute;
  122. top: 70px;
  123. right: 20px;
  124. background-color: rgba(255, 255, 255, 0.9);
  125. padding: 8px 12px;
  126. border-radius: 6px;
  127. box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  128. font-size: 0.85em;
  129. color: #333;
  130. z-index: 900;
  131. }
  132. #summary-info div { margin-bottom: 3px; }
  133. #summary-info div:last-child { margin-bottom: 0; }
  134.  
  135.  
  136. .main-content {
  137. display: flex;
  138. flex-grow: 1;
  139. position: relative;
  140. }
  141.  
  142. #background-canvas {
  143. position: absolute;
  144. top: 0;
  145. left: 0;
  146. width: 100%;
  147. height: 100%;
  148. z-index: 0;
  149. opacity: 0.1;
  150. }
  151.  
  152. #stats-container {
  153. min-width: 280px;
  154. max-width: 280px;
  155. padding: 20px;
  156. background-color: #ffffff;
  157. border-right: 1px solid #dee2e6;
  158. overflow-y: auto;
  159. z-index: 800;
  160. transition: margin-left 0.3s ease-in-out;
  161. margin-left: 0;
  162. }
  163. #stats-container.hidden {
  164. margin-left: -280px;
  165. }
  166.  
  167.  
  168. #stats-container h3 {
  169. margin-top: 0;
  170. color: #007bff;
  171. border-bottom: 1px solid #007bff;
  172. padding-bottom: 5px;
  173. }
  174.  
  175. .stats-level {
  176. background-color: #e9ecef;
  177. padding: 8px;
  178. margin-bottom: 8px;
  179. border-radius: 4px;
  180. font-size: 0.9em;
  181. cursor: default;
  182. color: #333;
  183. }
  184. .stats-level strong {
  185. color: #0056b3;
  186. }
  187.  
  188.  
  189. #graph-container {
  190. flex-grow: 1;
  191. position: relative;
  192. z-index: 1;
  193. background-color: #f8f9fa;
  194. }
  195.  
  196. #graph-svg {
  197. width: 100%;
  198. height: 100%;
  199. display: block;
  200. }
  201.  
  202. #zoom-level-display {
  203. position: absolute;
  204. bottom: 10px;
  205. right: 10px;
  206. background-color: rgba(0, 0, 0, 0.6);
  207. color: #fff;
  208. padding: 5px 10px;
  209. border-radius: 4px;
  210. font-size: 0.8em;
  211. z-index: 10;
  212. }
  213.  
  214. .node {
  215. cursor: pointer;
  216. transition: filter 0.3s ease;
  217. }
  218.  
  219. .node rect {
  220. stroke-width: 1.5px;
  221. transition: fill 0.3s ease, stroke 0.3s ease, filter 0.3s ease;
  222. filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.15));
  223. }
  224.  
  225. .node text {
  226. font-size: 11px;
  227. fill: #212529;
  228. pointer-events: none;
  229. text-anchor: middle;
  230. }
  231. .node text.label-crafted {
  232. font-weight: bold;
  233. }
  234.  
  235. .node .emoji {
  236. font-size: 14px;
  237. }
  238.  
  239. .node-origin rect { fill: #fff3cd; stroke: #ffeeba; }
  240. .node-origin text { fill: #856404; }
  241. .node-base rect { fill: #e2d9f3; stroke: #d6c9e0; }
  242. .node-base text { fill: #492f6d; }
  243. .node-crafted rect { fill: #ffffff; stroke: #ced4da; }
  244. .node-crafted text { fill: #343a40; }
  245.  
  246. .node:hover rect {
  247. filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.2));
  248. fill: #e6f2ff;
  249. }
  250.  
  251.  
  252. .link {
  253. stroke: #adb5bd;
  254. stroke-width: 1.5px;
  255. transition: stroke-opacity 0.3s ease, stroke 0.3s ease;
  256. }
  257.  
  258. .link.secondary-highlight {
  259. stroke: #007bff;
  260. stroke-width: 2px;
  261. }
  262.  
  263. .modal {
  264. display: none;
  265. position: fixed;
  266. z-index: 1001;
  267. left: 0;
  268. top: 0;
  269. width: 100%;
  270. height: 100%;
  271. overflow: auto;
  272. background-color: rgba(0,0,0,0.4);
  273. justify-content: center;
  274. align-items: center;
  275. }
  276.  
  277. .modal-content {
  278. background-color: #fff;
  279. margin: auto;
  280. padding: 25px;
  281. border: 1px solid #dee2e6;
  282. border-radius: 8px;
  283. width: 80%;
  284. max-width: 500px;
  285. box-shadow: 0 5px 15px rgba(0,0,0,0.15);
  286. color: #333;
  287. }
  288.  
  289. .modal-header {
  290. display: flex;
  291. justify-content: space-between;
  292. align-items: center;
  293. border-bottom: 1px solid #dee2e6;
  294. padding-bottom: 10px;
  295. margin-bottom: 15px;
  296. }
  297. .modal-header h2 {
  298. margin: 0;
  299. font-size: 1.8em;
  300. color: #007bff;
  301. }
  302.  
  303. .close-button {
  304. color: #aaa;
  305. font-size: 30px;
  306. font-weight: bold;
  307. cursor: pointer;
  308. }
  309.  
  310. .close-button:hover,
  311. .close-button:focus {
  312. color: #0056b3;
  313. text-decoration: none;
  314. }
  315. .modal-body p {
  316. line-height: 1.6;
  317. }
  318. .modal-body ul {
  319. list-style-type: none;
  320. padding-left: 0;
  321. }
  322. .modal-body li {
  323. background-color: #f8f9fa;
  324. padding: 8px;
  325. margin-bottom: 5px;
  326. border-radius: 4px;
  327. border: 1px solid #e9ecef;
  328. }
  329.  
  330. #stats-tooltip {
  331. position: absolute;
  332. background-color: #343a40;
  333. color: #f8f9fa;
  334. border: 1px solid #495057;
  335. padding: 10px;
  336. border-radius: 6px;
  337. font-size: 0.85em;
  338. pointer-events: none;
  339. display: none;
  340. z-index: 1005;
  341. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  342. white-space: nowrap;
  343. }
  344. </style>
  345. </head>
  346. <body>
  347.  
  348. <div class="headbar">
  349. <div class="header-left">
  350. <button id="toggle-stats-button" title="Afficher/Cacher Statistiques">📊</button>
  351. <input type="file" id="json-file-input" accept=".json" style="display:none;">
  352. <button id="upload-json-button" title="Charger JSON">📤</button>
  353. </div>
  354. <div class="header-center">
  355. <div class="search-container">
  356. <input type="search" id="search-input" placeholder="Rechercher un élément...">
  357. <div id="search-suggestions"></div>
  358. </div>
  359. <div id="progress-bar-container">
  360. <div id="progress-bar">0%</div>
  361. </div>
  362. </div>
  363. <div class="header-right">
  364. <button id="toggle-links-button" title="Afficher/Cacher Tous les Liens">🔗</button>
  365. <span id="status-message">Chargez un fichier JSON.</span>
  366. </div>
  367. </div>
  368. <div id="summary-info">
  369. <div>Éléments découverts: <span id="discovered-elements-count">0</span></div>
  370. <div>Recettes chargées: <span id="loaded-recipes-count">0</span></div>
  371. </div>
  372.  
  373.  
  374. <div class="main-content">
  375. <canvas id="background-canvas"></canvas>
  376. <div id="stats-container">
  377. <h3>Statistiques par Étage</h3>
  378. <div id="stats-content"></div>
  379. </div>
  380. <div id="graph-container">
  381. <svg id="graph-svg"></svg>
  382. <div id="zoom-level-display">Zoom: 1.00x</div>
  383. </div>
  384. </div>
  385.  
  386. <div id="item-modal" class="modal">
  387. <div class="modal-content">
  388. <div class="modal-header">
  389. <h2 id="modal-title"></h2>
  390. <span class="close-button" id="modal-close-button">&times;</span>
  391. </div>
  392. <div class="modal-body" id="modal-body"></div>
  393. </div>
  394. </div>
  395. <div id="stats-tooltip"></div>
  396.  
  397. <script type="module">
  398. // --- Configuration Globale ---
  399. const MIN_NODE_WIDTH = 100;
  400. const NODE_PADDING_X = 15;
  401. const NODE_PADDING_Y = 5;
  402. const NODE_HEIGHT = 40;
  403. const VIEWPORT_PADDING = 200;
  404. const LEVEL_SPACING_Y = NODE_HEIGHT * 2.5;
  405. const HORIZONTAL_NODE_SPACING = 30;
  406. const INITIAL_ORIGIN_SCALE = 1.0;
  407. const DEFAULT_EMOJI = '❔';
  408.  
  409. // --- État de l'application ---
  410. let allElements = new Map();
  411. let allRecipes = [];
  412. let allPotentialLinks = [];
  413. let baseElementNames = [];
  414. let showAllLinks = false;
  415. let statsPanelVisible = true;
  416. let dataWorker;
  417. let workerObjectURL = null;
  418.  
  419. let simulation;
  420. let svg, gMain, gLinks, gNodes;
  421. let zoomBehavior;
  422. let currentTransform = d3.zoomIdentity;
  423. let selectedNodeId = null;
  424.  
  425. // --- DOM Elements ---
  426. const fileInput = document.getElementById('json-file-input');
  427. const uploadJsonButton = document.getElementById('upload-json-button');
  428. const searchInput = document.getElementById('search-input');
  429. const toggleLinksButton = document.getElementById('toggle-links-button');
  430. const toggleStatsButton = document.getElementById('toggle-stats-button');
  431. const searchSuggestionsContainer = document.getElementById('search-suggestions');
  432. const progressBarContainer = document.getElementById('progress-bar-container');
  433. const progressBar = document.getElementById('progress-bar');
  434. const statusMessage = document.getElementById('status-message');
  435. const statsContainer = document.getElementById('stats-container');
  436. const statsContent = document.getElementById('stats-content');
  437. const statsTooltip = document.getElementById('stats-tooltip');
  438. const modal = document.getElementById('item-modal');
  439. const modalCloseButton = document.getElementById('modal-close-button');
  440. const modalTitle = document.getElementById('modal-title');
  441. const modalBody = document.getElementById('modal-body');
  442. const zoomLevelDisplay = document.getElementById('zoom-level-display');
  443. const discoveredElementsCountSpan = document.getElementById('discovered-elements-count');
  444. const loadedRecipesCountSpan = document.getElementById('loaded-recipes-count');
  445.  
  446. // --- Code du Web Worker (sous forme de chaîne de caractères) ---
  447. const workerScriptContent = `
  448. const BASE_ELEMENT_DEFINITIONS_WORKER = [
  449. { name: "Water", emoji: "💧" }, { name: "Fire", emoji: "🔥" },
  450. { name: "Earth", emoji: "🌍" }, { name: "Wind", emoji: "🌬️" }
  451. ];
  452. const DEFAULT_EMOJI_WORKER = '❔';
  453.  
  454. self.onmessage = function(e) {
  455. const fileContent = e.data;
  456. let jsonData;
  457. try {
  458. jsonData = JSON.parse(fileContent);
  459. } catch (error) {
  460. self.postMessage({ type: 'error', message: "Erreur de parsing JSON: " + error.message });
  461. return;
  462. }
  463.  
  464. let itemNames = [];
  465. let recipesInputArray = [];
  466. let formatType = 'unknown';
  467.  
  468. // Détection du format
  469. if (jsonData && typeof jsonData === 'object' && Array.isArray(jsonData.items) && Array.isArray(jsonData.recipes) && jsonData.recipes.every(r => Array.isArray(r) && r.length === 3 && r.every(i => typeof i === 'number'))) {
  470. formatType = 'new_indexed';
  471. itemNames = jsonData.items;
  472. recipesInputArray = jsonData.recipes;
  473. } else if (jsonData && typeof jsonData === 'object' && Array.isArray(jsonData.baseElements) && Array.isArray(jsonData.recipes) && jsonData.recipes.every(r => typeof r === 'object' && Array.isArray(r.inputs) && typeof r.output === 'string')) {
  474. formatType = 'old_structured';
  475. // itemNames sera construit à partir des baseElements et des outputs des recettes
  476. // recipesInputArray sera jsonData.recipes directement (après extraction des noms)
  477. } else if (Array.isArray(jsonData) && jsonData.length > 0 && typeof jsonData[0] === 'object' && 'first' in jsonData[0] && 'second' in jsonData[0] && 'result' in jsonData[0]) {
  478. formatType = 'old_flat_array';
  479. // itemNames sera construit à partir de tous les noms uniques dans les recettes
  480. // recipesInputArray sera jsonData directement
  481. }
  482.  
  483. if (formatType === 'unknown') {
  484. self.postMessage({ type: 'error', message: "Format JSON non reconnu." });
  485. return;
  486. }
  487.  
  488. const localAllElements = new Map();
  489. const localBaseElementNames = [];
  490. let localAllRecipes = [];
  491.  
  492. // Initialiser les éléments de base communs
  493. BASE_ELEMENT_DEFINITIONS_WORKER.forEach(elDef => {
  494. if (!localAllElements.has(elDef.name)) {
  495. localBaseElementNames.push(elDef.name);
  496. localAllElements.set(elDef.name, {
  497. id: elDef.name, name: elDef.name, emoji: elDef.emoji, level: 0,
  498. isBase: true, isOrigin: false, recipes: [], creates: [], discoveredBy: null
  499. });
  500. }
  501. });
  502. if (!localAllElements.has("✨ Origine")) {
  503. const originElement = {
  504. id: "✨ Origine", name: "✨ Origine", emoji: "✨", level: -1, isBase: false, isOrigin: true,
  505. recipes: [], creates: localBaseElementNames.map(name => ({productName: name, recipe: [name]})), discoveredBy: null
  506. };
  507. localAllElements.set(originElement.id, originElement);
  508. }
  509.  
  510.  
  511. if (formatType === 'new_indexed') {
  512. localAllRecipes = recipesInputArray.map(indexedRecipe => {
  513. const input1Name = itemNames[indexedRecipe[0]];
  514. const input2Name = itemNames[indexedRecipe[1]];
  515. const outputName = itemNames[indexedRecipe[2]];
  516. if (!input1Name || !input2Name || !outputName) return null;
  517.  
  518. const baseDefOutput = BASE_ELEMENT_DEFINITIONS_WORKER.find(b => b.name === outputName);
  519. const outputEmoji = baseDefOutput ? baseDefOutput.emoji : DEFAULT_EMOJI_WORKER;
  520. return { inputs: [input1Name, input2Name].sort(), output: outputName, outputEmoji: outputEmoji };
  521. }).filter(r => r !== null);
  522. } else if (formatType === 'old_structured') {
  523. jsonData.baseElements.forEach(el => {
  524. if (!localAllElements.has(el.name)) { // S'assurer qu'il n'est pas déjà un des 4 de base
  525. if (!BASE_ELEMENT_DEFINITIONS_WORKER.some(b => b.name === el.name)) {
  526. localBaseElementNames.push(el.name); // Ajouter aux noms de base si pas déjà là
  527. }
  528. localAllElements.set(el.name, {
  529. id: el.name, name: el.name, emoji: el.emoji || DEFAULT_EMOJI_WORKER, level: 0,
  530. isBase: true, isOrigin: false, recipes: [], creates: [], discoveredBy: null
  531. });
  532. }
  533. });
  534. localAllRecipes = jsonData.recipes.map(r => ({
  535. inputs: r.inputs.sort(),
  536. output: r.output,
  537. outputEmoji: r.outputEmoji || DEFAULT_EMOJI_WORKER
  538. }));
  539. } else if (formatType === 'old_flat_array') {
  540. localAllRecipes = jsonData.map(r => ({
  541. inputs: [r.first, r.second].sort(),
  542. output: r.result,
  543. outputEmoji: r.emoji || DEFAULT_EMOJI_WORKER
  544. }));
  545. }
  546.  
  547. // --- Suite du traitement (calcul des niveaux, etc.) ---
  548. let recipesToProcess = [...localAllRecipes];
  549. let maxLevelReached = 0;
  550. let newDiscoveriesMadeInIteration = true;
  551. const maxIterations = 500;
  552. let iterationCount = 0;
  553. const totalRecipes = localAllRecipes.length;
  554.  
  555. self.postMessage({ type: 'progress', value: 0, message: "Calcul des niveaux..." });
  556.  
  557. while (newDiscoveriesMadeInIteration && iterationCount < maxIterations && recipesToProcess.length > 0) {
  558. newDiscoveriesMadeInIteration = false;
  559. let remainingRecipesForNextIteration = [];
  560. for (const recipe of recipesToProcess) {
  561. const ingredientsKnownAndLeveled = recipe.inputs.every(inputName => {
  562. const el = localAllElements.get(inputName);
  563. return el && typeof el.level === 'number';
  564. });
  565. if (ingredientsKnownAndLeveled) {
  566. const inputElements = recipe.inputs.map(name => localAllElements.get(name));
  567. const maxInputLevel = Math.max(0, ...inputElements.map(el => el.level));
  568. const outputLevel = maxInputLevel + 1;
  569. let outputElement = localAllElements.get(recipe.output);
  570. if (!outputElement) {
  571. outputElement = {
  572. id: recipe.output, name: recipe.output, emoji: recipe.outputEmoji,
  573. level: outputLevel, isBase: false, isOrigin: false,
  574. recipes: [], creates: [], discoveredBy: recipe.inputs.sort()
  575. };
  576. localAllElements.set(recipe.output, outputElement);
  577. newDiscoveriesMadeInIteration = true;
  578. } else {
  579. if (typeof outputElement.level === 'undefined' || outputLevel < outputElement.level) {
  580. outputElement.level = outputLevel;
  581. outputElement.discoveredBy = recipe.inputs.sort();
  582. newDiscoveriesMadeInIteration = true;
  583. }
  584. if (outputElement.emoji === DEFAULT_EMOJI_WORKER && recipe.outputEmoji !== DEFAULT_EMOJI_WORKER) {
  585. outputElement.emoji = recipe.outputEmoji;
  586. }
  587. }
  588. const recipeKey = recipe.inputs.sort().join('+');
  589. if (!outputElement.recipes.find(r => r.sort().join('+') === recipeKey)) {
  590. outputElement.recipes.push(recipe.inputs.sort());
  591. }
  592. recipe.inputs.forEach(inputName => {
  593. const inputEl = localAllElements.get(inputName);
  594. if (inputEl && !inputEl.creates.some(c => c.productName === recipe.output && JSON.stringify(c.recipe.sort()) === JSON.stringify(recipe.inputs.sort()))) {
  595. inputEl.creates.push({ productName: recipe.output, recipe: recipe.inputs.sort() });
  596. }
  597. });
  598. if (maxLevelReached < outputLevel) maxLevelReached = outputLevel;
  599. } else {
  600. remainingRecipesForNextIteration.push(recipe);
  601. }
  602. }
  603. recipesToProcess = remainingRecipesForNextIteration;
  604. iterationCount++;
  605. if (iterationCount % 50 === 0) {
  606. self.postMessage({ type: 'progress', value: (totalRecipes - recipesToProcess.length) / totalRecipes * 100, message: \`Calcul des niveaux (\${iterationCount})...\` });
  607. }
  608. }
  609.  
  610. localAllElements.forEach(el => {
  611. if (typeof el.level === 'undefined') {
  612. const baseDef = BASE_ELEMENT_DEFINITIONS_WORKER.find(b => b.name === el.id);
  613. if (baseDef) {
  614. el.emoji = el.emoji || baseDef.emoji;
  615. el.level = 0;
  616. el.isBase = true;
  617. } else {
  618. el.emoji = el.emoji || DEFAULT_EMOJI_WORKER;
  619. el.level = maxLevelReached + 1;
  620. }
  621. }
  622. });
  623.  
  624. self.postMessage({ type: 'progress', value: 95, message: "Génération des liens..." });
  625.  
  626. const localAllPotentialLinks = [];
  627. const origin = localAllElements.get("✨ Origine");
  628. if (origin && origin.creates) {
  629. origin.creates.forEach(creation => {
  630. if (localAllElements.has(creation.productName)) {
  631. localAllPotentialLinks.push({source: origin.id, target: localAllElements.get(creation.productName).id, type: 'base_connection'});
  632. }
  633. });
  634. }
  635. localAllRecipes.forEach(recipe => {
  636. const outputEl = localAllElements.get(recipe.output);
  637. if (outputEl) {
  638. recipe.inputs.forEach(inputName => {
  639. const inputEl = localAllElements.get(inputName);
  640. if (inputEl) {
  641. if (!localAllPotentialLinks.some(l => l.source === inputName && l.target === recipe.output)) {
  642. localAllPotentialLinks.push({ source: inputEl.id, target: outputEl.id, type: 'ingredient' });
  643. }
  644. }
  645. });
  646. }
  647. });
  648.  
  649. const elementsArray = Array.from(localAllElements.entries());
  650.  
  651. self.postMessage({
  652. type: 'done',
  653. elements: elementsArray,
  654. recipes: localAllRecipes,
  655. potentialLinks: localAllPotentialLinks,
  656. baseNames: localBaseElementNames
  657. });
  658. };
  659. `;
  660.  
  661.  
  662. // --- Initialisation ---
  663. function initializeGraph() {
  664. svg = d3.select("#graph-svg");
  665.  
  666. zoomBehavior = d3.zoom()
  667. .scaleExtent([0.02, 8])
  668. .on("zoom", zoomed);
  669. svg.call(zoomBehavior);
  670.  
  671. if (gMain) gMain.remove();
  672. gMain = svg.append("g").attr("class", "main-group");
  673. gLinks = gMain.append("g").attr("class", "links-group");
  674. gNodes = gMain.append("g").attr("class", "nodes-group");
  675.  
  676. simulation = d3.forceSimulation()
  677. .on("tick", () => {
  678. updateRenderedNodesAndLinks();
  679. });
  680.  
  681. svg.on("click", (event) => {
  682. if (event.target === svg.node()) {
  683. deselectNode();
  684. }
  685. });
  686.  
  687. window.addEventListener('resize', () => {
  688. if (allElements.size > 0) {
  689. calculateFixedPositions();
  690. }
  691. updateRenderedNodesAndLinks();
  692. });
  693. }
  694.  
  695. function zoomed(event) {
  696. currentTransform = event.transform;
  697. if (gMain) gMain.attr("transform", currentTransform);
  698. if (zoomLevelDisplay) {
  699. zoomLevelDisplay.textContent = `Zoom: ${currentTransform.k.toFixed(2)}x`;
  700. }
  701. updateRenderedNodesAndLinks();
  702. }
  703.  
  704. // --- Traitement des Données ---
  705. uploadJsonButton.addEventListener('click', () => fileInput.click());
  706.  
  707. fileInput.addEventListener('change', async (event) => {
  708. const file = event.target.files[0];
  709. if (file) {
  710. statusMessage.textContent = "Chargement...";
  711. progressBarContainer.style.display = 'flex';
  712. progressBar.style.width = '0%';
  713. progressBar.textContent = '0%';
  714.  
  715. if (simulation) simulation.stop();
  716. if (dataWorker) {
  717. dataWorker.terminate();
  718. if(workerObjectURL) URL.revokeObjectURL(workerObjectURL);
  719. }
  720. initializeGraph();
  721.  
  722. allElements.clear();
  723. allRecipes = [];
  724. allPotentialLinks = [];
  725. baseElementNames = [];
  726. selectedNodeId = null;
  727. showAllLinks = false;
  728. toggleLinksButton.innerHTML = "🔗";
  729. toggleLinksButton.title = "Afficher Tous les Liens";
  730. statsContent.innerHTML = '';
  731. discoveredElementsCountSpan.textContent = '0';
  732. loadedRecipesCountSpan.textContent = '0';
  733.  
  734. const reader = new FileReader();
  735. reader.onload = function(e) {
  736. const fileContent = e.target.result;
  737.  
  738. try {
  739. const blob = new Blob([workerScriptContent], { type: 'application/javascript' });
  740. workerObjectURL = URL.createObjectURL(blob);
  741. dataWorker = new Worker(workerObjectURL);
  742. } catch (workerError) {
  743. console.error("Erreur lors de la création du Worker:", workerError);
  744. statusMessage.textContent = "Erreur: Impossible de démarrer le traitement en arrière-plan.";
  745. progressBarContainer.style.display = 'none';
  746. if (window.location.protocol === 'file:') {
  747. statusMessage.innerHTML += "<br><small>Les Web Workers ne fonctionnent souvent pas avec le protocole file://. Essayez de servir la page via un serveur HTTP local.</small>";
  748. }
  749. return;
  750. }
  751.  
  752. dataWorker.postMessage(fileContent);
  753.  
  754. dataWorker.onmessage = function(eventFromWorker) {
  755. const data = eventFromWorker.data;
  756. if (data.type === 'progress') {
  757. progressBar.style.width = `${data.value}%`;
  758. progressBar.textContent = `${Math.round(data.value)}%`;
  759. if(data.message) statusMessage.textContent = data.message;
  760. } else if (data.type === 'done') {
  761. allElements = new Map(data.elements);
  762. allRecipes = data.recipes;
  763. allPotentialLinks = data.potentialLinks.map(l => ({
  764. source: allElements.get(l.source),
  765. target: allElements.get(l.target),
  766. type: l.type
  767. }));
  768. baseElementNames = data.baseNames;
  769.  
  770. calculateFixedPositions();
  771. simulation.nodes(Array.from(allElements.values()));
  772. simulation.alpha(0.1).restart();
  773.  
  774. setTimeout(updateRenderedNodesAndLinks, 50);
  775. updateStatistics();
  776. populateSearchSuggestions();
  777.  
  778. statusMessage.textContent = `Fichier traité.`;
  779. discoveredElementsCountSpan.textContent = allElements.size > 0 ? allElements.size -1 : 0;
  780. loadedRecipesCountSpan.textContent = allRecipes.length;
  781. centerViewOnOrigin();
  782. progressBarContainer.style.display = 'none';
  783. dataWorker.terminate();
  784. if(workerObjectURL) URL.revokeObjectURL(workerObjectURL);
  785. } else if (data.type === 'error') {
  786. console.error("Erreur du Worker:", data.message);
  787. statusMessage.textContent = "Erreur de traitement: " + data.message;
  788. progressBarContainer.style.display = 'none';
  789. dataWorker.terminate();
  790. if(workerObjectURL) URL.revokeObjectURL(workerObjectURL);
  791. }
  792. };
  793. dataWorker.onerror = function(error) {
  794. console.error("Erreur fatale du Worker:", error);
  795. statusMessage.textContent = "Erreur fatale de traitement en arrière-plan.";
  796. progressBarContainer.style.display = 'none';
  797. dataWorker.terminate();
  798. if(workerObjectURL) URL.revokeObjectURL(workerObjectURL);
  799. };
  800.  
  801. };
  802. reader.onerror = function() {
  803. statusMessage.textContent = "Erreur de lecture du fichier.";
  804. progressBarContainer.style.display = 'none';
  805. };
  806. reader.readAsText(file);
  807. }
  808. });
  809.  
  810.  
  811. function calculateNodeWidth(nodeData) {
  812. const tempText = svg.append("text")
  813. .attr("class", "node-text-width-calculator")
  814. .style("font-size", "11px")
  815. .style("font-weight", (!nodeData.isBase && !nodeData.isOrigin) ? "bold" : "normal")
  816. .text(nodeData.name);
  817. const tempEmoji = svg.append("text")
  818. .attr("class", "node-text-width-calculator")
  819. .style("font-size", "14px")
  820. .text(nodeData.emoji || "");
  821.  
  822. const nameWidth = tempText.node().getComputedTextLength();
  823. const emojiWidth = nodeData.emoji ? tempEmoji.node().getComputedTextLength() + 5 : 0;
  824.  
  825. tempText.remove();
  826. tempEmoji.remove();
  827.  
  828. return Math.max(MIN_NODE_WIDTH, emojiWidth + nameWidth + NODE_PADDING_X * 2);
  829. }
  830.  
  831.  
  832. function calculateFixedPositions() {
  833. const graphContainerWidth = svg.node()?.getBoundingClientRect().width || 800;
  834. const elementsByLevel = new Map();
  835. let maxLevel = -1;
  836.  
  837. allElements.forEach(el => {
  838. if (typeof el.level !== 'number') return;
  839. el.width = calculateNodeWidth(el);
  840. if (!elementsByLevel.has(el.level)) {
  841. elementsByLevel.set(el.level, []);
  842. }
  843. elementsByLevel.get(el.level).push(el);
  844. if (el.level > maxLevel) maxLevel = el.level;
  845. });
  846.  
  847. const originNode = allElements.get("✨ Origine");
  848. if (originNode) {
  849. originNode.fx = graphContainerWidth / 2;
  850. originNode.fy = NODE_HEIGHT / 2;
  851. }
  852.  
  853. let currentY = LEVEL_SPACING_Y + NODE_HEIGHT / 2;
  854. for (let level = 0; level <= maxLevel; level++) {
  855. const nodesInLevel = elementsByLevel.get(level) || [];
  856. nodesInLevel.sort((a, b) => a.name.localeCompare(b.name));
  857.  
  858. const totalWidthOfNodesInLevel = nodesInLevel.reduce((sum, node) => sum + node.width, 0);
  859. const totalSpacingInLevel = (nodesInLevel.length - 1) * HORIZONTAL_NODE_SPACING;
  860. const levelContentWidth = totalWidthOfNodesInLevel + totalSpacingInLevel;
  861.  
  862. let currentX = (graphContainerWidth / 2) - (levelContentWidth / 2);
  863.  
  864. nodesInLevel.forEach((node) => {
  865. node.fy = currentY;
  866. node.fx = currentX + node.width / 2;
  867. currentX += node.width + HORIZONTAL_NODE_SPACING;
  868. });
  869. if (nodesInLevel.length > 0) {
  870. currentY += LEVEL_SPACING_Y;
  871. }
  872. }
  873. }
  874.  
  875. function centerViewOnOrigin() {
  876. const originNode = allElements.get("✨ Origine");
  877. if (originNode && originNode.fx != null && originNode.fy != null && svg.node()) {
  878. const svgWidth = svg.node().getBoundingClientRect().width;
  879. const svgHeight = svg.node().getBoundingClientRect().height;
  880.  
  881. const translateX = svgWidth / 2 - originNode.fx * INITIAL_ORIGIN_SCALE;
  882. const translateY = (svgHeight / 2) - (originNode.fy * INITIAL_ORIGIN_SCALE) - (LEVEL_SPACING_Y * 0.5 * INITIAL_ORIGIN_SCALE);
  883.  
  884. const initialTransform = d3.zoomIdentity.translate(translateX, translateY).scale(INITIAL_ORIGIN_SCALE);
  885.  
  886. svg.call(zoomBehavior.transform, initialTransform);
  887. currentTransform = initialTransform;
  888. if (zoomLevelDisplay) {
  889. zoomLevelDisplay.textContent = `Zoom: ${currentTransform.k.toFixed(2)}x`;
  890. }
  891.  
  892. selectedNodeId = originNode.id;
  893. statusMessage.textContent = `Élément sélectionné : ${originNode.emoji} ${originNode.name}`;
  894. updateRenderedNodesAndLinks();
  895. const originNodeG = d3.select(`#node-${sanitizeId(originNode.id)}`);
  896. if (!originNodeG.empty()) {
  897. originNodeG.select("rect").style("filter", "drop-shadow(0px 0px 8px #007bff)");
  898. }
  899. } else {
  900. centerViewOnLoad();
  901. }
  902. }
  903.  
  904. function centerViewOnLoad() {
  905. if (!allElements.size || !svg.node()) return;
  906. const originNode = allElements.get("✨ Origine");
  907. if (originNode && originNode.fx != null && originNode.fy != null) {
  908. const svgWidth = svg.node().getBoundingClientRect().width;
  909. const svgHeight = svg.node().getBoundingClientRect().height;
  910. const scale = 0.5;
  911. const translateX = svgWidth / 2 - originNode.fx * scale;
  912. const translateY = svgHeight / 3 - originNode.fy * scale;
  913. const initialTransform = d3.zoomIdentity.translate(translateX, translateY).scale(scale);
  914. svg.call(zoomBehavior.transform, initialTransform);
  915. currentTransform = initialTransform;
  916. if (zoomLevelDisplay) {
  917. zoomLevelDisplay.textContent = `Zoom: ${currentTransform.k.toFixed(2)}x`;
  918. }
  919. }
  920. }
  921.  
  922.  
  923. // --- Rendu dynamique basé sur le viewport ---
  924. function updateRenderedNodesAndLinks() {
  925. if (!svg || !gNodes || !gLinks || !allElements.size) return;
  926.  
  927. const svgWidth = svg.node().getBoundingClientRect().width;
  928. const svgHeight = svg.node().getBoundingClientRect().height;
  929.  
  930. const topLeftData = currentTransform.invert([-VIEWPORT_PADDING, -VIEWPORT_PADDING]);
  931. const bottomRightData = currentTransform.invert([svgWidth + VIEWPORT_PADDING, svgHeight + VIEWPORT_PADDING]);
  932.  
  933. const nodesToRender = Array.from(allElements.values()).filter(node => {
  934. if (node.fx == null || node.fy == null || node.width == null) return false;
  935. return node.fx + node.width / 2 > topLeftData[0] &&
  936. node.fx - node.width / 2 < bottomRightData[0] &&
  937. node.fy + NODE_HEIGHT / 2 > topLeftData[1] &&
  938. node.fy - NODE_HEIGHT / 2 < bottomRightData[1];
  939. });
  940.  
  941. const nodeIdsToRender = new Set(nodesToRender.map(n => n.id));
  942.  
  943. const linksToRender = allPotentialLinks.filter(linkData => {
  944. const sourceNode = typeof linkData.source === 'string' ? allElements.get(linkData.source) : linkData.source;
  945. const targetNode = typeof linkData.target === 'string' ? allElements.get(linkData.target) : linkData.target;
  946. if (!sourceNode || !targetNode) return false;
  947.  
  948. if (showAllLinks) {
  949. return nodeIdsToRender.has(sourceNode.id) || nodeIdsToRender.has(targetNode.id);
  950. } else {
  951. return nodeIdsToRender.has(sourceNode.id) && nodeIdsToRender.has(targetNode.id);
  952. }
  953. });
  954.  
  955. const nodeSelection = gNodes.selectAll(".node")
  956. .data(nodesToRender, d => d.id);
  957.  
  958. nodeSelection.exit().remove();
  959.  
  960. const nodeEnter = nodeSelection.enter().append("g")
  961. .attr("class", d => `node node-${d.isOrigin ? 'origin' : (d.isBase ? 'base' : 'crafted')}`)
  962. .attr("id", d => `node-${sanitizeId(d.id)}`)
  963. .on("click", nodeClicked)
  964. .on("dblclick", nodeDoubleClicked);
  965.  
  966. nodeEnter.append("rect")
  967. .attr("height", NODE_HEIGHT)
  968. .attr("rx", 8).attr("ry", 8);
  969.  
  970. nodeEnter.append("text")
  971. .attr("class", "emoji")
  972. .attr("dy", "0.35em");
  973.  
  974. nodeEnter.append("text")
  975. .attr("class", d => d.isBase || d.isOrigin ? "label" : "label label-crafted")
  976. .attr("dy", "0.35em");
  977.  
  978. const allRenderedNodes = nodeEnter.merge(nodeSelection);
  979. allRenderedNodes.attr("transform", d => `translate(${d.fx - d.width / 2}, ${d.fy - NODE_HEIGHT / 2})`);
  980.  
  981. allRenderedNodes.select("rect").attr("width", d => d.width);
  982.  
  983.  
  984. allRenderedNodes.each(function(d) {
  985. const nodeElement = d3.select(this);
  986. const emojiText = nodeElement.select(".emoji").text(d.emoji);
  987. const labelText = nodeElement.select(".label").text(d.name);
  988.  
  989. const emojiBBoxWidth = d.emoji ? emojiText.node().getBBox().width + 5 : 0;
  990. const labelBBoxWidth = labelText.node().getBBox().width;
  991.  
  992. emojiText.attr("x", (d.width - labelBBoxWidth - emojiBBoxWidth + (d.emoji ? 5:0) ) / 2 + (d.emoji ? 0 : emojiBBoxWidth/2) );
  993. labelText.attr("x", (d.width + emojiBBoxWidth - (d.emoji ? 5:0) ) / 2 );
  994.  
  995.  
  996. emojiText.attr("y", NODE_HEIGHT / 2);
  997. labelText.attr("y", NODE_HEIGHT / 2);
  998.  
  999. if (!d.isBase && !d.isOrigin) {
  1000. labelText.classed("label-crafted", true);
  1001. } else {
  1002. labelText.classed("label-crafted", false);
  1003. }
  1004. });
  1005.  
  1006. const linkSelection = gLinks.selectAll(".link")
  1007. .data(linksToRender, d => `${(d.source.id || d.source)}-${(d.target.id || d.target)}`);
  1008.  
  1009. linkSelection.exit().remove();
  1010.  
  1011. linkSelection.enter().append("line")
  1012. .attr("class", "link")
  1013. .merge(linkSelection)
  1014. .attr("x1", d => d.source.fx)
  1015. .attr("y1", d => d.source.fy)
  1016. .attr("x2", d => d.target.fx)
  1017. .attr("y2", d => d.target.fy)
  1018. .attr("stroke-opacity", function(d) {
  1019. if (selectedNodeId) {
  1020. const selectedElement = allElements.get(selectedNodeId);
  1021. if (!selectedElement) return showAllLinks ? 0.25 : 0.1;
  1022.  
  1023. const parentsOfSelected = new Set(selectedElement.recipes.flat());
  1024. const childrenOfSelected = new Set(selectedElement.creates.map(c => c.productName));
  1025. const sourceId = d.source.id || d.source;
  1026. const targetId = d.target.id || d.target;
  1027.  
  1028. if (sourceId === selectedNodeId || targetId === selectedNodeId ||
  1029. (parentsOfSelected.has(sourceId) && parentsOfSelected.has(targetId)) ||
  1030. (childrenOfSelected.has(sourceId) && childrenOfSelected.has(targetId))) {
  1031. return 0.8;
  1032. }
  1033. }
  1034. return showAllLinks ? 0.25 : 0.1;
  1035. })
  1036. .classed("secondary-highlight", function(d) {
  1037. if (selectedNodeId) {
  1038. const selectedElement = allElements.get(selectedNodeId);
  1039. if (!selectedElement) return false;
  1040. const parentsOfSelected = new Set(selectedElement.recipes.flat());
  1041. const childrenOfSelected = new Set(selectedElement.creates.map(c => c.productName));
  1042. const sourceId = d.source.id || d.source;
  1043. const targetId = d.target.id || d.target;
  1044.  
  1045. return (sourceId === selectedNodeId || targetId === selectedNodeId ||
  1046. (parentsOfSelected.has(sourceId) && parentsOfSelected.has(targetId)) ||
  1047. (childrenOfSelected.has(sourceId) && childrenOfSelected.has(targetId)));
  1048. }
  1049. return false;
  1050. });
  1051. }
  1052.  
  1053. function dragstarted(event, d) { /* No-op pour layout fixe */ }
  1054. function dragged(event, d) { /* No-op pour layout fixe */ }
  1055. function dragended(event, d) { /* No-op pour layout fixe */ }
  1056.  
  1057.  
  1058. function sanitizeId(id) {
  1059. let sanitized = String(id).replace(/[^\w\s-]/gi, '');
  1060. sanitized = sanitized.replace(/\s+/g, '-');
  1061. if (sanitized.match(/^[\d-]/)) {
  1062. sanitized = "id-" + sanitized;
  1063. }
  1064. return sanitized;
  1065. }
  1066.  
  1067.  
  1068. // --- Interactions ---
  1069. toggleLinksButton.addEventListener('click', () => {
  1070. showAllLinks = !showAllLinks;
  1071. toggleLinksButton.innerHTML = showAllLinks ? "🔗" : "⛓️";
  1072. toggleLinksButton.title = showAllLinks ? "Cacher Liens Non Pertinents" : "Afficher Tous les Liens";
  1073. updateRenderedNodesAndLinks();
  1074. });
  1075.  
  1076. toggleStatsButton.addEventListener('click', () => {
  1077. statsPanelVisible = !statsPanelVisible;
  1078. statsContainer.classList.toggle('hidden', !statsPanelVisible);
  1079. toggleStatsButton.innerHTML = statsPanelVisible ? "📊" : "📈";
  1080. toggleStatsButton.title = statsPanelVisible ? "Cacher Statistiques" : "Afficher Statistiques";
  1081. });
  1082.  
  1083.  
  1084. function nodeClicked(event, d) {
  1085. event.stopPropagation();
  1086.  
  1087. if (selectedNodeId === d.id) {
  1088. updateRenderedNodesAndLinks();
  1089. return;
  1090. }
  1091.  
  1092. selectedNodeId = d.id;
  1093. statusMessage.textContent = `Élément sélectionné : ${d.emoji} ${d.name}`;
  1094.  
  1095. svg.transition().duration(750)
  1096. .call(zoomBehavior.translateTo, d.fx, d.fy)
  1097. .transition().duration(250)
  1098. .call(zoomBehavior.scaleTo, 1.5);
  1099.  
  1100. gNodes.selectAll(".node rect")
  1101. .style("filter", "drop-shadow(0px 1px 2px rgba(0,0,0,0.15))");
  1102.  
  1103. const clickedNodeElement = gNodes.select(`#node-${sanitizeId(d.id)}`);
  1104. if (!clickedNodeElement.empty()) {
  1105. clickedNodeElement.select("rect").style("filter", "drop-shadow(0px 0px 6px #007bff)");
  1106. }
  1107. updateRenderedNodesAndLinks();
  1108. }
  1109.  
  1110.  
  1111. function deselectNode() {
  1112. selectedNodeId = null;
  1113. statusMessage.textContent = "Aucun élément sélectionné.";
  1114. gNodes.selectAll(".node rect").style("filter", "drop-shadow(0px 1px 2px rgba(0,0,0,0.15))");
  1115. updateRenderedNodesAndLinks();
  1116. }
  1117.  
  1118. function nodeDoubleClicked(event, d) {
  1119. event.stopPropagation();
  1120. modalTitle.textContent = `${d.emoji} ${d.name}`;
  1121.  
  1122. let modalHtml = `<p><strong>Niveau de découverte :</strong> ${d.level}</p>`;
  1123.  
  1124. if (d.recipes && d.recipes.length > 0) {
  1125. modalHtml += `<h4>Recettes pour créer cet élément :</h4><ul>`;
  1126. d.recipes.forEach(recipe => {
  1127. const ingredientsText = recipe.map(ingName => {
  1128. const ingEl = allElements.get(ingName);
  1129. return ingEl ? `${ingEl.emoji} ${ingEl.name}` : ingName;
  1130. }).join(' + ');
  1131. modalHtml += `<li>${ingredientsText}</li>`;
  1132. });
  1133. modalHtml += `</ul>`;
  1134. } else if (d.isBase) {
  1135. modalHtml += `<p>C'est un élément de base.</p>`;
  1136. } else if (d.isOrigin) {
  1137. modalHtml += `<p>C'est le point d'origine de toutes les découvertes.</p>`;
  1138. }
  1139.  
  1140.  
  1141. if (d.creates && d.creates.length > 0) {
  1142. modalHtml += `<h4>Aide à créer :</h4><ul>`;
  1143. const uniqueCreations = [];
  1144. d.creates.forEach(creation => {
  1145. if (!uniqueCreations.find(uc => uc.productName === creation.productName)) {
  1146. uniqueCreations.push(creation);
  1147. }
  1148. });
  1149. uniqueCreations.forEach(creation => {
  1150. const productEl = allElements.get(creation.productName);
  1151. modalHtml += `<li>${productEl ? productEl.emoji : ''} ${creation.productName}</li>`;
  1152. });
  1153. modalHtml += `</ul>`;
  1154. }
  1155.  
  1156. modalBody.innerHTML = modalHtml;
  1157. modal.style.display = 'flex';
  1158. }
  1159.  
  1160. modalCloseButton.onclick = () => { modal.style.display = 'none'; }
  1161. window.onclick = (event) => {
  1162. if (event.target == modal) {
  1163. modal.style.display = 'none';
  1164. }
  1165. }
  1166.  
  1167. // --- Recherche ---
  1168. function positionSearchSuggestions() {
  1169. const searchInputRect = searchInput.getBoundingClientRect();
  1170. const headbarRect = searchInput.closest('.headbar').getBoundingClientRect();
  1171.  
  1172. searchSuggestionsContainer.style.left = `${searchInputRect.left - headbarRect.left}px`;
  1173. searchSuggestionsContainer.style.top = `${searchInputRect.bottom - headbarRect.top}px`;
  1174. searchSuggestionsContainer.style.width = `${searchInputRect.width}px`;
  1175. }
  1176.  
  1177.  
  1178. function populateSearchSuggestions() {
  1179. const searchTerm = searchInput.value.toLowerCase();
  1180. searchSuggestionsContainer.innerHTML = '';
  1181. positionSearchSuggestions();
  1182.  
  1183.  
  1184. if (searchTerm.length < 1) {
  1185. searchSuggestionsContainer.style.display = 'none';
  1186. return;
  1187. }
  1188.  
  1189. const suggestions = Array.from(allElements.values())
  1190. .filter(el => el.name.toLowerCase().includes(searchTerm) && !el.isOrigin)
  1191. .sort((a,b) => a.name.localeCompare(b.name))
  1192. .slice(0, 10);
  1193.  
  1194. if (suggestions.length > 0) {
  1195. suggestions.forEach(el => {
  1196. const div = document.createElement('div');
  1197. div.textContent = `${el.emoji} ${el.name}`;
  1198. div.onclick = () => {
  1199. searchInput.value = el.name;
  1200. searchSuggestionsContainer.style.display = 'none';
  1201. handleSearch();
  1202. };
  1203. searchSuggestionsContainer.appendChild(div);
  1204. });
  1205. searchSuggestionsContainer.style.display = 'block';
  1206. } else {
  1207. searchSuggestionsContainer.style.display = 'none';
  1208. }
  1209. }
  1210. searchInput.addEventListener('input', populateSearchSuggestions);
  1211. searchInput.addEventListener('focus', populateSearchSuggestions);
  1212. searchInput.addEventListener('keypress', (e) => {
  1213. if (e.key === 'Enter') {
  1214. e.preventDefault();
  1215. handleSearch();
  1216. }
  1217. });
  1218.  
  1219. document.addEventListener('click', function(event) {
  1220. if (searchInput && searchSuggestionsContainer) {
  1221. if (!searchInput.contains(event.target) && !searchSuggestionsContainer.contains(event.target)) {
  1222. searchSuggestionsContainer.style.display = 'none';
  1223. }
  1224. }
  1225. });
  1226.  
  1227.  
  1228. function handleSearch() {
  1229. const searchTerm = searchInput.value.trim().toLowerCase();
  1230. searchSuggestionsContainer.style.display = 'none';
  1231. if (!searchTerm) return;
  1232.  
  1233. const foundElement = Array.from(allElements.values()).find(el => el.name.toLowerCase() === searchTerm);
  1234.  
  1235. if (foundElement && !foundElement.isOrigin) {
  1236. svg.transition().duration(750)
  1237. .call(zoomBehavior.translateTo, foundElement.fx, foundElement.fy)
  1238. .transition().duration(250)
  1239. .call(zoomBehavior.scaleTo, 1.5)
  1240. .on("end", () => {
  1241. updateRenderedNodesAndLinks();
  1242. const nodeG = d3.select(`#node-${sanitizeId(foundElement.id)}`);
  1243. if (!nodeG.empty() && nodeG.datum()) {
  1244. nodeClicked.call(nodeG.node(), new MouseEvent('click'), nodeG.datum());
  1245. statusMessage.textContent = `Élément trouvé et sélectionné : ${foundElement.emoji} ${foundElement.name}`;
  1246. } else {
  1247. selectedNodeId = foundElement.id;
  1248. statusMessage.textContent = `Élément trouvé : ${foundElement.emoji} ${foundElement.name}.`;
  1249. updateRenderedNodesAndLinks();
  1250. }
  1251. });
  1252. } else {
  1253. statusMessage.textContent = `Élément "${searchInput.value}" non trouvé.`;
  1254. }
  1255. }
  1256.  
  1257. // --- Statistiques ---
  1258. function updateStatistics() {
  1259. statsContent.innerHTML = '';
  1260. const elementsByLevel = {};
  1261. let maxDiscoveredLevel = -1;
  1262.  
  1263. allElements.forEach(el => {
  1264. if (el.isOrigin || typeof el.level !== 'number') return;
  1265. if (!elementsByLevel[el.level]) {
  1266. elementsByLevel[el.level] = [];
  1267. }
  1268. elementsByLevel[el.level].push(el);
  1269. if (el.level > maxDiscoveredLevel) maxDiscoveredLevel = el.level;
  1270. });
  1271.  
  1272. let cumulativeUniqueElementsSet = new Set();
  1273. baseElementNames.forEach(name => cumulativeUniqueElementsSet.add(name));
  1274.  
  1275.  
  1276. for (let i = 0; i <= maxDiscoveredLevel; i++) {
  1277. const levelDiv = document.createElement('div');
  1278. levelDiv.classList.add('stats-level');
  1279.  
  1280. const actualDiscoveriesOnLevel = elementsByLevel[i] ? elementsByLevel[i].length : 0;
  1281.  
  1282. let potentialDiscoveriesForLevelText;
  1283. if (i === 0) {
  1284. potentialDiscoveriesForLevelText = baseElementNames.length;
  1285. } else {
  1286. const numPrevUniqueForText = cumulativeUniqueElementsSet.size;
  1287. potentialDiscoveriesForLevelText = numPrevUniqueForText * (numPrevUniqueForText + 1) / 2;
  1288. }
  1289. levelDiv.innerHTML = `<strong>Étage ${i}:</strong> ${actualDiscoveriesOnLevel} / ${Math.round(potentialDiscoveriesForLevelText)} découvertes`;
  1290.  
  1291. const elementsUpToLMinus1 = new Set(cumulativeUniqueElementsSet);
  1292. const countUniqueElementsUpToLMinus1 = elementsUpToLMinus1.size;
  1293. const totalPossibleInputCombinations = countUniqueElementsUpToLMinus1 * (countUniqueElementsUpToLMinus1 + 1) / 2;
  1294.  
  1295. const uniqueTestedInputPairsForLevel = new Set();
  1296. if (i > -1) {
  1297. allRecipes.forEach(recipe => {
  1298. const outputElement = allElements.get(recipe.output);
  1299. if (outputElement && outputElement.level === i) {
  1300. const inputsAreFromPreviousLevels = recipe.inputs.every(inputName => elementsUpToLMinus1.has(inputName));
  1301. if (inputsAreFromPreviousLevels) {
  1302. uniqueTestedInputPairsForLevel.add(recipe.inputs.sort().join('+'));
  1303. }
  1304. }
  1305. });
  1306. }
  1307. const actualTestedInputCombinations = uniqueTestedInputPairsForLevel.size;
  1308.  
  1309. levelDiv.dataset.tested = actualTestedInputCombinations;
  1310. levelDiv.dataset.possible = totalPossibleInputCombinations;
  1311. levelDiv.dataset.levelNum = i;
  1312.  
  1313. if (elementsByLevel[i]) {
  1314. elementsByLevel[i].forEach(el => cumulativeUniqueElementsSet.add(el.name));
  1315. }
  1316.  
  1317. statsContent.appendChild(levelDiv);
  1318.  
  1319. levelDiv.addEventListener('mouseenter', handleStatsMouseEnter);
  1320. levelDiv.addEventListener('mousemove', handleStatsMouseMove);
  1321. levelDiv.addEventListener('mouseleave', handleStatsMouseLeave);
  1322. }
  1323. }
  1324.  
  1325. function handleStatsMouseEnter(event) {
  1326. const targetDiv = event.currentTarget;
  1327. const tested = parseInt(targetDiv.dataset.tested, 10);
  1328. const possible = parseInt(targetDiv.dataset.possible, 10);
  1329. const levelNum = targetDiv.dataset.levelNum;
  1330.  
  1331. if (isNaN(tested) || isNaN(possible)) return;
  1332.  
  1333. const percentage = possible > 0 ? ((tested / possible) * 100).toFixed(1) : "N/A";
  1334.  
  1335. statsTooltip.innerHTML = `
  1336. <strong>Étage ${levelNum} - Combinaisons :</strong><br>
  1337. Testées : ${tested}<br>
  1338. Possibles (basées sur étages précédents) : ${possible}<br>
  1339. Exploration : ${percentage}%
  1340. `;
  1341. statsTooltip.style.display = 'block';
  1342. }
  1343.  
  1344. function handleStatsMouseMove(event) {
  1345. statsTooltip.style.left = `${event.pageX + 15}px`;
  1346. statsTooltip.style.top = `${event.pageY + 15}px`;
  1347. }
  1348.  
  1349. function handleStatsMouseLeave() {
  1350. statsTooltip.style.display = 'none';
  1351. }
  1352.  
  1353.  
  1354. // --- Animation de Fond ---
  1355. const bgCanvas = document.getElementById('background-canvas');
  1356. const bgCtx = bgCanvas.getContext('2d');
  1357. let bgLines = [];
  1358. let animationFrameId;
  1359.  
  1360.  
  1361. function resizeBgCanvas() {
  1362. if (!bgCanvas) return;
  1363. bgCanvas.width = bgCanvas.offsetWidth;
  1364. bgCanvas.height = bgCanvas.offsetHeight;
  1365. if(bgLines.length > 0 || bgCanvas.width > 0 && bgCanvas.height > 0) initBgLines();
  1366. }
  1367.  
  1368. function initBgLines() {
  1369. if (!bgCanvas || !bgCtx) return;
  1370. bgLines = [];
  1371. const numLines = Math.min(30, Math.floor(bgCanvas.width * bgCanvas.height / 30000));
  1372. for (let i = 0; i < numLines; i++) {
  1373. bgLines.push({
  1374. x: Math.random() * bgCanvas.width,
  1375. y: Math.random() * bgCanvas.height,
  1376. length: Math.random() * 60 + 30,
  1377. speed: Math.random() * 0.15 + 0.03,
  1378. angle: Math.random() * Math.PI * 2,
  1379. opacity: Math.random() * 0.03 + 0.01
  1380. });
  1381. }
  1382. }
  1383.  
  1384. function drawBgAnimation() {
  1385. if (!bgCtx || !bgCanvas || bgCanvas.width === 0 || bgCanvas.height === 0) {
  1386. animationFrameId = requestAnimationFrame(drawBgAnimation);
  1387. return;
  1388. }
  1389. bgCtx.clearRect(0, 0, bgCanvas.width, bgCanvas.height);
  1390. bgLines.forEach(line => {
  1391. bgCtx.beginPath();
  1392. bgCtx.moveTo(line.x, line.y);
  1393.  
  1394. line.x += Math.cos(line.angle) * line.speed;
  1395. line.y += Math.sin(line.angle) * line.speed;
  1396.  
  1397. if (line.x < -line.length) line.x = bgCanvas.width + line.length;
  1398. if (line.x > bgCanvas.width + line.length) line.x = -line.length;
  1399. if (line.y < -line.length) line.y = bgCanvas.height + line.length;
  1400. if (line.y > bgCanvas.height + line.length) line.y = -line.length;
  1401.  
  1402. const endX = line.x - Math.cos(line.angle) * line.length;
  1403. const endY = line.y - Math.sin(line.angle) * line.length;
  1404.  
  1405. bgCtx.lineTo(endX, endY);
  1406. bgCtx.strokeStyle = `rgba(0, 123, 255, ${line.opacity})`;
  1407. bgCtx.lineWidth = 0.5;
  1408. bgCtx.stroke();
  1409. });
  1410. animationFrameId = requestAnimationFrame(drawBgAnimation);
  1411. }
  1412.  
  1413. window.onload = () => {
  1414. initializeGraph();
  1415. resizeBgCanvas();
  1416. initBgLines();
  1417.  
  1418. if (animationFrameId) cancelAnimationFrame(animationFrameId);
  1419. drawBgAnimation();
  1420.  
  1421. window.addEventListener('resize', () => {
  1422. resizeBgCanvas();
  1423. if (animationFrameId) cancelAnimationFrame(animationFrameId);
  1424. drawBgAnimation();
  1425. });
  1426. };
  1427.  
  1428. </script>
  1429. </body>
  1430. </html>
  1431.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement