/** @param {NS} ns */ export async function main(ns) { const doc = eval("document"); // File to store money data const moneyRecordFile = "MoneyRecord.txt"; const recordInterval = 10000; // Record every 10 seconds const graphDuration = 300000; // Last 5 minutes (in milliseconds) let isClosed = false; // Track if the window is closed // Function to format numbers as K (thousand), M (million), B (billion), etc. function formatLargeNumber(number) { if (number >= 1e12) { return `$${(number / 1e12).toFixed(3)}t`; } else if (number >= 1e9) { return `$${(number / 1e9).toFixed(3)}b`; } else if (number >= 1e6) { return `$${(number / 1e6).toFixed(3)}m`; } else if (number >= 1e3) { return `$${(number / 1e3).toFixed(3)}k`; } else { return `$${number.toFixed(2)}`; // Less than 1000, show as a standard number } } // Remove any existing container if it exists const existingContainer = doc.getElementById("money-graph-container"); if (existingContainer) { existingContainer.remove(); // Remove the existing container ns.tprint("Previous graph closed."); } // Create the container for the graph const container = doc.createElement("div"); container.id = "money-graph-container"; container.style = ` position: absolute; top: 200px; left: 200px; width: 600px; height: 300px; background-color: rgba(0, 0, 0, 0.9); /* Black background with 90% opacity */ color: #0c0; /* Green text */ border: 2px solid #666; border-radius: 8px; padding: 10px; font-family: "Source Code Pro", monospace; z-index: 1000; cursor: move; box-shadow: 0px 0px 15px rgba(0, 255, 0, 0.5); /* Glow effect */ `; // Create a header bar with a close button const header = doc.createElement("div"); header.style = ` display: flex; justify-content: space-between; align-items: center; font-weight: bold; color: #0c0; /* Green text */ padding: 5px; background: rgba(17, 17, 17, 0.9); /* Dark background with 90% opacity */ border-bottom: 1px solid #666; text-shadow: 0px 0px 10px #0f0; `; header.innerText = "Server Money Graph"; // Close button const closeButton = doc.createElement("button"); closeButton.innerText = "X"; closeButton.style = ` background: none; border: 1px solid #666; color: #0c0; /* Green text */ cursor: pointer; padding: 2px 8px; font-weight: bold; border-radius: 3px; margin-left: 10px; `; closeButton.addEventListener("click", () => { container.remove(); // Close the graph container ns.tprint("Graph window closed."); isClosed = true; // Mark the window as closed }); header.appendChild(closeButton); container.appendChild(header); // Create the canvas for the graph const canvas = doc.createElement("canvas"); canvas.id = "money-graph"; canvas.width = 580; // Slightly smaller than the container for padding canvas.height = 200; canvas.style = ` background-color: rgba(17, 17, 17, 0.9); /* Dark background with 90% opacity */ border: 1px solid #666; border-radius: 4px; margin-top: 10px; `; container.appendChild(canvas); // Create the money display (on the right side of the graph) const moneyDisplay = doc.createElement("div"); moneyDisplay.style = ` color: #ffd700; /* Yellow color for money display */ font-size: 18px; font-weight: bold; position: absolute; right: 20px; top: 100px; `; moneyDisplay.innerText = "Money: Loading..."; container.appendChild(moneyDisplay); doc.body.appendChild(container); const ctx = canvas.getContext("2d"); // Movable functionality for the entire container let isMoving = false; let offsetX = 0, offsetY = 0; // Start moving the container on mousedown function startMove(e) { isMoving = true; offsetX = e.clientX - container.getBoundingClientRect().left; offsetY = e.clientY - container.getBoundingClientRect().top; container.style.cursor = "grabbing"; // Change cursor to grabbing while moving } // Stop moving on mouseup function stopMove() { if (isMoving) { isMoving = false; container.style.cursor = "move"; // Change cursor back to move } } // Handle the movement of the window function moveWindow(e) { if (isMoving) { container.style.left = `${e.clientX - offsetX}px`; container.style.top = `${e.clientY - offsetY}px`; } } // Attach event listeners for movement container.addEventListener("mousedown", startMove); doc.addEventListener("mousemove", moveWindow); doc.addEventListener("mouseup", stopMove); doc.addEventListener("mouseleave", stopMove); // Stop moving if mouse leaves the window // Function to record money data to file with timestamps function recordMoneyData() { const currentMoney = ns.getServerMoneyAvailable("home"); const timestamp = Date.now(); const record = `${timestamp},${currentMoney}\n`; ns.write(moneyRecordFile, record, "a"); // Append without await moneyDisplay.innerText = `Money: ${formatLargeNumber(currentMoney)}`; // Update money display with formatted value } // Function to draw grid lines function drawGridLines(maxMoney, minMoney) { const numSteps = 5; // Number of steps on the axes const stepY = (maxMoney - minMoney) / numSteps; // Step size for Y-axis const stepX = graphDuration / numSteps; // Step size for X-axis (time) // Draw vertical grid lines (X-axis steps) for (let i = 1; i <= numSteps; i++) { const x = 40 + (i * (canvas.width - 60)) / numSteps; ctx.beginPath(); ctx.moveTo(x, 10); ctx.lineTo(x, 180); ctx.strokeStyle = 'rgba(0, 255, 0, 0.2)'; // Faint green grid line ctx.stroke(); // Draw X-axis labels const timeLabel = `${Math.round((i * stepX) / 1000)}s`; ctx.fillStyle = '#0c0'; ctx.font = '12px "Source Code Pro"'; ctx.fillText(timeLabel, x - 10, 190); } // Draw horizontal grid lines (Y-axis steps) for (let i = 1; i <= numSteps; i++) { const y = 180 - (i * 170) / numSteps; ctx.beginPath(); ctx.moveTo(40, y); ctx.lineTo(550, y); ctx.strokeStyle = 'rgba(0, 255, 0, 0.2)'; // Faint green grid line ctx.stroke(); // Draw Y-axis labels with formatted money const moneyLabel = formatLargeNumber(minMoney + i * stepY); ctx.fillStyle = '#0c0'; ctx.font = '12px "Source Code Pro"'; ctx.fillText(moneyLabel, 5, y + 5); } } // Function to draw the graph with dynamic scaling from file async function drawGraph() { const fileData = await ns.read(moneyRecordFile); if (!fileData) return; const records = fileData.trim().split("\n").map(line => { const [timestamp, money] = line.split(","); return { timestamp: parseInt(timestamp), money: parseFloat(money) }; }); const now = Date.now(); const recentRecords = records.filter(record => now - record.timestamp <= graphDuration); // Clear the canvas ctx.clearRect(0, 0, canvas.width, canvas.height); if (recentRecords.length === 0) { return; } const maxMoney = Math.max(...recentRecords.map(record => record.money)); const minMoney = Math.min(...recentRecords.map(record => record.money)); if (maxMoney === minMoney) { return; } // Draw gridlines drawGridLines(maxMoney, minMoney); // Draw axes ctx.beginPath(); ctx.moveTo(40, 10); // Left margin ctx.lineTo(40, 180); // Bottom margin ctx.lineTo(550, 180); // Right margin ctx.strokeStyle = '#0c0'; // Green for axes ctx.stroke(); // Plot the data points with yellow line ctx.strokeStyle = '#ffd700'; // Yellow for the money line ctx.beginPath(); const firstPoint = recentRecords[0]; const startTime = firstPoint.timestamp; const initialY = 180 - ((firstPoint.money - minMoney) / (maxMoney - minMoney)) * 170; // Scale to fit graph height ctx.moveTo(40, initialY); recentRecords.forEach((record) => { const timeDiff = record.timestamp - startTime; const x = 40 + (timeDiff / graphDuration) * (canvas.width - 60); // Scaled x-axis const y = 180 - ((record.money - minMoney) / (maxMoney - minMoney)) * 170; // Scaled y-axis ctx.lineTo(x, y); }); ctx.stroke(); } // Main loop to record money data and update graph try { while (!isClosed) { recordMoneyData(); // Record data without await await drawGraph(); // Ensure graph is updated properly await ns.sleep(recordInterval); // Wait for 10 seconds } } catch (e) { ns.tprint(`Error: ${e.message}`); } finally { if (doc.body.contains(container)) { container.remove(); // Clean up the container when the script ends } ns.tprint("Script stopped."); } }