/** @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; crime.money_per_chance = crime.money * (crime.chance / 100) / timeInSeconds; // Money per chance per second } // Function to map value to a color between green and red function getGradientColor(value, min, max, isReversed = false) { if (min === max) return "rgb(0,255,0)"; // Return green if all values are the same let normalized = (value - min) / (max - min); if (isReversed) normalized = 1 - normalized; // Reverse the color gradient for columns where lower values are better let r = Math.round(255 * (1 - normalized)); // Red for lower values let g = Math.round(255 * normalized); // Green for higher values return `rgb(${r},${g},0)`; // Return color from green to red } // Create the CSS window container let container = doc.createElement("div"); container.style = ` position: fixed; top: 100px; left: 100px; background: black; /* Black background */ opacity: 0.9; /* 50% transparency */ 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); } // Find min and max for each column (for color gradient) const columnsWithValues = [ { key: "time", reverseGradient: true }, { key: "chance", reverseGradient: false }, { key: "hacking_exp_per_sec", reverseGradient: false }, { key: "strength_exp_per_sec", reverseGradient: false }, { key: "defense_exp_per_sec", reverseGradient: false }, { key: "dexterity_exp_per_sec", reverseGradient: false }, { key: "agility_exp_per_sec", reverseGradient: false }, { key: "charisma_exp_per_sec", reverseGradient: false }, { key: "money_per_sec", reverseGradient: false }, { key: "karma_per_sec", reverseGradient: false }, { key: "money_per_chance", reverseGradient: false }, { key: "intelligence_exp_per_sec", reverseGradient: false } ]; let minMax = {}; // Get min and max values for each column, excluding non-data elements columnsWithValues.forEach(column => { let values = crimesList.map(crime => crime[column.key]).filter(v => !isNaN(v)); // Ensure we're working with numbers minMax[column.key] = { min: Math.min(...values), max: Math.max(...values) }; }); crimesList.forEach(crime => { let row = doc.createElement("tr"); let columns = [ { text: crime.name, value: crime.name, isName: true }, // Crime name (always green) { text: formatNumber(crime.time / 1000), value: crime.time / 1000, key: "time" }, // Time in seconds { text: formatNumber(crime.chance), value: crime.chance, key: "chance" }, // Success chance { text: formatNumber(crime.hacking_exp_per_sec), value: crime.hacking_exp_per_sec, key: "hacking_exp_per_sec" }, { text: formatNumber(crime.strength_exp_per_sec), value: crime.strength_exp_per_sec, key: "strength_exp_per_sec" }, { text: formatNumber(crime.defense_exp_per_sec), value: crime.defense_exp_per_sec, key: "defense_exp_per_sec" }, { text: formatNumber(crime.dexterity_exp_per_sec), value: crime.dexterity_exp_per_sec, key: "dexterity_exp_per_sec" }, { text: formatNumber(crime.agility_exp_per_sec), value: crime.agility_exp_per_sec, key: "agility_exp_per_sec" }, { text: formatNumber(crime.charisma_exp_per_sec), value: crime.charisma_exp_per_sec, key: "charisma_exp_per_sec" }, { text: formatNumber(crime.money_per_sec), value: crime.money_per_sec, key: "money_per_sec" }, { text: formatNumber(crime.karma_per_sec), value: crime.karma_per_sec, key: "karma_per_sec" }, { text: formatNumber(crime.money_per_chance), value: crime.money_per_chance, key: "money_per_chance" }, // Money per chance per second { text: formatNumber(crime.intelligence_exp_per_sec), value: crime.intelligence_exp_per_sec, key: "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 // Apply the color gradient to each cell based on the column let gradientColor = col.isName ? "#0c0" // Always green for names : getGradientColor(col.value, minMax[col.key].min, minMax[col.key].max, columnsWithValues[index]?.reverseGradient); td.style.color = gradientColor; // Apply color td.style.padding = "5px"; td.style.borderBottom = "1px solid #666"; td.style.textAlign = "center"; td.style.whiteSpace = "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; }); }