Powerline.io Data Logger

Track and analyze your Powerline.io gameplay with real time statistics, name performance, and exportable data for deep insights

// ==UserScript==
// @name         Powerline.io Data Logger
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Track and analyze your Powerline.io gameplay with real time statistics, name performance, and exportable data for deep insights
// @author       ᴀʏʟɪᴠᴀ  ⋆。°·☁
// @match        https://powerline.io/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Default script name constant
    const defaultScriptName = "Powerline.io Data Logger";

    // Initialize storage if it doesn't exist
    if (!localStorage.getItem('powerlineData')) {
        localStorage.setItem('powerlineData', JSON.stringify({
            metadata: {
                playerName: '',
                startDate: new Date().toISOString(),
                lastUpdated: new Date().toISOString()
            },
            playerNames: {},
            games: [],
            statistics: {
                totalGames: 0,
                averageTimeAlive: '0s',
                averageScore: 0,
                totalKills: 0,
                bestKillStreak: 0,
                bestScore: 0,
                deathTypes: {}
            }
        }));
    }

    let lastGameTime = null;
    let uiVisible = true;
    let currentGameName = null;

    // Constants for kill reasons
    const KILL_REASONS = {
        LEFT_SCREEN: 'LEFT_SCREEN',
        KILLED: 'KILLED',
        BOUNDARY: 'BOUNDARY',
        SUICIDE: 'SUICIDE',
        UNKNOWN: 'UNKNOWN'
    };

    // Monitors the player's name from the input field and in-game UI
    function monitorPlayerName() {
        const nickInput = document.getElementById('nick');
        if (nickInput && nickInput.value) {
            const name = nickInput.value.trim();
            if (name && name !== currentGameName) {
                updateCurrentName(name);
            }
        }
        const nameElements = document.querySelectorAll('.name');
        nameElements.forEach(elem => {
            const name = elem.textContent.trim();
            if (name && document.getElementById('nick') && name === document.getElementById('nick').value.trim()) {
                updateCurrentName(name);
            }
        });
    }

    // Updates the current game name and logs its usage in localStorage
    function updateCurrentName(newName) {
        if (!newName || newName === currentGameName || newName.length < 1 || newName.length > 15) return;

        currentGameName = newName;
        console.log("Current game name updated:", currentGameName);

        let data = JSON.parse(localStorage.getItem('powerlineData'));
        if (!data.playerNames[newName]) {
            data.playerNames[newName] = 1;
        } else {
            data.playerNames[newName]++;
        }
        data.metadata.lastUpdated = new Date().toISOString();
        localStorage.setItem('powerlineData', JSON.stringify(data));
        updateCounter();
    }

    // Returns the effective player name based on current game name or default
    function getEffectivePlayerName() {
        const data = JSON.parse(localStorage.getItem('powerlineData'));
        return currentGameName || data.metadata.playerName || 'Unknown';
    }

    // Listen for play button clicks and Enter key events in the nickname field
    document.addEventListener('DOMContentLoaded', function() {
        const playButton = document.querySelector('button[onclick*="clickPlay"]');
        if (playButton) {
            playButton.addEventListener('click', function() {
                const nickInput = document.getElementById('nick');
                if (nickInput && nickInput.value) {
                    updateCurrentName(nickInput.value.trim());
                }
            });
        }
        const nickInput = document.getElementById('nick');
        if (nickInput) {
            nickInput.addEventListener('keydown', function(event) {
                if (event.keyCode === 13) { // Enter key
                    updateCurrentName(this.value.trim());
                }
            });
        }
    });

    // Saves game data when changes are detected in gameplay elements
    function saveGameData() {
        const currentTime = document.getElementById('stat-time')?.textContent.trim();
        if (!currentTime || currentTime === lastGameTime || currentTime === '0s') {
            return;
        }
        lastGameTime = currentTime;

        const deathTitle = document.getElementById('stat-title')?.textContent.trim() || '';

        let killReason = KILL_REASONS.UNKNOWN;
        if (deathTitle.includes('COLLIDED')) {
            killReason = KILL_REASONS.BOUNDARY;
        } else if (deathTitle.includes('KILLED BY')) {
            killReason = KILL_REASONS.KILLED;
        } else if (deathTitle.includes('LEFT SCREEN')) {
            killReason = KILL_REASONS.LEFT_SCREEN;
        } else if (deathTitle.includes('SUICIDE')) {
            killReason = KILL_REASONS.SUICIDE;
        }

        let data = JSON.parse(localStorage.getItem('powerlineData'));
        monitorPlayerName();

        const currentKillStreak = parseInt(document.getElementById('stat-bks')?.textContent.trim() || '0');
        const currentScore = parseInt(document.getElementById('stat-blength')?.textContent.trim() || '0');
        const bestKillStreak = Math.max(currentKillStreak, data.statistics.bestKillStreak || 0);
        const bestScore = Math.max(currentScore, data.statistics.bestScore || 0);

        const gameData = {
            timestamp: new Date().toISOString(),
            timeAlive: currentTime,
            topPosition: document.getElementById('stat-top')?.textContent.trim() || '0',
            score: document.getElementById('stat-length')?.textContent.trim() || '0',
            currentKills: document.getElementById('stat-ks')?.textContent.trim() || '0',
            bestKillStreak: bestKillStreak.toString(),
            bestScore: bestScore.toString(),
            deathType: killReason,
            killedBy: deathTitle.includes('KILLED BY') ? deathTitle.split('KILLED BY')[1].trim() : 'none',
            playerName: currentGameName || 'Unknown',  // The actual in-game name used
            defaultName: data.metadata.playerName  // The player's default/main name
        };

        data.games.push(gameData);

        const scoreLimitInput = document.getElementById('score-limit-input');
        const infinityToggle = document.getElementById('infinity-toggle');
        const scoreLimit = parseInt(scoreLimitInput?.value || '0');
        if (!infinityToggle.checked && scoreLimit > 0 && data.games.length > scoreLimit) {
            data.games.sort((a, b) => parseInt(b.score) - parseInt(a.score));
            data.games = data.games.slice(0, scoreLimit);
        }

        data.metadata.lastUpdated = new Date().toISOString();
        data.statistics.bestKillStreak = bestKillStreak;
        data.statistics.bestScore = bestScore;
        data = updateStatistics(data);

        localStorage.setItem('powerlineData', JSON.stringify(data, null, 4));
        updateCounter();
    }

    // Update statistics based on logged games
    function updateStatistics(data) {
        data.statistics.totalGames = data.games.length;
        let totalTime = 0;
        let totalScore = 0;
        let totalKills = 0;

        data.games.forEach(game => {
            let time = parseFloat(game.timeAlive);
            if (!isNaN(time)) totalTime += time;

            let score = parseInt(game.score);
            if (!isNaN(score)) totalScore += score;

            let kills = parseInt(game.currentKills);
            if (!isNaN(kills)) totalKills += kills;
        });

        data.statistics.averageTimeAlive = data.games.length ? (totalTime / data.games.length).toFixed(1) + 's' : '0s';
        data.statistics.averageScore = data.games.length ? Math.round(totalScore / data.games.length) : 0;
        data.statistics.totalKills = totalKills;

        data.statistics.deathTypes = {};
        data.games.forEach(game => {
            if (!data.statistics.deathTypes[game.deathType]) {
                data.statistics.deathTypes[game.deathType] = 1;
            } else {
                data.statistics.deathTypes[game.deathType]++;
            }
        });

        return data;
    }

    // Create control panel element with styling
    const controlPanel = document.createElement('div');
    controlPanel.style.cssText = `
        position: fixed;
        top: 10px;
        right: 10px;
        z-index: 9999;
        background: rgba(0, 0, 0, 0.8);
        padding: 15px;
        border-radius: 8px;
        color: #00FFFF;
        transition: opacity 0.3s ease;
        font-family: Arial, sans-serif;
        box-shadow: 0 0 10px rgba(0, 0, 255, 0.3);
    `;

    // Header displaying the script name
    const headerDisplay = document.createElement('div');
    headerDisplay.style.cssText = `
        font-size: 14px;
        font-weight: bold;
        margin-bottom: 10px;
        text-align: center;
    `;
    headerDisplay.textContent = defaultScriptName;

    // Info section for controls
    const controlsInfo = document.createElement('div');
    controlsInfo.style.cssText = `
        font-size: 11px;
        margin-bottom: 10px;
        padding-bottom: 10px;
        border-bottom: 1px solid #00FFFF;
        opacity: 0.8;
    `;
    controlsInfo.innerHTML = `
        Controls:<br>
        ENTER - Hide panel<br>
        \` (Backtick) - Toggle panel
    `;

    // Current name display
    const nameDisplay = document.createElement('div');
    nameDisplay.style.cssText = `
        margin: 10px 0;
        padding: 10px 0;
        border-bottom: 1px solid #00FFFF;
        font-size: 12px;
    `;
    nameDisplay.innerHTML = `Current Name: <span id="current-name-display">None</span>`;

    // Default name input
    const nameInput = document.createElement('div');
    nameInput.style.cssText = `
        margin: 10px 0;
        padding: 10px 0;
        border-bottom: 1px solid #00FFFF;
    `;
    nameInput.innerHTML = `
        <label style="display: block; margin-bottom: 5px; font-size: 12px;">Default Player Name:</label>
        <input type="text" id="player-name-input" style="
            background: rgba(0, 0, 0, 0.5);
            border: 1px solid #00FFFF;
            color: #00FFFF;
            padding: 5px;
            width: 100%;
            border-radius: 4px;
            font-size: 12px;
        ">
    `;

    // Score Limit Controls
    const scoreLimitDiv = document.createElement('div');
    scoreLimitDiv.style.cssText = `
        margin: 10px 0;
        padding: 10px 0;
        border-bottom: 1px solid #00FFFF;
    `;
    scoreLimitDiv.innerHTML = `
        <label style="display: block; margin-bottom: 5px; font-size: 12px;">Top Scores Limit:</label>
        <input type="number" id="score-limit-input" style="
            background: rgba(0, 0, 0, 0.5);
            border: 1px solid #00FFFF;
            color: #00FFFF;
            padding: 5px;
            width: 100%;
            border-radius: 4px;
            font-size: 12px;
        " placeholder="e.g., 100">
        <label style="font-size: 12px; display: block; margin-top: 5px;">
            <input type="checkbox" id="infinity-toggle" style="margin-right: 5px;">
            Unlimited Records
        </label>
    `;

    const buttonStyle = `
        background-color: #004444;
        color: #00FFFF;
        border: 1px solid #00FFFF;
        padding: 8px 15px;
        margin: 5px;
        border-radius: 5px;
        cursor: pointer;
        transition: all 0.2s ease;
        font-size: 14px;
    `;

    // Export Data button
    const exportButton = document.createElement('button');
    exportButton.textContent = 'Export Data';
    exportButton.style.cssText = buttonStyle;
    exportButton.onclick = function() {
        const data = localStorage.getItem('powerlineData');
        const blob = new Blob([data], {type: 'application/json'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        const date = new Date().toISOString().split('T')[0];
        a.download = `powerline-data-${date}.json`;
        a.click();
    };

    // Clear Data button
    const clearButton = document.createElement('button');
    clearButton.textContent = 'Clear Data';
    clearButton.style.cssText = buttonStyle;
    clearButton.onclick = function() {
        if (confirm('Are you sure you want to clear all logged data?')) {
            const defaultName = JSON.parse(localStorage.getItem('powerlineData')).metadata.playerName;
            localStorage.setItem('powerlineData', JSON.stringify({
                metadata: {
                    playerName: defaultName,  // Preserve the default name
                    startDate: new Date().toISOString(),
                    lastUpdated: new Date().toISOString()
                },
                playerNames: {},
                games: [],
                statistics: {
                    totalGames: 0,
                    averageTimeAlive: '0s',
                    averageScore: 0,
                    totalKills: 0,
                    bestKillStreak: 0,
                    bestScore: 0,
                    deathTypes: {}
                }
            }));
            updateCounter();
        }
    };

    // Statistics display
    const statsDisplay = document.createElement('div');
    statsDisplay.style.cssText = `
        margin-top: 10px;
        padding-top: 10px;
        border-top: 1px solid #00FFFF;
        font-size: 12px;
        text-align: left;
    `;

   function updateCounter() {
    const data = JSON.parse(localStorage.getItem('powerlineData'));
    const namesUsed = Object.keys(data.playerNames).length;
    const mostUsedName = Object.entries(data.playerNames)
        .sort(([,a], [,b]) => b - a)[0]?.[0] || 'None';

    statsDisplay.innerHTML = `
        <div>Games logged: ${data.statistics.totalGames}</div>
        <div>Avg time: ${data.statistics.averageTimeAlive}</div>
        <div>Avg score: ${data.statistics.averageScore}</div>
        <div>Total kills: ${data.statistics.totalKills}</div>
        <div>Best streak: ${data.statistics.bestKillStreak}</div>
        <div>Best score: ${data.statistics.bestScore}</div>
        <div>Names used: ${namesUsed}</div>
        <div>Most used: ${mostUsedName}</div>
        <div>Default Name: ${data.metadata.playerName || 'None'}</div>
        <div>Current Name: ${currentGameName || 'None'}</div>
    `;

    const nameDisplaySpan = document.getElementById('current-name-display');
    if (nameDisplaySpan) {
        nameDisplaySpan.textContent = currentGameName || 'None';
    }
}

// Assemble the control panel
controlPanel.appendChild(headerDisplay);
controlPanel.appendChild(controlsInfo);
controlPanel.appendChild(nameDisplay);
controlPanel.appendChild(nameInput);
controlPanel.appendChild(scoreLimitDiv);
controlPanel.appendChild(exportButton);
controlPanel.appendChild(clearButton);
controlPanel.appendChild(statsDisplay);
document.body.appendChild(controlPanel);

// Set up the default name input
const defaultNameInput = nameInput.querySelector('#player-name-input');
defaultNameInput.value = JSON.parse(localStorage.getItem('powerlineData')).metadata.playerName;
defaultNameInput.addEventListener('change', function(e) {
    const data = JSON.parse(localStorage.getItem('powerlineData'));
    data.metadata.playerName = e.target.value;
    localStorage.setItem('powerlineData', JSON.stringify(data));
});

// Toggle control panel visibility
function toggleUIVisibility(force = null) {
    uiVisible = force !== null ? force : !uiVisible;
    controlPanel.style.opacity = uiVisible ? '1' : '0';
    controlPanel.style.pointerEvents = uiVisible ? 'auto' : 'none';
}

// Keyboard event listeners to hide/toggle the panel
document.addEventListener('keydown', function(e) {
    if (e.key === 'Enter') {
        toggleUIVisibility(false);
    } else if (e.key === '`') {
        toggleUIVisibility();
    }
});

// MutationObserver
const timeObserver = new MutationObserver((mutations) => {
    mutations.forEach((mutation) => {
        if (mutation.type === 'characterData' || mutation.type === 'childList') {
            saveGameData();
        }
    });
});

// Start observing the game time element
const startObserving = () => {
    const timeElement = document.getElementById('stat-time');
    if (timeElement) {
        timeObserver.observe(timeElement, {
            characterData: true,
            childList: true,
            subtree: true
        });
    }
};

// Initialize observation when the stat-time element is available
const checkInterval = setInterval(() => {
    if (document.getElementById('stat-time')) {
        startObserving();
        clearInterval(checkInterval);
    }
}, 1000);

// Additional observers for updating the player name from various UI elements
const setupNameObservers = () => {
    const leaderboard = document.getElementById('leaderboard');
    if (leaderboard) {
        new MutationObserver(monitorPlayerName).observe(leaderboard, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }
    const scoreDisplay = document.querySelector('.stats');
    if (scoreDisplay) {
        new MutationObserver(monitorPlayerName).observe(scoreDisplay, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }
    const deathScreen = document.getElementById('stat-title');
    if (deathScreen) {
        new MutationObserver(monitorPlayerName).observe(deathScreen, {
            childList: true,
            characterData: true
        });
    }
};

const setupInterval = setInterval(() => {
    if (document.getElementById('leaderboard') ||
        document.querySelector('.stats') ||
        document.getElementById('stat-title')) {
        setupNameObservers();
        clearInterval(setupInterval);
    }
}, 1000);

updateCounter();
console.log('Enhanced Powerline.io Data Logger initialized with improved name tracking');
})();