/** @param {NS} ns */ export async function main(ns) { ns.disableLog('sleep'); // Disable log spamming const doc = eval("document"); const body = doc.body; let isMinimized = false; // Track the minimize state // Read crime stats from the CrimeStats.txt file let crimeStatsData = await ns.read("CrimeStats.txt"); let crimes = JSON.parse(crimeStatsData); // Parse the JSON data from the file // Add dynamic crime chance and calculate stats per second for (let crime of crimes) { crime.chance = ns.singularity.getCrimeChance(crime.name) * 100; // Get success chance as a number let timeInSeconds = crime.time / 1000; // Convert ms to seconds for calculations // Calculate stats per second crime.hacking_exp_per_sec = crime.hacking_exp / timeInSeconds; crime.strength_exp_per_sec = crime.strength_exp / timeInSeconds; crime.defense_exp_per_sec = crime.defense_exp / timeInSeconds; crime.dexterity_exp_per_sec = crime.dexterity_exp / timeInSeconds; crime.agility_exp_per_sec = crime.agility_exp / timeInSeconds; crime.charisma_exp_per_sec = crime.charisma_exp / timeInSeconds; crime.intelligence_exp_per_sec = crime.intelligence_exp / timeInSeconds || 0; // Handle potential missing data crime.money_per_sec = crime.money / timeInSeconds; crime.karma_per_sec = crime.karma / timeInSeconds; } // Create the CSS window container let container = doc.createElement("div"); container.style = ` position: fixed; top: 100px; left: 100px; background: black; /* Black background */ color: #0c0; /* Green text */ z-index: 1000; font-family: "Source Code Pro", monospace; /* Apply the terminal font */ font-size: 16px; border: 2px solid #666; border-radius: 5px; width: 1400px; /* Increased width */ height: 400px; /* Increased height */ overflow-y: auto; `; body.appendChild(container); // Create the title bar with minimize and close buttons let titleBar = doc.createElement("div"); titleBar.style = ` display: flex; justify-content: space-between; align-items: center; background: black; /* Black background for title bar */ color: #0c0; /* Green text */ border-bottom: 1px solid #666; font-weight: bold; `; container.appendChild(titleBar); // Title text let title = doc.createElement("span"); title.innerText = "Crime Stats Monitor"; titleBar.appendChild(title); // Create a container for the buttons and align them to the right let buttonContainer = doc.createElement("div"); buttonContainer.style = ` display: flex; justify-content: flex-end; `; titleBar.appendChild(buttonContainer); // Minimize button (▲ for minimize, ▼ for restore) let minimizeButton = doc.createElement("button"); minimizeButton.innerText = "▲"; // Start as minimize button minimizeButton.style = ` background: none; border: 1px solid #666; color: #0c0; /* Green text */ cursor: pointer; font-weight: bold; padding: 5px; width: 30px; /* Set same width for buttons */ `; buttonContainer.appendChild(minimizeButton); minimizeButton.addEventListener("click", () => { if (isMinimized) { minimizeButton.innerText = "▲"; // Minimize arrow container.style.height = "400px"; // Restore height } else { minimizeButton.innerText = "▼"; // Restore arrow container.style.height = "25px"; // Minimize height } isMinimized = !isMinimized; }); // Close button (X) let closeButton = doc.createElement("button"); closeButton.innerText = "X"; closeButton.style = ` background: none; border: 1px solid #666; color: #0c0; /* Green text */ cursor: pointer; font-weight: bold; padding: 5px; width: 30px; /* Set same width for buttons */ `; buttonContainer.appendChild(closeButton); closeButton.addEventListener("click", () => { container.remove(); // Close the window }); // Table for displaying the crime stats let table = doc.createElement("table"); table.style = ` width: 100%; border-collapse: collapse; color: #0c0; /* Green text */ `; container.appendChild(table); // Track the current sort direction for each column (null, 'asc', or 'desc') let sortOrder = new Array(13).fill(null); // One for each column // Add table headers with clickable sorting functionality and arrows let headers = [ "Crime", "Time (s)", "Chance", "Hack Exp/s", "Str Exp/s", "Def Exp/s", "Dex Exp/s", "Agi Exp/s", "Cha Exp/s", "Money/s", "Karma/s", "$/%/s", "IntXP/s" ]; let headerRow = doc.createElement("tr"); headers.forEach((header, index) => { let th = doc.createElement("th"); th.innerHTML = `${header} `; // Span for the arrow th.style = ` padding: 5px; background: #333; border: 1px solid #666; cursor: pointer; white-space: nowrap; /* Prevent wrapping */ `; // Add sorting functionality for each column th.addEventListener("click", () => { clearAllArrows(); // Remove all arrows sortCrimesByColumn(index); // Sort by this column toggleSortOrder(index); // Toggle sort order for this column }); headerRow.appendChild(th); }); table.appendChild(headerRow); // Function to toggle the sort order for a column function toggleSortOrder(columnIndex) { // Toggle between 'asc' and 'desc' if (sortOrder[columnIndex] === 'asc') { sortOrder[columnIndex] = 'desc'; } else { sortOrder[columnIndex] = 'asc'; } // Update the arrows for this column updateArrow(columnIndex); } // Function to update the arrow for a specific column function updateArrow(columnIndex) { let th = headerRow.querySelectorAll("th")[columnIndex]; let arrowSpan = th.querySelector("span"); if (sortOrder[columnIndex] === 'asc') { arrowSpan.innerHTML = " ▲"; // Up arrow for ascending } else if (sortOrder[columnIndex] === 'desc') { arrowSpan.innerHTML = " ▼"; // Down arrow for descending } } // Function to clear all arrows function clearAllArrows() { headerRow.querySelectorAll("th span").forEach(arrow => { arrow.innerHTML = ""; // Remove all arrows }); } // Function to format numbers with 4 decimal places function formatNumber(num) { return parseFloat(num.toFixed(4)); // Convert to number with 4 decimal places } // Function to display crimes in the table function displayCrimes(crimesList) { // Clear any existing rows (except the header) while (table.rows.length > 1) { table.deleteRow(1); } crimesList.forEach(crime => { let row = doc.createElement("tr"); let columns = [ { text: crime.name, value: crime.name }, // Treat as string { text: formatNumber(crime.time / 1000), value: crime.time / 1000 }, // Time in seconds { text: formatNumber(crime.chance), value: crime.chance }, // Success chance { text: formatNumber(crime.hacking_exp_per_sec), value: crime.hacking_exp_per_sec }, { text: formatNumber(crime.strength_exp_per_sec), value: crime.strength_exp_per_sec }, { text: formatNumber(crime.defense_exp_per_sec), value: crime.defense_exp_per_sec }, { text: formatNumber(crime.dexterity_exp_per_sec), value: crime.dexterity_exp_per_sec }, { text: formatNumber(crime.agility_exp_per_sec), value: crime.agility_exp_per_sec }, { text: formatNumber(crime.charisma_exp_per_sec), value: crime.charisma_exp_per_sec }, { text: formatNumber(crime.money_per_sec), value: crime.money_per_sec }, { text: formatNumber(crime.karma_per_sec), value: crime.karma_per_sec }, { text: formatNumber(crime.money * crime.chance / 100), value: crime.money * crime.chance / 100 }, // Money per chance per second { text: formatNumber(crime.intelligence_exp_per_sec), value: crime.intelligence_exp_per_sec } // Intelligence XP per second ]; columns.forEach((col, index) => { let td = doc.createElement("td"); td.innerText = col.text; // Display formatted text td.dataset.value = col.value; // Store the numeric value for sorting td.style = ` padding: 5px; border-bottom: 1px solid #666; text-align: center; white-space: nowrap; /* Prevent text wrapping */ `; row.appendChild(td); }); table.appendChild(row); }); } // Sorting function (high to low for numeric values) function sortCrimesByColumn(columnIndex) { let rows = Array.from(table.querySelectorAll('tr:nth-child(n+2)')); // Get all rows except header let order = sortOrder[columnIndex]; // Ascending or descending rows.sort((rowA, rowB) => { let valA = rowA.cells[columnIndex].dataset.value; let valB = rowB.cells[columnIndex].dataset.value; // Check if values are numeric and sort accordingly if (!isNaN(valA) && !isNaN(valB)) { return order === 'asc' ? parseFloat(valA) - parseFloat(valB) : parseFloat(valB) - parseFloat(valA); } // If not numeric, sort alphabetically return order === 'asc' ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA)); }); // Append rows in sorted order rows.forEach(row => table.appendChild(row)); } // Display the crimes initially displayCrimes(crimes); // Make the window draggable let isDragging = false; let offsetX, offsetY; titleBar.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - container.getBoundingClientRect().left; offsetY = e.clientY - container.getBoundingClientRect().top; }); doc.addEventListener("mousemove", (e) => { if (isDragging) { container.style.left = `${e.clientX - offsetX}px`; container.style.top = `${e.clientY - offsetY}px`; } }); doc.addEventListener("mouseup", () => { isDragging = false; }); }