Klavia Points Tracker Lib

Tracks Nitro Type race points with performance stats UI Lib

สคริปต์นี้ไม่ควรถูกติดตั้งโดยตรง มันเป็นคลังสำหรับสคริปต์อื่น ๆ เพื่อบรรจุด้วยคำสั่งเมทา // @require https://update.greatest.deepsurf.us/scripts/532760/1570608/Klavia%20Points%20Tracker%20Lib.js

  1. // ==UserScript==
  2. // @name Klavia Points Tracker Lib
  3. // @version 2024-04-13.l1
  4. // @namespace https://greatest.deepsurf.us/users/1331131-tensorflow-dvorak
  5. // @description Tracks Nitro Type race points with performance stats UI Lib
  6. // @author TensorFlow - Dvorak
  7. // @match *://*.ntcomps.com/*
  8. // @run-at document-start
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function injectRaceLogger() {
  13. const loggerCode = `
  14. (function () {
  15. const STORAGE_KEY = "klaviaRaceHistory";
  16. const seenRaceIDs = new Set();
  17. const estimatePoints = (wpm, accuracy) => Math.round(Math.pow(wpm, 1) * Math.pow(accuracy, 2.5) * 0.000027);
  18.  
  19. function loadRaceHistory() {
  20. try {
  21. return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
  22. } catch {
  23. return [];
  24. }
  25. }
  26.  
  27. function saveRaceHistory(history) {
  28. localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
  29. }
  30.  
  31. function attachRaceLogger(ws) {
  32. ws.addEventListener("message", (event) => {
  33. let parsed;
  34. try {
  35. parsed = JSON.parse(event.data);
  36. } catch {
  37. return;
  38. }
  39.  
  40. const identifier = parsed.identifier ? JSON.parse(parsed.identifier) : {};
  41. const msg = parsed.message;
  42.  
  43. if (
  44. identifier.channel === "RaceChannel" &&
  45. msg?.message === "update_race_results" &&
  46. msg?.textCompleted === true &&
  47. msg?.raceId &&
  48. !seenRaceIDs.has(msg.raceId)
  49. ) {
  50. seenRaceIDs.add(msg.raceId);
  51.  
  52. const raceData = {
  53. raceId: msg.raceId,
  54. points: estimatePoints(msg.wpm, parseFloat(msg.accuracy)),
  55. wpm: msg.wpm,
  56. accuracy: parseFloat(msg.accuracy),
  57. raceSeconds: msg.raceSeconds,
  58. textSeconds: msg.textSeconds,
  59. boostBonus: msg.boostBonus,
  60. timestamp: new Date().toISOString(),
  61. };
  62.  
  63. const history = loadRaceHistory();
  64. history.unshift(raceData);
  65. saveRaceHistory(history);
  66. window.dispatchEvent(new CustomEvent("klavia:race-logged", { detail: raceData }));
  67. }
  68. });
  69. }
  70.  
  71. const originalWS = window.WebSocket;
  72. window.WebSocket = new Proxy(originalWS, {
  73. construct(target, args) {
  74. const ws = new target(...args);
  75. attachRaceLogger(ws);
  76. return ws;
  77. }
  78. });
  79. })();
  80. `;
  81. const script = document.createElement("script");
  82. script.textContent = loggerCode;
  83. document.documentElement.appendChild(script);
  84. })();
  85.  
  86. window.addEventListener("DOMContentLoaded", () => {
  87. const STORAGE_KEY = "klaviaRaceHistory";
  88. let raceHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
  89. let activeTab = "stats";
  90. let isUIVisible = false;
  91. let lastHistoryJSON = JSON.stringify(raceHistory);
  92.  
  93. const createElement = (tag, attrs = {}, styles = {}, html = "") => {
  94. const el = document.createElement(tag);
  95. Object.assign(el, attrs);
  96. Object.assign(el.style, styles);
  97. el.innerHTML = html;
  98. return el;
  99. };
  100.  
  101. function getColor(value, values) {
  102. const sorted = [...values].sort((a, b) => a - b);
  103. const low = sorted[Math.floor(sorted.length * 0.33)];
  104. const high = sorted[Math.floor(sorted.length * 0.66)];
  105. if (value >= high) return "#4CAF50";
  106. if (value >= low) return "#FFC107";
  107. return "#F44336";
  108. }
  109.  
  110. function createStatsUI() {
  111. const oldUI = document.getElementById("klavia-stats");
  112. if (oldUI) oldUI.remove();
  113.  
  114. const container = createElement(
  115. "div",
  116. { id: "klavia-stats" },
  117. {
  118. position: "fixed",
  119. top: "10px",
  120. right: "10px",
  121. background: isUIVisible ? "#121212" : "none",
  122. color: "#e0e0e0",
  123. padding: isUIVisible ? "20px" : "0",
  124. borderRadius: "12px",
  125. zIndex: 9999,
  126. maxWidth: "600px",
  127. maxHeight: "80vh",
  128. overflowY: isUIVisible ? "auto" : "visible",
  129. boxShadow: isUIVisible ? "0 4px 20px rgba(0,0,0,0.3)" : "none",
  130. fontFamily: "Segoe UI, sans-serif",
  131. }
  132. );
  133.  
  134. const infoBtn = createElement(
  135. "button",
  136. {
  137. onclick: () => {
  138. isUIVisible = !isUIVisible;
  139. renderStatsUI();
  140. },
  141. },
  142. {
  143. position: "absolute",
  144. top: "10px",
  145. right: "10px",
  146. backgroundColor: "#ff4500",
  147. color: "white",
  148. border: "none",
  149. borderRadius: "50%",
  150. width: "40px",
  151. height: "40px",
  152. fontSize: "14px",
  153. fontWeight: "bold",
  154. cursor: "pointer",
  155. fontFamily: "Segoe UI, sans-serif",
  156. boxShadow: "0 2px 6px rgba(0,0,0,0.3)",
  157. },
  158. "DTR"
  159. );
  160.  
  161. const tabs = createElement(
  162. "div",
  163. {},
  164. {
  165. display: isUIVisible ? "flex" : "none",
  166. gap: "10px",
  167. paddingRight: '2rem',
  168. marginBottom: "16px",
  169. }
  170. );
  171.  
  172. const createTabBtn = (name, label) =>
  173. createElement(
  174. "button",
  175. {
  176. onclick: () => {
  177. activeTab = name;
  178. renderStatsUI();
  179. },
  180. },
  181. {
  182. padding: "6px 12px",
  183. background: activeTab === name ? "#1976d2" : "#333",
  184. color: "#fff",
  185. border: "none",
  186. borderRadius: "4px",
  187. cursor: "pointer",
  188. },
  189. label
  190. );
  191.  
  192. tabs.append(
  193. createTabBtn("stats", "Stats"),
  194. createTabBtn("table", "Race Table"),
  195. createTabBtn("analysis", "Analysis"),
  196. createTabBtn("teamChat", "Team Chat")
  197. );
  198.  
  199. const content = createElement(
  200. "div",
  201. { id: "klavia-stats-content" },
  202. {
  203. fontSize: "15px",
  204. lineHeight: "1.6",
  205. color: "#ccc",
  206. display: isUIVisible ? "block" : "none",
  207. }
  208. );
  209.  
  210. const clearBtn = createElement(
  211. "button",
  212. {
  213. onclick: () => {
  214. raceHistory = [];
  215. localStorage.removeItem(STORAGE_KEY);
  216. lastHistoryJSON = "[]";
  217. renderStatsUI();
  218. },
  219. },
  220. {
  221. display: isUIVisible ? "block" : "none",
  222. marginTop: "16px",
  223. padding: "8px 16px",
  224. background: "#c62828",
  225. color: "white",
  226. border: "none",
  227. borderRadius: "4px",
  228. cursor: "pointer",
  229. },
  230. "Clear History"
  231. );
  232.  
  233. container.append(infoBtn);
  234. if (isUIVisible) container.append(tabs, content, clearBtn);
  235. document.body.appendChild(container);
  236. updateContent();
  237. }
  238.  
  239. function updateContent() {
  240. const content = document.getElementById("klavia-stats-content");
  241. if (!content) return;
  242.  
  243. raceHistory.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
  244. const r = raceHistory;
  245. const last = r.at(-1);
  246. const avg = (key) => r.reduce((s, e) => s + e[key], 0) / r.length;
  247. const values = (key) => r.map((e) => e[key]);
  248.  
  249. if (activeTab === "teamChat") {
  250. content.innerHTML = `<div style="padding: 16px;">Team Chat for DTR coming soon...</div>`;
  251. return;
  252. }
  253.  
  254. if (activeTab === "analysis") {
  255. content.innerHTML = `<div style="padding: 16px;">Analysis page coming soon...</div>`;
  256. return;
  257. }
  258.  
  259. if (activeTab === "table") {
  260. if (r.length === 0) {
  261. content.innerHTML = `<div style="padding:16px; text-align:center; color:#aaa;">No race data available yet</div>`;
  262. return;
  263. }
  264.  
  265. const rows = [...r]
  266. .reverse()
  267. .map(
  268. (row, i) => `
  269. <tr data-timestamp="${new Date(
  270. row.timestamp
  271. ).toISOString()}" style="background:${i % 2 ? "#2c2c2c" : "#1f1f1f"};">
  272. <td style="padding: 8px; color:#aaa;">${r.length - i}</td>
  273. <td style="padding: 8px; color:${getColor(
  274. row.points,
  275. values("points")
  276. )};">${row.points}</td>
  277. <td style="padding: 8px; color:${getColor(
  278. row.wpm,
  279. values("wpm")
  280. )};">${row.wpm.toFixed(1)}</td>
  281. <td style="padding: 8px; color:${getColor(
  282. row.accuracy,
  283. values("accuracy")
  284. )};">${row.accuracy.toFixed(2)}%</td>
  285. </tr>
  286. `
  287. )
  288. .join("");
  289.  
  290. content.innerHTML = `
  291. <table style="width:100%; border-collapse:collapse;">
  292. <thead style="background:#333;"><tr>
  293. <th style="padding:8px;">#</th>
  294. <th style="padding:8px;">Points</th>
  295. <th style="padding:8px;">WPM</th>
  296. <th style="padding:8px;">Accuracy</th>
  297. </tr></thead>
  298. <tbody>${rows}</tbody>
  299. </table>
  300. <style>
  301. #klavia-stats-content tr:hover td {
  302. background-color: #444 !important;
  303. transition: background 0.2s ease;
  304. }
  305. </style>
  306. `;
  307. return;
  308. }
  309.  
  310. if (r.length === 0) {
  311. content.innerHTML = `<div style="padding:16px; text-align:center; color:#aaa;">No race data available yet</div>`;
  312. return;
  313. }
  314.  
  315. let estimate = "";
  316. if (r.length > 1) {
  317. const intervals = [];
  318. for (let i = 1; i < r.length; i++) {
  319. const diff =
  320. (new Date(r[i].timestamp) - new Date(r[i - 1].timestamp)) / 1000;
  321. if (diff > 0) intervals.push(diff);
  322. }
  323.  
  324. if (intervals.length > 0) {
  325. const avgSecs = intervals.reduce((a, b) => a + b, 0) / intervals.length;
  326. const racesPerHr = 3600 / avgSecs;
  327. const ptsPerHr = racesPerHr * avg("points");
  328. estimate = `
  329. <div><strong style="color:#90caf9;">Estimates:</strong><br>
  330. Races/hr: ${racesPerHr.toFixed(1)}<br>
  331. Points/hr: ${ptsPerHr.toFixed(0)}<br>
  332. <span style="font-size:0.9em;">(Avg interval: ${avgSecs.toFixed(
  333. 1
  334. )}s)</span>
  335. </div>`;
  336. }
  337. }
  338.  
  339. content.innerHTML = `
  340. <div><strong style="color:#90caf9;">Last Race:</strong><br>
  341. <span style="color:${getColor(last.points, values("points"))};">Points: ${
  342. last.points
  343. }</span> |
  344. <span style="color:${getColor(
  345. last.wpm,
  346. values("wpm")
  347. )};">WPM: ${last.wpm.toFixed(1)}</span> |
  348. <span style="color:${getColor(
  349. last.accuracy,
  350. values("accuracy")
  351. )};">Accuracy: ${last.accuracy.toFixed(2)}%</span>
  352. </div><br>
  353. <div><strong style="color:#90caf9;">Averages (${
  354. r.length
  355. } races):</strong><br>
  356. <span style="color:${getColor(
  357. avg("points"),
  358. values("points")
  359. )};">Points: ${avg("points").toFixed(2)}</span> |
  360. <span style="color:${getColor(avg("wpm"), values("wpm"))};">WPM: ${avg(
  361. "wpm"
  362. ).toFixed(1)}</span> |
  363. <span style="color:${getColor(
  364. avg("accuracy"),
  365. values("accuracy")
  366. )};">Accuracy: ${avg("accuracy").toFixed(2)}%</span>
  367. </div><br>
  368. ${estimate}`;
  369. }
  370.  
  371. function renderStatsUI() {
  372. raceHistory = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
  373. lastHistoryJSON = JSON.stringify(raceHistory);
  374. createStatsUI();
  375. }
  376.  
  377. window.addEventListener("klavia:race-logged", (e) => {
  378. renderStatsUI();
  379. });
  380.  
  381. setInterval(() => {
  382. if (!document.getElementById("klavia-stats")) renderStatsUI();
  383. }, 1000);
  384.  
  385. })();