smoothretro1982

Updated Userlist script 2.1 (afk tracking update)

Jun 30th, 2026 (edited)
82
0
Never
3
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 23.03 KB | None | 0 0
  1. // --- PREVENT DUPLICATE LISTENERS ---
  2. if (window.handleCanvasHUDKeys) {
  3. window.removeEventListener("keydown", window.handleCanvasHUDKeys);
  4. }
  5.  
  6. // --- CONFIGURATION & INITIAL STATE ---
  7. let lastDraw = []; // Stores [{char, color}] lines for diff-rendering
  8. let spawnX = cursor.x;
  9. let spawnY = cursor.y;
  10. let typeIndex = 0; // Controls character reveal count
  11. let typeSpeed = 2.0; // Characters revealed per animation frame
  12. let typingDone = false;
  13. let lastRevealedCount = -1;
  14. let running = true; // Master control loop flag
  15.  
  16. // Map to track cursor activity: Map<id, {x, y, lastMoveTime}>
  17. const cursorActivity = new Map();
  18.  
  19. // --- TOGGLES & CONFIGURATION ---
  20. let showStatus = true; // Displays Active / Idle status bar indicator
  21. let showCoords = false; // Toggle to show/hide coordinates column
  22. let showColors = true;
  23. let showNicknames = true;
  24. let sortAlphabetical = true;
  25. let sortByID = false; // Toggle to sort cursors by ID
  26. let sortByAFK = false; // Toggle to sort cursors by AFK status
  27. let tickerPaused = true; // Toggle to start/stop the ticker animation
  28.  
  29. // --- COLOR CONFIGURATION ---
  30. const ACTIVE_STATUS_COLOR = 0; // Green for active users
  31. const AFK_STATUS_COLOR = 0; // Red/Orange for AFK users
  32. const LOGIN_COLOR = 3; // Green for logged in notifications
  33. const LOGOUT_COLOR = 4; // Red for logged out notifications
  34. const CLOCK_COLOR = 0; // <--- Add your preferred color code here
  35. const NEARBY_COLOR = 0; // <--- Color for the Nearby count (e.g., 2 for Green)
  36. const ONLINE_COLOR = 0; // <--- Color for the Online count (e.g., 3 for Cyan)
  37.  
  38. // --- LOGIN / LOGOUT TRACKING ---
  39. let trackedCursors = new Map();
  40. let initializedCursors = false;
  41. let currentNotification = "";
  42. let notificationExpireTime = 0;
  43. const NOTIFICATION_DURATION = 3000; // 3 seconds
  44.  
  45. function triggerNotification(msg) {
  46. currentNotification = msg;
  47. notificationExpireTime = Date.now() + NOTIFICATION_DURATION;
  48. }
  49.  
  50. // --- SCROLLING / REVEAL TICKER CONFIGURATION ---
  51. const tickerMessages = [
  52. "Welcome to tw.2s4.me and stay safe",
  53. "Please read tw.2s4.me/rules",
  54. "Join our discord! *",
  55. " 67 ",
  56. ];
  57.  
  58. let currentMessageIndex = 0;
  59. let currentFrame = 0;
  60. let lastTickerStepTime = Date.now();
  61. const TICKER_STEP_INTERVAL = 470; // Frame animation speed in ms
  62. const TICKER_PAUSE_DURATION = 5000; // Pause duration between messages in ms
  63.  
  64. let isPaused = false;
  65. let pauseStartTime = 0;
  66.  
  67. // HUD & Progress Bar overlay options
  68. const HUD_CONFIG = {
  69. enabled: true,
  70. showDate: false,
  71. showClock: true,
  72. showProgress: true,
  73. showTicker: true,
  74. tickerMode: "typewriter", // Options: "scroll" or "typewriter"
  75. barLength: 12,
  76. barFill: "▓",
  77. barEmpty: "▒",
  78. barLeft: " ",
  79. barRight: " ",
  80. percentPrecision: 5,
  81. fixedWidth: 70 // Interior box width precisely sized for aligned columns
  82. };
  83.  
  84. /**
  85. * Calculates the current ticker text frame based on tickerMode and manages pauses
  86. */
  87. function getTickerFrameText() {
  88. const message = tickerMessages[currentMessageIndex];
  89. const fullLength = message.length;
  90. let displayedText = "";
  91. let totalFrames = 0;
  92.  
  93. if (HUD_CONFIG.tickerMode === "typewriter") {
  94. // --- MODE 1: TYPEWRITER ---
  95. totalFrames = fullLength;
  96.  
  97. if (currentFrame <= fullLength) {
  98. displayedText = message.substring(0, currentFrame);
  99. } else {
  100. displayedText = message;
  101. }
  102. } else {
  103. // --- MODE 2: SCROLLING TICKER ---
  104. totalFrames = fullLength * 2 + 1;
  105.  
  106. if (currentFrame < fullLength) {
  107. // Phase 1: Text scrolls off to the left
  108. displayedText = message.substring(currentFrame) + " ".repeat(currentFrame);
  109. } else {
  110. // Phase 2: Text scrolls in from the right
  111. const charsToShow = currentFrame - fullLength;
  112. displayedText = " ".repeat(fullLength - charsToShow) + message.substring(0, charsToShow);
  113. }
  114. }
  115.  
  116. // --- MANUAL TICKER PAUSE CONTROL ---
  117. if (tickerPaused) {
  118. return displayedText.padEnd(fullLength, " ");
  119. }
  120.  
  121. const now = Date.now();
  122.  
  123. // Handle Pause Phase between messages
  124. if (isPaused) {
  125. if (now - pauseStartTime >= TICKER_PAUSE_DURATION) {
  126. isPaused = false;
  127. currentFrame = 0;
  128. currentMessageIndex = (currentMessageIndex + 1) % tickerMessages.length;
  129. lastTickerStepTime = now;
  130. }
  131. return displayedText.padEnd(fullLength, " ");
  132. }
  133.  
  134. // Handle Animation Step Clock
  135. if (now - lastTickerStepTime >= TICKER_STEP_INTERVAL) {
  136. lastTickerStepTime = now;
  137. currentFrame++;
  138.  
  139. // Check if current message sequence finished -> trigger pause
  140. if (currentFrame >= totalFrames) {
  141. isPaused = true;
  142. pauseStartTime = now;
  143. }
  144. }
  145.  
  146. return displayedText.padEnd(fullLength, " ");
  147. }
  148.  
  149. /**
  150. * UTILITY: Extract first number from a string
  151. */
  152. function extractNumber(str) {
  153. const m = str.match(/\d+/);
  154. return m ? parseInt(m[0], 10) : 0;
  155. }
  156.  
  157. /**
  158. * UTILITY: Get current (x, y) coordinates for a cursor cleanly
  159. */
  160. function getCursorPos(cur) {
  161. const x = cur?.rawx ?? cur?.l?.[0] ?? 0;
  162. const y = cur?.rawy ?? cur?.l?.[1] ?? 0;
  163. return { x, y };
  164. }
  165.  
  166. // --- SERVER TIME SYNC STATE ---
  167. let serverTimeOffset = 0; // Difference in ms between local time and server time
  168. let lastSyncTime = 0;
  169.  
  170. /**
  171. * Synchronizes client time with the server using HTTP response headers
  172. */
  173. async function syncServerTime() {
  174. try {
  175. const t0 = performance.now();
  176. const response = await fetch(window.location.origin, { method: "HEAD", cache: "no-store" });
  177. const t1 = performance.now();
  178.  
  179. const serverDateHeader = response.headers.get("date");
  180. if (serverDateHeader) {
  181. const serverMs = new Date(serverDateHeader).getTime();
  182. const roundTripTime = t1 - t0;
  183. // Estimate server time at the moment the response returned
  184. const estimatedServerTime = serverMs + (roundTripTime / 2);
  185.  
  186. serverTimeOffset = estimatedServerTime - Date.now();
  187. lastSyncTime = Date.now();
  188. }
  189. } catch (err) {
  190. console.warn("Server time sync failed, falling back to client time.", err);
  191. }
  192. }
  193.  
  194. /**
  195. * Returns current Date object synchronized with server time
  196. */
  197. function getServerTime() {
  198. return new Date(Date.now() + serverTimeOffset);
  199. }
  200.  
  201. /**
  202. * Formats synchronized server time as HH:MM:SS AM/PM
  203. */
  204. function getServerTimeStr() {
  205. const d = getServerTime();
  206. let hrs = d.getHours();
  207. const mins = d.getMinutes().toString().padStart(2, "0");
  208. const secs = d.getSeconds().toString().padStart(2, "0");
  209.  
  210. const ampm = hrs >= 12 ? "PM" : "AM";
  211. hrs = hrs % 12;
  212. hrs = hrs ? hrs : 12; // Convert hour '0' to '12'
  213. const hrsStr = hrs.toString().padStart(2, "0");
  214.  
  215. return `| ${hrsStr}:${mins}:${secs} ${ampm}`;
  216. }
  217.  
  218. // Initial sync on script startup + periodic re-sync every 5 minutes
  219. syncServerTime();
  220. setInterval(syncServerTime, 5 * 60 * 1000);
  221.  
  222. /**
  223. * DATE FORMATTER: Formats current date
  224. */
  225. function getDateStr() {
  226. const d = new Date();
  227. const weekdays = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
  228. const months = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
  229.  
  230. const w = weekdays[(d.getDay() + 6) % 7];
  231. const day = d.getDate().toString().padStart(2, "0");
  232. const month = months[d.getMonth() + 1];
  233. const year = d.getFullYear();
  234.  
  235. return w + day + month + year;
  236. }
  237.  
  238. /**
  239. * YEAR PROGRESS CALCULATOR: Returns float between 0.0 and 1.0
  240. */
  241. function getYearProgress() {
  242. const now = new Date();
  243. const start = new Date(now.getFullYear(), 0, 1);
  244. const end = new Date(now.getFullYear() + 1, 0, 1);
  245. return Math.min(1, Math.max(0, (now - start) / (end - start)));
  246. }
  247.  
  248. /**
  249. * Helper text builders for styled/colored cells
  250. */
  251. function makeColoredText(text, color) {
  252. const finalColor = showColors ? color : 0;
  253. return [...text].map(ch => ({ char: ch, color: finalColor }));
  254. }
  255.  
  256. function makeText(text) {
  257. return [...text].map(ch => ({ char: ch, color: 0 }));
  258. }
  259.  
  260. /**
  261. * FIXED-WIDTH BOX ROW CREATOR
  262. */
  263. function createBoxRow(cellSegments, targetWidth) {
  264. let processed = cellSegments;
  265.  
  266. if (cellSegments.length > targetWidth) {
  267. processed = cellSegments.slice(0, targetWidth - 3);
  268. processed.push({ char: ".", color: 0 }, { char: ".", color: 0 }, { char: ".", color: 0 });
  269. }
  270.  
  271. const row = [{ char: "║", color: 0 }, { char: " ", color: 0 }];
  272. row.push(...processed);
  273.  
  274. const paddingNeeded = Math.max(0, targetWidth - processed.length);
  275. for (let i = 0; i < paddingNeeded; i++) {
  276. row.push({ char: " ", color: 0 });
  277. }
  278.  
  279. row.push({ char: " ", color: 0 }, { char: "║", color: 0 });
  280. return row;
  281. }
  282.  
  283. /**
  284. * Calculates typewriter animation overlay bounding limits.
  285. */
  286. function applyTypewriterMask(rows) {
  287. let totalChars = 0;
  288. for (let y = 0; y < rows.length; y++) {
  289. totalChars += rows[y].length;
  290. }
  291.  
  292. if (typingDone) {
  293. typeIndex = totalChars;
  294. }
  295.  
  296. let count = 0;
  297. const masked = [];
  298. const visibleChars = Math.floor(typeIndex);
  299.  
  300. for (let y = 0; y < rows.length; y++) {
  301. const row = [];
  302. for (let x = 0; x < rows[y].length; x++) {
  303. if (count < visibleChars) {
  304. row.push(rows[y][x]);
  305. } else {
  306. row.push({ char: " ", color: 0 });
  307. }
  308. count++;
  309. }
  310. masked.push(row);
  311. }
  312.  
  313. if (!typingDone) {
  314. typeIndex += typeSpeed;
  315. if (typeIndex >= count) {
  316. typeIndex = count;
  317. typingDone = true;
  318. }
  319. }
  320.  
  321. return { masked, count };
  322. }
  323.  
  324. /**
  325. * Performs frame-by-frame diff comparison to eliminate flickering.
  326. */
  327. function drawDiffLines(lines, x0, y0) {
  328. const maxLines = Math.max(lines.length, lastDraw.length);
  329.  
  330. for (let y = 0; y < maxLines; y++) {
  331. const newLine = lines[y] || [];
  332. const oldLine = lastDraw[y] || [];
  333. const maxLen = Math.max(newLine.length, oldLine.length);
  334.  
  335. for (let x = 0; x < maxLen; x++) {
  336. const n = newLine[x] || { char: " ", color: 0 };
  337. const o = oldLine[x] || { char: " ", color: 0 };
  338.  
  339. if (n.char !== o.char || n.color !== o.color) {
  340. writeCharAt(n.char, n.color, x0 + x, y0 + y);
  341. }
  342. }
  343. }
  344. lastDraw = lines.map(row => row.map(cell => ({ ...cell })));
  345. }
  346.  
  347. // Reference to observer for cleanup
  348. let chatObserver = null;
  349.  
  350. /**
  351. * Tears down listeners, observers, and clears text cleanly.
  352. */
  353. function stopScript() {
  354. running = false;
  355.  
  356. if (chatObserver) {
  357. chatObserver.disconnect();
  358. window._chatAFKObserverActive = false;
  359. }
  360.  
  361. const emptyLines = lastDraw.map(row => row.map(() => ({ char: " ", color: 0 })));
  362. drawDiffLines(emptyLines, spawnX, spawnY);
  363. window.removeEventListener("keydown", window.handleCanvasHUDKeys);
  364. console.log("Script stopped and overlay cleared.");
  365. }
  366.  
  367. /**
  368. * Handles toggles and script exit.
  369. */
  370. window.handleCanvasHUDKeys = function(e) {
  371. const key = e.key.toLowerCase();
  372. if (e.key === "`") {
  373. stopScript();
  374. }
  375. if (key === "!") {
  376. sortAlphabetical = !sortAlphabetical;
  377. sortByID = false;
  378. sortByAFK = false;
  379. typeIndex = 0;
  380. typingDone = true;
  381. }
  382. if (key === "@") {
  383. sortByID = !sortByID;
  384. sortAlphabetical = false;
  385. sortByAFK = false;
  386. typeIndex = 0;
  387. typingDone = true;
  388. }
  389. if (key === "#") {
  390. showCoords = !showCoords;
  391. }
  392. if (key === "$") {
  393. showStatus = !showStatus;
  394. }
  395. if (key === "%") {
  396. sortByAFK = !sortByAFK;
  397. sortAlphabetical = false;
  398. sortByID = false;
  399. typeIndex = 0;
  400. typingDone = false;
  401. }
  402. if (key === "^") { // Toggle Ticker Mode between "scroll" and "typewriter"
  403. HUD_CONFIG.tickerMode = (HUD_CONFIG.tickerMode === "scroll") ? "typewriter" : "scroll";
  404. currentFrame = 0;
  405. isPaused = false;
  406. }
  407. if (key === "&") { // Toggle start/stop (pause/play) for ticker animation
  408. tickerPaused = !tickerPaused;
  409. lastTickerStepTime = Date.now();
  410. }
  411. };
  412.  
  413. window.addEventListener("keydown", window.handleCanvasHUDKeys);
  414.  
  415. function getCursorName(cur) {
  416. if (!cur) return "Anon";
  417. if (cur.dn && cur.n && cur.dn !== cur.n) {
  418. return `${cur.n} (${cur.dn})`;
  419. }
  420. return cur.n || cur.dn || "Anon";
  421. }
  422.  
  423. /**
  424. * Resets the AFK timer for a specific user ID or username
  425. */
  426. function resetUserAFK(identifier) {
  427. if (!identifier) return;
  428. const cleanId = String(identifier).trim();
  429. const now = Date.now();
  430.  
  431. for (const [id, cur] of w.cursors.entries()) {
  432. const name = getCursorName(cur);
  433. if (String(id) === cleanId || name === cleanId || cur.n === cleanId) {
  434. const { x, y } = getCursorPos(cur);
  435. cursorActivity.set(id, { x, y, lastMoveTime: now });
  436. break;
  437. }
  438. }
  439. }
  440.  
  441. /**
  442. * Returns raw idle time in seconds (0 = active)
  443. */
  444. function getCursorIdleSeconds(curId, cur) {
  445. const now = Date.now();
  446. const { x, y } = getCursorPos(cur);
  447. const previous = cursorActivity.get(curId);
  448.  
  449. if (!previous || previous.x !== x || previous.y !== y) {
  450. cursorActivity.set(curId, { x, y, lastMoveTime: now });
  451. return 0;
  452. }
  453.  
  454. return Math.floor((now - previous.lastMoveTime) / 500);
  455. }
  456.  
  457. /**
  458. * Returns true if the cursor was active in the last 2 seconds.
  459. */
  460. function isCursorActive(curId, cur) {
  461. return getCursorIdleSeconds(curId, cur) < 2;
  462. }
  463.  
  464. (function setupChatAFKReset() {
  465. if (window._chatAFKObserverActive) return;
  466. window._chatAFKObserverActive = true;
  467.  
  468. function handleNewMessage(msgEl) {
  469. const userLink = msgEl.querySelector('a[title*="Username:"]');
  470. if (userLink) {
  471. const titleAttr = userLink.getAttribute("title");
  472. const match = titleAttr.match(/Username:\s*([^\n\r]+)/);
  473. if (match && match[1]) {
  474. resetUserAFK(match[1].trim());
  475. }
  476. }
  477. }
  478.  
  479. chatObserver = new MutationObserver((mutations) => {
  480. for (const mutation of mutations) {
  481. for (const node of mutation.addedNodes) {
  482. if (node.nodeType === 1) {
  483. if (node.id === "_msg" || node.classList?.contains("fade-in")) {
  484. handleNewMessage(node);
  485. } else {
  486. const msgs = node.querySelectorAll?.('#_msg, .fade-in');
  487. msgs?.forEach(handleNewMessage);
  488. }
  489. }
  490. }
  491. }
  492. });
  493.  
  494. chatObserver.observe(document.body, { childList: true, subtree: true });
  495. })();
  496.  
  497. /**
  498. * Main application loop executing calculations per frame.
  499. */
  500. function ut() {
  501. if (!running) return;
  502.  
  503. // Skip computation while the tab is minimized/hidden
  504. if (document.hidden) {
  505. requestAnimationFrame(ut);
  506. return;
  507. }
  508.  
  509. // ============================================================
  510. // LOGIN / LOGOUT DETECTION
  511. // ============================================================
  512. const currentCursors = new Map();
  513. for (const [id, cur] of w.cursors.entries()) {
  514. currentCursors.set(String(id), getCursorName(cur));
  515. }
  516.  
  517. if (initializedCursors) {
  518. for (const [id, name] of currentCursors.entries()) {
  519. if (!trackedCursors.has(id)) {
  520. triggerNotification(`${name} joined`);
  521. }
  522. }
  523. for (const [id, name] of trackedCursors.entries()) {
  524. if (!currentCursors.has(id)) {
  525. triggerNotification(`${name} left`);
  526. }
  527. }
  528. } else {
  529. initializedCursors = true;
  530. }
  531.  
  532. trackedCursors = currentCursors;
  533.  
  534. const contentSections = [];
  535.  
  536. // --- SECTION 1: HUD METRICS, YEAR PROGRESS & TICKER ---
  537. if (HUD_CONFIG.enabled) {
  538. const sec1 = [];
  539. const el = document.getElementById("nearby");
  540. const nearbyText = el?.textContent?.trim() || "0 nearby";
  541. const nearbyCount = extractNumber(nearbyText);
  542. const onlineCount = extractNumber(el?.title || "");
  543.  
  544. const dateStr = HUD_CONFIG.showDate ? getDateStr() : "";
  545. const clockStr = HUD_CONFIG.showClock ? getServerTimeStr() : "";
  546.  
  547. // Base status text
  548. const baseText = `Near: ${nearbyText} | Online: ${onlineCount}`;
  549. const dateText = dateStr ? ` | ${dateStr}` : "";
  550.  
  551. const activeNotice = (Date.now() < notificationExpireTime) ? currentNotification : "";
  552. const boxInteriorWidth = HUD_CONFIG.fixedWidth || 70;
  553.  
  554. // Calculate total text length to handle space padding
  555. const leftTextLen = baseText.length + (clockStr ? 3 + clockStr.length : 0) + dateText.length;
  556. const availableSpace = boxInteriorWidth - leftTextLen - activeNotice.length;
  557. const paddingCount = Math.max(1, availableSpace);
  558.  
  559. // Build the character cell array with custom segment colors
  560. const hudCells = [];
  561.  
  562. // 1. Base status text with custom label & counter colors
  563. const nearLabel = "Near: ";
  564. const nearNum = `${nearbyText}`;
  565. const onlineLabel = " ║ Online: ";
  566. const onlineNum = `${onlineCount}`;
  567.  
  568. // "Near: "
  569. for (const c of nearLabel) {
  570. hudCells.push({ char: c, color: 0 });
  571. }
  572. // Nearby Number
  573. for (const c of nearNum) {
  574. hudCells.push({ char: c, color: showColors ? NEARBY_COLOR : 0 });
  575. }
  576. // " | Online: "
  577. for (const c of onlineLabel) {
  578. hudCells.push({ char: c, color: 0 });
  579. }
  580. // Online Number
  581. for (const c of onlineNum) {
  582. hudCells.push({ char: c, color: showColors ? ONLINE_COLOR : 0 });
  583. }
  584.  
  585. // 2. Clock segment (Colored using CLOCK_COLOR)
  586. if (clockStr) {
  587. hudCells.push(...makeText(" | "));
  588. for (const c of clockStr) {
  589. hudCells.push({
  590. char: c,
  591. color: showColors ? CLOCK_COLOR : 0
  592. });
  593. }
  594. }
  595.  
  596. // 3. Date segment
  597. if (dateText) {
  598. hudCells.push(...makeText(dateText));
  599. }
  600.  
  601. // 4. Space Padding
  602. hudCells.push(...makeText(" ".repeat(paddingCount)));
  603.  
  604. // 5. Active Notice (Join / Leave notifications)
  605. if (activeNotice) {
  606. const isLogin = activeNotice.includes("joined");
  607. const noticeColor = showColors ? (isLogin ? LOGIN_COLOR : LOGOUT_COLOR) : 0;
  608. for (const c of activeNotice) {
  609. hudCells.push({ char: c, color: noticeColor });
  610. }
  611. }
  612.  
  613. sec1.push(hudCells);
  614.  
  615. if (HUD_CONFIG.showProgress) {
  616. const progress = getYearProgress();
  617. const filledUnits = Math.floor(progress * HUD_CONFIG.barLength);
  618.  
  619. const fillStr = HUD_CONFIG.barFill || "=";
  620. const emptyStr = HUD_CONFIG.barEmpty || "-";
  621. const leftStr = HUD_CONFIG.barLeft || "[";
  622. const rightStr = HUD_CONFIG.barRight || "]";
  623.  
  624. const filled = Array(filledUnits).fill(fillStr).join("");
  625. const empty = Array(HUD_CONFIG.barLength - filledUnits).fill(emptyStr).join("");
  626. const percent = (progress * 100).toFixed(HUD_CONFIG.percentPrecision) + "%";
  627.  
  628. const barStr = `${leftStr}${filled}${empty}${rightStr} ${percent}`;
  629. sec1.push(makeText(barStr));
  630. }
  631.  
  632. if (HUD_CONFIG.showTicker) {
  633. const tickerText = getTickerFrameText();
  634. sec1.push(makeText(tickerText));
  635. }
  636.  
  637. contentSections.push(sec1);
  638. }
  639.  
  640. // ============================================================
  641. // SECTION 2: CURSOR LIST ([STATUS SYMBOL] [NAME] [ID] [COORDS])
  642. // ============================================================
  643. const sec2 = [];
  644. let cursorList = Array.from(w.cursors.entries());
  645.  
  646. const NAME_WIDTH = 43;
  647. const ID_WIDTH = 10;
  648. const COORD_WIDTH = 16;
  649. const GAP = " ";
  650.  
  651. const hasNoneUser = cursorList.some(([_, cur]) => {
  652. const name = getCursorName(cur).toLowerCase();
  653. return name === "^" || cur?.n?.toLowerCase() === "^" || cur?.dn?.toLowerCase() === "^";
  654. });
  655.  
  656. if (!hasNoneUser) {
  657. if (sortByAFK) {
  658. cursorList.sort((a, b) => {
  659. const idleA = getCursorIdleSeconds(a[0], a[1]);
  660. const idleB = getCursorIdleSeconds(b[0], b[1]);
  661. return idleA - idleB;
  662. });
  663. } else if (sortByID) {
  664. cursorList.sort((a, b) => {
  665. const idA = extractNumber(String(a[0] || 0));
  666. const idB = extractNumber(String(b[0] || 0));
  667. return idA - idB;
  668. });
  669. } else if (sortAlphabetical) {
  670. cursorList.sort((a, b) => {
  671. const nameA = getCursorName(a[1]).toLowerCase();
  672. const nameB = getCursorName(b[1]).toLowerCase();
  673. return nameA.localeCompare(nameB);
  674. });
  675. }
  676. }
  677.  
  678. const hName = "USER LIST".padEnd(NAME_WIDTH + 2, " ");
  679. const hID = "ID".padEnd(ID_WIDTH, " ");
  680. const hCoords = showCoords ? "COORDS".padEnd(COORD_WIDTH, " ") : "";
  681.  
  682. let headerLine = hName + GAP + hID;
  683. if (showCoords) headerLine += GAP + hCoords;
  684.  
  685. sec2.push(makeText(headerLine.trimEnd()));
  686.  
  687. for (const [id, cur] of cursorList) {
  688. if (!cur) continue;
  689.  
  690. const unameColor = cur.c || 0;
  691. let rowSegments = [];
  692.  
  693. // --- 1. AFK STATUS INDICATOR ---
  694. const active = isCursorActive(id, cur);
  695. const indicatorChar = active ? "▰" : "▱";
  696. const indicatorColor = active ? ACTIVE_STATUS_COLOR : AFK_STATUS_COLOR;
  697.  
  698. if (showStatus) {
  699. rowSegments.push(...makeColoredText(indicatorChar, indicatorColor));
  700. rowSegments.push(...makeText(" "));
  701. } else {
  702. rowSegments.push(...makeText(" "));
  703. }
  704.  
  705. // --- 2. USERNAME ---
  706. let rawName = showNicknames ? getCursorName(cur) : "Anon";
  707. if (rawName.length > NAME_WIDTH) {
  708. rawName = rawName.slice(0, NAME_WIDTH - 3) + "...";
  709. }
  710. const paddedName = rawName.padEnd(NAME_WIDTH, " ");
  711. rowSegments.push(...makeColoredText(paddedName, unameColor));
  712.  
  713. // --- 3. ID ---
  714. const rawID = String(cur.id ?? id ?? "?");
  715. const paddedID = rawID.padEnd(ID_WIDTH, " ");
  716. rowSegments.push(...makeText(GAP + paddedID));
  717.  
  718. // --- 4. COORDINATES ---
  719. if (showCoords) {
  720. const { x, y } = getCursorPos(cur);
  721. const rawCoord = `(${x},${y})`;
  722. const paddedCoord = rawCoord.padEnd(COORD_WIDTH, " ");
  723. rowSegments.push(...makeText(GAP + paddedCoord));
  724. }
  725.  
  726. sec2.push(rowSegments);
  727. }
  728.  
  729. if (sec2.length === 1) {
  730. sec2.push(makeText("No active cursors"));
  731. }
  732. contentSections.push(sec2);
  733.  
  734. // ============================================================
  735. // SECTION 3: FOOTER METRICS
  736. // ============================================================
  737. const sec3 = [];
  738. const sortLabel = hasNoneUser
  739. ? "Disabled (^)"
  740. : (sortByAFK ? "AFK" : (sortByID ? "ID" : (sortAlphabetical ? "A-Z" : "Default")));
  741.  
  742. sec3.push(makeText("Sorted: " + sortLabel));
  743. contentSections.push(sec3);
  744.  
  745. // ============================================================
  746. // RENDER BOX STRUCTURE
  747. // ============================================================
  748. const targetWidth = HUD_CONFIG.fixedWidth || 70;
  749. const borderChar = "═".repeat(targetWidth + 2);
  750. const rows = [];
  751.  
  752. rows.push(makeText(`╔${borderChar}╗`));
  753.  
  754. contentSections.forEach((sec, idx) => {
  755. for (const rowCells of sec) {
  756. rows.push(createBoxRow(rowCells, targetWidth));
  757. }
  758.  
  759. if (idx < contentSections.length - 1) {
  760. rows.push(makeText(`╟${borderChar}╢`));
  761. } else {
  762. rows.push(makeText(`╚${borderChar}╝`));
  763. }
  764. });
  765.  
  766. const { masked } = applyTypewriterMask(rows);
  767. const currentRevealed = Math.floor(typeIndex);
  768.  
  769. if (currentRevealed !== lastRevealedCount || typingDone) {
  770. drawDiffLines(masked, spawnX, spawnY);
  771. lastRevealedCount = currentRevealed;
  772. }
  773.  
  774. requestAnimationFrame(ut);
  775. }
  776.  
  777. ut();
Advertisement
Comments
Add Comment
Please, Sign In to add comment