Milkyway Idle - Current Loot Tracker

Tracks loot with overlay, total coin value via ask prices, improved UI/CSS, and fixed display logic.

  1. // ==UserScript==
  2. // @name Milkyway Idle - Current Loot Tracker
  3. // @namespace https://milkywayidle.com/
  4. // @version 2.1
  5. // @description Tracks loot with overlay, total coin value via ask prices, improved UI/CSS, and fixed display logic.
  6. // @match https://www.milkywayidle.com/*
  7. // @grant none
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. "use strict";
  13.  
  14. const playerLootData = {};
  15. const previousLootCounts = {};
  16. const lastBattleLoot = {};
  17. let myPlayerName = null;
  18. let activePlayer = null;
  19. let selfTabSelected = false;
  20. let isMinimized = localStorage.getItem("lootOverlayMinimized") === "true";
  21. let isLootListMinimized =
  22. localStorage.getItem("lootListMinimized") === "true";
  23. let overlayReady = false;
  24.  
  25. let marketData = {};
  26.  
  27. fetch(
  28. "https://raw.githubusercontent.com/holychikenz/MWIApi/main/medianmarket.json"
  29. )
  30. .then((response) => {
  31. if (!response.ok) {
  32. throw new Error(`HTTP error! status: ${response.status}`);
  33. }
  34. return response.json();
  35. })
  36. .then((data) => {
  37. marketData = data;
  38. console.log("[LootTracker] Market data loaded successfully.");
  39. if (activePlayer && document.getElementById("lootOverlay")) {
  40. updateLootDisplay(activePlayer);
  41. }
  42. })
  43. .catch((err) =>
  44. console.error("[LootTracker] Failed to load market data:", err)
  45. );
  46.  
  47. function formatGold(value) {
  48. const numValue = Number(value) || 0;
  49. return Math.round(numValue).toLocaleString() + " coin";
  50. }
  51.  
  52. function detectPlayerName() {
  53. const nameDiv =
  54. document.querySelector(".CharacterStatus_playerName__XXXXX") ||
  55. document.querySelector(".CharacterName_name__1amXp[data-name]");
  56.  
  57. if (nameDiv) {
  58. myPlayerName = nameDiv.dataset.name || nameDiv.textContent.trim();
  59. if (
  60. overlayReady &&
  61. myPlayerName &&
  62. playerLootData[myPlayerName] &&
  63. !selfTabSelected
  64. ) {
  65. selfTabSelected = true;
  66. switchTab(myPlayerName);
  67. }
  68. } else {
  69. setTimeout(detectPlayerName, 1000);
  70. }
  71. }
  72.  
  73. function createOverlay() {
  74. if (overlayReady || document.getElementById("lootOverlay")) return;
  75. overlayReady = true;
  76.  
  77. const panel = document.createElement("div");
  78. panel.id = "lootOverlay";
  79. panel.style.top = localStorage.getItem("lootOverlayTop") || "100px";
  80. panel.style.left = localStorage.getItem("lootOverlayLeft") || "20px";
  81.  
  82. panel.innerHTML = `
  83. <div id="lootHeader">
  84. <span id="lootTitle">📦 Current Loot</span>
  85. <div id="lootHeaderButtons">
  86. <button id="lootExportBtn" class="loot-btn" data-tooltip="Export current player's loot as CSV">CSV</button>
  87. <button id="lootClearBtn" class="loot-btn" data-tooltip="Clear ALL tracked loot">⟳</button>
  88. <button id="lootMinBtn" class="loot-btn" data-tooltip="Minimize/Restore Overlay">
  89. ${isMinimized ? "+" : "−"}
  90. </button>
  91. </div>
  92. </div>
  93. <div id="lootContent">
  94. <div id="lootTabs"></div>
  95. <div id="lootToggleHeader">
  96. Loot <span id="lootToggleIcon">${
  97. isLootListMinimized ? "▲" : "▼"
  98. }</span>
  99. </div>
  100. <div id="lootTotals"></div>
  101. <div id="lootBottomDragger">
  102. <div id="lootRevenueLine">Total Value: Calculating...</div>
  103. <div class="drag-spacer"></div>
  104. </div>
  105. </div>
  106. `;
  107.  
  108. document.body.appendChild(panel);
  109.  
  110. const style = document.createElement("style");
  111. style.textContent = `
  112. #lootOverlay {
  113. position: fixed;
  114. width: 260px;
  115. background: rgba(30, 30, 30, 0.95);
  116. color: #fff;
  117. font-family: monospace;
  118. font-size: 13px;
  119. border: 1px solid #555;
  120. border-radius: 8px;
  121. z-index: 99999;
  122. user-select: none;
  123. box-shadow: 0 4px 10px rgba(0,0,0,0.4);
  124. }
  125. #lootHeader {
  126. display: flex; justify-content: space-between; align-items: center;
  127. padding: 6px 10px; background: rgba(20, 20, 20, 0.85);
  128. border-bottom: 1px solid #333; border-radius: 8px 8px 0 0; cursor: move;
  129. }
  130. #lootTitle { font-weight: bold; }
  131. #lootHeaderButtons { display: flex; gap: 4px; }
  132. .loot-btn {
  133. background: none; border: none; color: #aaa; cursor: pointer;
  134. font-size: 14px; padding: 0 3px; position: relative;
  135. }
  136. .loot-btn:hover { color: #fff; }
  137. .loot-btn:hover::after {
  138. content: attr(data-tooltip); position: absolute; left: 50%; top: 110%;
  139. transform: translateX(-50%); background: #222; color: #fff; padding: 4px 8px;
  140. font-size: 11px; border-radius: 4px; white-space: nowrap; opacity: 0.95;
  141. pointer-events: none; z-index: 100000;
  142. }
  143. #lootContent {
  144. overflow: hidden; transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
  145. will-change: max-height, opacity;
  146. }
  147. #lootTabs {
  148. display: flex; flex-wrap: wrap; padding: 5px 10px; gap: 6px;
  149. border-bottom: 1px solid #333; background: rgba(24, 24, 24, 0.8); min-height: 26px;
  150. }
  151. #lootTabs button {
  152. background: none; border: 1px solid #444; color: #aaa; padding: 2px 6px;
  153. font-family: monospace; cursor: pointer; border-radius: 4px; font-size: 12px;
  154. transition: background-color 0.2s, color 0.2s, border-color 0.2s;
  155. }
  156. #lootTabs button:hover { background-color: #555; color: #fff; }
  157. #lootTabs button.active {
  158. background: #4caf50; color: #fff; border-color: #4caf50; font-weight: bold;
  159. }
  160. #lootToggleHeader {
  161. padding: 6px 10px; cursor: pointer; font-weight: bold; border-bottom: 1px solid #333;
  162. background: rgba(28, 28, 28, 0.8);
  163. }
  164. #lootToggleHeader:hover { background: rgba(40, 40, 40, 0.9); }
  165. #lootToggleIcon { display: inline-block; transition: transform 0.2s ease-out; margin-left: 5px; }
  166. #lootTotals {
  167. padding: 10px; overflow-y: auto; max-height: 400px;
  168. transition: max-height 0.3s ease-out, opacity 0.3s ease-out, padding 0.3s ease-out;
  169. will-change: max-height, opacity, padding;
  170. }
  171. #lootTotals > div { margin-bottom: 3px; line-height: 1.3; }
  172. #lootBottomDragger {
  173. padding: 6px 10px; cursor: move; border-top: 1px solid #444;
  174. background: rgba(20, 20, 20, 0.85); border-radius: 0 0 8px 8px;
  175. }
  176. #lootRevenueLine {
  177. font-weight: bold; color: gold; cursor: inherit; padding-bottom: 4px;
  178. white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  179. }
  180. .drag-spacer { height: 8px; cursor: inherit; }
  181. @keyframes lootFlashText { 0% { color: #b6ffb8; transform: scale(1.02); } 100% { color: white; transform: scale(1); } }
  182. .flashLoot { animation: lootFlashText 1s ease-out; }
  183. .fadeGain {
  184. color: lime; font-weight: bold; font-size: 10px; vertical-align: super;
  185. opacity: 1; transition: opacity 2s ease-out; margin-left: 3px; display: inline-block;
  186. }
  187. `;
  188. document.head.appendChild(style);
  189.  
  190. const content = document.getElementById("lootContent");
  191. const lootTotals = document.getElementById("lootTotals");
  192. content.style.maxHeight = isMinimized ? "0" : "1000px";
  193. content.style.opacity = isMinimized ? "0" : "1";
  194. lootTotals.style.maxHeight = isLootListMinimized ? "0" : "400px";
  195. lootTotals.style.opacity = isLootListMinimized ? "0" : "1";
  196. lootTotals.style.padding = isLootListMinimized ? "0 10px" : "10px";
  197.  
  198. document.getElementById("lootMinBtn").onclick = () => {
  199. isMinimized = !isMinimized;
  200. content.style.maxHeight = isMinimized ? "0" : "1000px";
  201. content.style.opacity = isMinimized ? "0" : "1";
  202. document.getElementById("lootMinBtn").textContent = isMinimized
  203. ? "+"
  204. : "−";
  205. localStorage.setItem("lootOverlayMinimized", isMinimized);
  206. };
  207. document.getElementById("lootToggleHeader").onclick = () => {
  208. isLootListMinimized = !isLootListMinimized;
  209. lootTotals.style.maxHeight = isLootListMinimized ? "0" : "400px";
  210. lootTotals.style.opacity = isLootListMinimized ? "0" : "1";
  211. lootTotals.style.padding = isLootListMinimized ? "0 10px" : "10px";
  212. document.getElementById("lootToggleIcon").textContent =
  213. isLootListMinimized ? "▲" : "▼";
  214. localStorage.setItem("lootListMinimized", isLootListMinimized);
  215. };
  216. const exportBtn = document.getElementById("lootExportBtn");
  217. exportBtn.onclick = () => {
  218. if (
  219. !activePlayer ||
  220. !playerLootData[activePlayer] ||
  221. Object.keys(playerLootData[activePlayer]).length === 0
  222. ) {
  223. alert("No loot data available for the active player to export.");
  224. return;
  225. }
  226. try {
  227. const dataToExport = playerLootData[activePlayer];
  228. const csvContent = Object.entries(dataToExport)
  229. .map(([hrid, count]) => {
  230. let itemName = hrid.replace("/items/", "").replace(/_/g, " ");
  231. itemName = `"${itemName.replace(/"/g, '""')}"`;
  232. return `${itemName},${count}`;
  233. })
  234. .join("\n");
  235. const csvOutput = "Item Name,Count\n" + csvContent;
  236. navigator.clipboard
  237. .writeText(csvOutput)
  238. .then(() => {
  239. const originalText = exportBtn.textContent;
  240. exportBtn.textContent = "Copied!";
  241. exportBtn.style.color = "#4caf50";
  242. setTimeout(() => {
  243. exportBtn.textContent = originalText;
  244. exportBtn.style.color = "";
  245. }, 1500);
  246. })
  247. .catch((err) => {
  248. console.error(
  249. "[LootTracker] Failed to copy CSV to clipboard:",
  250. err
  251. );
  252. alert("Failed to copy CSV. See console.");
  253. });
  254. } catch (error) {
  255. console.error("[LootTracker] Error generating CSV:", error);
  256. alert("Error generating CSV data.");
  257. }
  258. };
  259. document.getElementById("lootClearBtn").onclick = () => {
  260. if (
  261. confirm(
  262. "Are you sure you want to clear ALL tracked loot data? This cannot be undone."
  263. )
  264. ) {
  265. clearAllLootData();
  266. }
  267. };
  268.  
  269. let dragging = false;
  270. let offsetX = 0;
  271. let offsetY = 0;
  272. function beginDrag(e) {
  273. if (e.target.closest("button")) return;
  274. dragging = true;
  275. panel.style.transition = "none";
  276. offsetX = e.clientX - panel.offsetLeft;
  277. offsetY = e.clientY - panel.offsetTop;
  278. document.body.style.userSelect = "none";
  279. document.body.style.cursor = "move";
  280. }
  281. document
  282. .getElementById("lootHeader")
  283. .addEventListener("mousedown", beginDrag);
  284. document
  285. .getElementById("lootBottomDragger")
  286. .addEventListener("mousedown", beginDrag);
  287. document.addEventListener("mousemove", (e) => {
  288. if (!dragging) return;
  289. const newX = Math.max(
  290. 0,
  291. Math.min(window.innerWidth - panel.offsetWidth, e.clientX - offsetX)
  292. );
  293. const newY = Math.max(
  294. 0,
  295. Math.min(window.innerHeight - panel.offsetHeight, e.clientY - offsetY)
  296. );
  297. panel.style.left = `${newX}px`;
  298. panel.style.top = `${newY}px`;
  299. });
  300. document.addEventListener("mouseup", () => {
  301. if (!dragging) return;
  302. dragging = false;
  303. panel.style.transition = "";
  304. document.body.style.userSelect = "";
  305. document.body.style.cursor = "";
  306. localStorage.setItem("lootOverlayTop", panel.style.top);
  307. localStorage.setItem("lootOverlayLeft", panel.style.left);
  308. });
  309.  
  310. console.log("[LootTracker] Overlay created.");
  311. }
  312.  
  313. function updateLootDisplay(playerName) {
  314. const container = document.getElementById("lootTotals");
  315. const revenueLine = document.getElementById("lootRevenueLine");
  316.  
  317. if (!container) {
  318. console.error(
  319. "[LootTracker] updateLootDisplay: Could not find #lootTotals element!"
  320. );
  321. if (revenueLine) revenueLine.textContent = "Total Value: Error (UI)";
  322. return;
  323. }
  324. if (!revenueLine) {
  325. console.warn(
  326. "[LootTracker] updateLootDisplay: Could not find #lootRevenueLine element."
  327. );
  328. }
  329.  
  330. if (!playerLootData[playerName]) {
  331. container.innerHTML = "<i>Waiting for player data...</i>";
  332. if (revenueLine) revenueLine.textContent = "Total Value: N/A";
  333. return;
  334. }
  335. if (!previousLootCounts[playerName]) previousLootCounts[playerName] = {};
  336.  
  337. const currentLoot = playerLootData[playerName];
  338. const sorted = Object.entries(currentLoot).sort(
  339. (a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
  340. );
  341.  
  342. let html = "";
  343. let totalRevenue = 0;
  344. let marketDataAvailable =
  345. marketData &&
  346. marketData.market &&
  347. Object.keys(marketData.market).length > 0;
  348.  
  349. if (sorted.length === 0) {
  350. html = "<i>No loot tracked yet.</i>";
  351. totalRevenue = 0;
  352. } else {
  353. sorted.forEach(([itemHrid, count]) => {
  354. const prevDisplayCount = previousLootCounts[playerName][itemHrid] || 0;
  355. const lastBattleStartCount =
  356. lastBattleLoot[playerName] && lastBattleLoot[playerName][itemHrid]
  357. ? lastBattleLoot[playerName][itemHrid]
  358. : prevDisplayCount;
  359. const gain = count - lastBattleStartCount;
  360. const flash = count > prevDisplayCount;
  361. const name = itemHrid.replace("/items/", "").replace(/_/g, " ");
  362. const gainHTML =
  363. gain > 0 ? `<span class="fadeGain">+${gain}</span>` : "";
  364.  
  365. let itemValue = 0;
  366. let priceFound = false;
  367. if (itemHrid.endsWith("/coin")) {
  368. itemValue = count;
  369. priceFound = true;
  370. } else if (marketDataAvailable) {
  371. const marketKey = name
  372. .split(" ")
  373. .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
  374. .join(" ");
  375. if (marketData.market[marketKey]?.ask) {
  376. itemValue = count * marketData.market[marketKey].ask;
  377. priceFound = true;
  378. }
  379. }
  380. totalRevenue += itemValue;
  381.  
  382. html += `<div class="${flash ? "flashLoot" : ""}">
  383. ${name} × ${count}${gainHTML} ${
  384. !priceFound && !itemHrid.endsWith("/coin")
  385. ? '<span style="color:gray;" title="Price data unavailable">?</span>'
  386. : ""
  387. }
  388. </div>`;
  389. previousLootCounts[playerName][itemHrid] = count;
  390. });
  391. }
  392.  
  393. const hasNonCoinItems = sorted.some(([hrid]) => !hrid.endsWith("/coin"));
  394. let finalRevenueText = "";
  395. if (!marketDataAvailable && hasNonCoinItems && sorted.length > 0) {
  396. finalRevenueText = `Total Value: Calculating...`;
  397. } else if (sorted.length === 0) {
  398. finalRevenueText = `Total Value: ${formatGold(0)}`;
  399. } else {
  400. finalRevenueText = `Total Value: ${formatGold(totalRevenue)}`;
  401. }
  402.  
  403. try {
  404. container.innerHTML = html;
  405.  
  406. if (revenueLine) {
  407. revenueLine.textContent = finalRevenueText;
  408. }
  409. } catch (uiError) {
  410. console.error(
  411. `[LootTracker] CRITICAL: Error occurred during DOM update!`,
  412. uiError
  413. );
  414. }
  415.  
  416. document.querySelectorAll(".fadeGain").forEach((el) => {
  417. setTimeout(() => {
  418. void el.offsetWidth;
  419. el.style.opacity = "0";
  420. }, 100);
  421. });
  422. if (
  423. lastBattleLoot[playerName] &&
  424. Object.keys(lastBattleLoot[playerName]).length > 0
  425. ) {
  426. lastBattleLoot[playerName] = {};
  427. }
  428. }
  429.  
  430. function switchTab(playerName) {
  431. activePlayer = playerName;
  432.  
  433. document.querySelectorAll("#lootTabs button").forEach((btn) => {
  434. btn.classList.toggle("active", btn.dataset.name === playerName);
  435. });
  436. updateLootDisplay(playerName);
  437. }
  438.  
  439. function addTab(player) {
  440. const playerName = player.name;
  441. const lootMap = player.totalLootMap || {};
  442. const container = document.getElementById("lootTabs");
  443. if (!container) {
  444. console.error("[LootTracker] Loot tabs container not found!");
  445. return;
  446. }
  447. if (!playerLootData[playerName]) playerLootData[playerName] = {};
  448. if (!previousLootCounts[playerName]) previousLootCounts[playerName] = {};
  449. if (!lastBattleLoot[playerName]) lastBattleLoot[playerName] = {};
  450.  
  451. let tabNeedsUpdate = false;
  452. for (const key in lootMap) {
  453. const { itemHrid, count } = lootMap[key];
  454. if (playerLootData[playerName][itemHrid] !== count) {
  455. lastBattleLoot[playerName][itemHrid] =
  456. playerLootData[playerName][itemHrid] || 0;
  457. playerLootData[playerName][itemHrid] = count;
  458. tabNeedsUpdate = true;
  459. }
  460. }
  461.  
  462. let tabButton = container.querySelector(
  463. `button[data-name="${playerName}"]`
  464. );
  465. if (!tabButton) {
  466. tabButton = document.createElement("button");
  467. tabButton.textContent = playerName;
  468. tabButton.dataset.name = playerName;
  469. tabButton.onclick = () => switchTab(playerName);
  470. container.appendChild(tabButton);
  471.  
  472. if (!activePlayer) activePlayer = playerName;
  473. }
  474.  
  475. if (playerName === myPlayerName && !selfTabSelected) {
  476. selfTabSelected = true;
  477. switchTab(playerName);
  478. tabNeedsUpdate = false;
  479. } else if (playerName === activePlayer && tabNeedsUpdate) {
  480. updateLootDisplay(playerName);
  481. }
  482. if (playerName === activePlayer) {
  483. document.querySelectorAll("#lootTabs button").forEach((btn) => {
  484. btn.classList.toggle("active", btn.dataset.name === activePlayer);
  485. });
  486. }
  487. }
  488.  
  489. function clearAllLootData() {
  490. console.log("[LootTracker] Clearing all loot data.");
  491.  
  492. for (const p in playerLootData) {
  493. playerLootData[p] = {};
  494. previousLootCounts[p] = {};
  495. lastBattleLoot[p] = {};
  496. }
  497.  
  498. const tabsContainer = document.getElementById("lootTabs");
  499. const totalsContainer = document.getElementById("lootTotals");
  500. const revenueLine = document.getElementById("lootRevenueLine");
  501.  
  502. if (tabsContainer) tabsContainer.innerHTML = "";
  503. if (totalsContainer)
  504. totalsContainer.innerHTML = "<i>Loot data cleared.</i>";
  505. if (revenueLine) revenueLine.textContent = "Total Value: N/A";
  506.  
  507. activePlayer = null;
  508. selfTabSelected = false;
  509. }
  510.  
  511. (function injectWebSocketInterceptor() {
  512. const scriptId = "milkyway-websocket-interceptor";
  513.  
  514. if (document.getElementById(scriptId)) return;
  515.  
  516. const s = document.createElement("script");
  517. s.id = scriptId;
  518. s.textContent = `
  519. (function() {
  520.  
  521. if (window.originalWebSocket) { return; }
  522. window.originalWebSocket = window.WebSocket;
  523.  
  524.  
  525. window.WebSocket = new Proxy(window.originalWebSocket, {
  526. construct(target, args) {
  527.  
  528. const wsInstance = new target(...args);
  529. try {
  530. const url = args[0];
  531.  
  532. if (typeof url === 'string' && (url.includes("api.milkywayidle.com/ws") || url.includes("api-test.milkywayidle.com/ws"))) {
  533.  
  534.  
  535. wsInstance.addEventListener("message", (event) => {
  536. try {
  537. const data = JSON.parse(event.data);
  538.  
  539. if (data.type === "new_battle" && data.players) {
  540.  
  541. window.dispatchEvent(new CustomEvent("LootTrackerBattle", { detail: data }));
  542. }
  543.  
  544. else if ( data.type === "new_character_action" && data.newCharacterActionData?.shouldClearQueue && data.newCharacterActionData.actionHrid?.startsWith("/actions/combat/") ) {
  545.  
  546. window.dispatchEvent(new CustomEvent("LootTrackerCombatReset"));
  547. }
  548. } catch (parseOrDispatchError) {
  549. console.error('[LootTracker WS Interceptor] Error processing message:', parseOrDispatchError, 'Raw Data:', event.data);
  550. }
  551. });
  552.  
  553.  
  554. wsInstance.addEventListener("open", () => {
  555.  
  556. });
  557.  
  558.  
  559. wsInstance.addEventListener("close", (event) => {
  560.  
  561. console.log(\`[LootTracker WS Interceptor] Target WebSocket connection closed. Code: \${event.code}, Reason: \${event.reason}. Dispatching LootTrackerWSClosed event.\`);
  562.  
  563. window.dispatchEvent(new CustomEvent("LootTrackerWSClosed", {
  564. detail: { code: event.code, reason: event.reason }
  565. }));
  566. });
  567.  
  568.  
  569. wsInstance.addEventListener("error", (event) => {
  570. console.error('[LootTracker WS Interceptor] Target WebSocket error:', event);
  571. });
  572.  
  573. }
  574. } catch (proxyConstructError) {
  575. console.error('[LootTracker WS Interceptor] Error setting up WebSocket proxy:', proxyConstructError);
  576. }
  577.  
  578. return wsInstance;
  579. }
  580. });
  581.  
  582. console.log('[LootTracker WS Interceptor] WebSocket Proxy installed.');
  583. })();
  584. `;
  585.  
  586. (document.head || document.documentElement).appendChild(s);
  587. })();
  588.  
  589. window.addEventListener("LootTrackerBattle", (e) => {
  590. if (!overlayReady) {
  591. console.warn(
  592. "[LootTracker] Overlay not ready when battle event received, skipping update."
  593. );
  594. return;
  595. }
  596. const data = e.detail;
  597.  
  598. if (data && data.players && Array.isArray(data.players)) {
  599. data.players.forEach((player) => {
  600. if (player && player.name) {
  601. addTab(player);
  602. } else {
  603. console.warn(
  604. "[LootTracker] Player data missing name in battle event:",
  605. player
  606. );
  607. }
  608. });
  609. } else {
  610. console.warn(
  611. "[LootTracker] Invalid data received in LootTrackerBattle event:",
  612. data
  613. );
  614. }
  615. });
  616.  
  617. window.addEventListener("LootTrackerWSClosed", (e) => {
  618. console.log(
  619. `[LootTracker] Detected WebSocket closure (Code: ${e.detail?.code}, Reason: ${e.detail?.reason}). Clearing all loot data and resetting player name.`
  620. );
  621.  
  622. myPlayerName = null;
  623.  
  624. activePlayer = null;
  625. selfTabSelected = false;
  626.  
  627. if (overlayReady) {
  628. clearAllLootData();
  629. } else {
  630. console.warn(
  631. "[LootTracker] WebSocket closed, but overlay not ready. Data should be clear on next init."
  632. );
  633. }
  634. });
  635.  
  636. window.addEventListener("LootTrackerCombatReset", (e) => {
  637. if (!overlayReady) {
  638. console.warn(
  639. "[LootTracker] Overlay not ready when reset event received, skipping clear."
  640. );
  641. return;
  642. }
  643. console.log("[LootTracker] Calling clearAllLootData due to combat reset.");
  644. clearAllLootData();
  645. });
  646.  
  647. function initialize() {
  648. console.log("[LootTracker] Initializing...");
  649. createOverlay();
  650. detectPlayerName();
  651. }
  652. if (document.readyState === "loading") {
  653. document.addEventListener("DOMContentLoaded", initialize);
  654. } else {
  655. initialize();
  656. }
  657. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址