TagPro Custom Map Tiles (Anchored PIXI Overlay & Editor)

Uses a sprite sheet from Imgur and a combined grid data object (with gridTiles, gridLines, and an optional imgurLink) to draw custom tiles and lines on the TagPro map background. Also adds a homepage modal editor for adding and removing maps.

// ==UserScript==
// @name         TagPro Custom Map Tiles (Anchored PIXI Overlay & Editor)
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Uses a sprite sheet from Imgur and a combined grid data object (with gridTiles, gridLines, and an optional imgurLink) to draw custom tiles and lines on the TagPro map background. Also adds a homepage modal editor for adding and removing maps.
// @author
// @match        https://tagpro.koalabeast.com/*
// @grant        none
// @license MIT
// ==/UserScript==
(function() {
    'use strict';

    // Run once TagPro is ready.
    tagpro.ready(function() {

        ////////////////////////////////
        // Setup Combined Grid Data
        ////////////////////////////////

        var defaultCombinedGridData = [
            {
                "Asida": {
                    "gridLines": [
                        { "start": { "x": 16.5, "y": 17.5 }, "end": { "x": 6, "y": 9.75 }, "color": "red" },
                        { "start": { "x": 6, "y": 9.75 },   "end": { "x": 6.75, "y": 9 },   "color": "red" },
                        { "start": { "x": 6.75, "y": 9 },     "end": { "x": 6.75, "y": 9.5 }, "color": "red" },
                        { "start": { "x": 6.75, "y": 9 },     "end": { "x": 6.25, "y": 9 },   "color": "red" },
                        { "start": { "x": 16.5, "y": 17.5 },  "end": { "x": 16.25, "y": 17.25 }, "color": "red" },
                        { "start": { "x": 16.5, "y": 17.5 },  "end": { "x": 11, "y": 6 },     "color": "#f79999" },
                        { "start": { "x": 11, "y": 6.25 },    "end": { "x": 10.75, "y": 7 },  "color": "#f79999" },
                        { "start": { "x": 11, "y": 6 },       "end": { "x": 12, "y": 6.5 },   "color": "#f79999" },
                        { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 30.75, "y": 16.75 }, "color": "blue" },
                        { "start": { "x": 30.75, "y": 16.75 }, "end": { "x": 31, "y": 17 },     "color": "blue" },
                        { "start": { "x": 30.75, "y": 17.25 }, "end": { "x": 30, "y": 18 },     "color": "blue" },
                        { "start": { "x": 30, "y": 18 },      "end": { "x": 30.5, "y": 18 },   "color": "blue" },
                        { "start": { "x": 30, "y": 17.75 },   "end": { "x": 30, "y": 17.75 },  "color": "blue" },
                        { "start": { "x": 30, "y": 18 },      "end": { "x": 30, "y": 17.5 },   "color": "blue" },
                        { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 20.25, "y": 9.25 }, "color": "#99c1f1" },
                        { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 20.25, "y": 9.25 }, "color": "#99c1f1" },
                        { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 26, "y": 21 },     "color": "#99c1f1" },
                        { "start": { "x": 26, "y": 21 },      "end": { "x": 26, "y": 20.25 },  "color": "#99c1f1" },
                        { "start": { "x": 26, "y": 21 },      "end": { "x": 25.25, "y": 20.5 }, "color": "#99c1f1" }
                    ],
                    "gridTiles": [
                        { "x": 18, "y": 13, "tileIndex": 0 },
                        { "x": 16, "y": 12, "tileIndex": 0 },
                        { "x": 17, "y": 15, "tileIndex": 0 },
                        { "x": 20, "y": 14, "tileIndex": 0 },
                        { "x": 20, "y": 12, "tileIndex": 0 },
                        { "x": 18, "y": 14, "tileIndex": 1 },
                        { "x": 17, "y": 12, "tileIndex": 1 },
                        { "x": 19, "y": 12, "tileIndex": 1 },
                        { "x": 20, "y": 13, "tileIndex": 1 },
                        { "x": 20, "y": 15, "tileIndex": 1 },
                        { "x": 16, "y": 15, "tileIndex": 1 },
                        { "x": 19, "y": 14, "tileIndex": 2 },
                        { "x": 17, "y": 14, "tileIndex": 2 },
                        { "x": 18, "y": 12, "tileIndex": 2 },
                        { "x": 19, "y": 13, "tileIndex": 3 },
                        { "x": 17, "y": 13, "tileIndex": 3 },
                        { "x": 18, "y": 15, "tileIndex": 3 },
                        { "x": 16, "y": 14, "tileIndex": 3 },
                        { "x": 19, "y": 15, "tileIndex": 4 },
                        { "x": 16, "y": 13, "tileIndex": 4 },
                        { "x": 19, "y": 11, "tileIndex": 4 },
                        { "x": 17, "y": 11, "tileIndex": 4 },
                        { "x": 18, "y": 11, "tileIndex": 4 },
                        { "x": 18, "y": 11, "tileIndex": 0 },
                        { "x": 20, "y": 11, "tileIndex": 2 },
                        { "x": 16, "y": 11, "tileIndex": 2 }
                    ],
                    "imgurLink": "https://i.imgur.com/oZDnzgO.png"
                }
            }
        ];

        // Load any saved data from localStorage (or use the default).
        var combinedGridData;
        try {
            var stored = localStorage.getItem("tagproCombinedGridData");
            if (stored) {
                combinedGridData = JSON.parse(stored);
            } else {
                combinedGridData = defaultCombinedGridData;
            }
        } catch (e) {
            console.error("Error loading combinedGridData from localStorage", e);
            combinedGridData = defaultCombinedGridData;
        }

        // Helper function to save the grid data.
        function saveCombinedGridData() {
            localStorage.setItem("tagproCombinedGridData", JSON.stringify(combinedGridData));
        }

        ////////////////////////////////
        // Branch based on current page:
        ////////////////////////////////

        if (window.location.pathname.includes("/game")) {
            // If we are on a game page, wait until players are loaded.
            var waitForPlayers = setInterval(function(){
                if (tagpro.players && Object.keys(tagpro.players).length > 0) {
                    clearInterval(waitForPlayers);

                    // Use the first available player as a reference.
                    var firstPlayerId = Object.keys(tagpro.players)[0];
                    var playerContainer = tagpro.players[firstPlayerId].sprite.parent;

                    // Wait until the map is available.
                    var checkMapInterval = setInterval(function(){
                        if (tagpro.map && tagpro.map.name) {
                            clearInterval(checkMapInterval);

                            // Find matching grid data using tagpro.map.name.
                            var currentMapData = null;
                            for (var i = 0; i < combinedGridData.length; i++) {
                                var mapNameKey = Object.keys(combinedGridData[i])[0];
                                if (mapNameKey === tagpro.map.name) {
                                    currentMapData = combinedGridData[i][mapNameKey];
                                    break;
                                }
                            }
                            if (!currentMapData) {
                                return;
                            }

                            var gridLines = currentMapData.gridLines;
                            var gridTiles = currentMapData.gridTiles;
                            var spriteSheetUrl = currentMapData.imgurLink;

                            // Get TagPro's background container.
                            var bgContainer = tagpro.renderer.stage.children[0];
                            if (!bgContainer) {
                                return;
                            }

                            var customOverlay = new PIXI.Container();
                            var tileSize = 40;
                            var spriteSheetTexture = PIXI.Texture.from(spriteSheetUrl);

                            // Add custom tiles.
                            gridTiles.forEach(function(tile) {
                                var posX = tile.x * tileSize;
                                var posY = tile.y * tileSize;
                                var index = tile.tileIndex;
                                var srcX = (index % 10) * tileSize;
                                var srcY = Math.floor(index / 10) * tileSize;
                                var tileTexture = new PIXI.Texture(spriteSheetTexture.baseTexture, new PIXI.Rectangle(srcX, srcY, tileSize, tileSize));
                                var sprite = new PIXI.Sprite(tileTexture);
                                sprite.x = posX;
                                sprite.y = posY;
                                customOverlay.addChild(sprite);
                            });

                            // Draw custom lines.
                            var lineGraphics = new PIXI.Graphics();
                            gridLines.forEach(function(line) {
                                var lineColor = (typeof line.color === "string")
                                    ? PIXI.utils.string2hex(line.color)
                                    : (line.color !== undefined ? line.color : 0xFFFFFF);
                                lineGraphics.lineStyle(2, lineColor, 1);
                                var startX = line.start.x * tileSize;
                                var startY = line.start.y * tileSize;
                                var endX = line.end.x * tileSize;
                                var endY = line.end.y * tileSize;
                                lineGraphics.moveTo(startX, startY);
                                lineGraphics.lineTo(endX, endY);
                            });
                            customOverlay.addChild(lineGraphics);

                            // Insert the overlay into the player container so it is rendered behind all players.
                            if (playerContainer) {
                                playerContainer.addChildAt(customOverlay, 0);
                            } else {
                                console.error("Player container not found.");
                            }
                        }
                    }, 100);
                }
            }, 100);
        } else {
            // Non-game pages (e.g., the homepage): Set up the modal editor immediately.

            // Inject CSS for the modal editor.
            var style = document.createElement('style');
            style.textContent = `
               #customMapEditorModal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.6);
    display: none;
    justify-content: center;
    align-items: center;
    z-index: 10000;
    font-family: Arial, sans-serif;
}

#customMapEditorContent {
    background: #f9f9f9;
    padding: 20px;
    border-radius: 12px;
    max-width: 500px;
    width: 90%;
    max-height: 80%;
    overflow-y: auto;
    position: relative;
    box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
    border: 1px solid #ddd;
}

#customMapEditorContent h2 {
    margin-top: 0;
    font-size: 20px;
    color: #333;
    text-align: center;
}

.mapItem {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    background: #ffffff;
    border-radius: 6px;
    margin-bottom: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.mapItem span {
    font-size: 16px;
    color: #444;
}

.mapItem button {
    background: #ff4d4d;
    color: white;
    border: none;
    padding: 5px 10px;
    cursor: pointer;
    border-radius: 4px;
    transition: background 0.2s ease-in-out;
}

.mapItem button:hover {
    background: #e60000;
}

#addMapTextbox {
    width: 100%;
    height: 100px;
    margin-top: 10px;
    border-radius: 6px;
    border: 1px solid #ccc;
    padding: 8px;
    font-size: 14px;
}

#customMapEditorClose {
    position: absolute;
    top: 10px;
    right: 15px;
    cursor: pointer;
    font-size: 24px;
    color: #666;
    transition: color 0.2s;
}

#customMapEditorClose:hover {
    color: #222;
}

#customMapEditorButton {
    position: fixed;
    bottom: 15px;
    right: 15px;
    z-index: 10000;
    padding: 12px 18px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.2s ease-in-out;
    box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2);
}

#customMapEditorButton:hover {
    background: #0056b3;
}`;
            document.head.appendChild(style);

            // Create a fixed-position button that opens the modal.
            var editorButton = document.createElement('button');
            editorButton.id = 'customMapEditorButton';
            editorButton.textContent = 'Edit Map Data';
            document.body.appendChild(editorButton);

            // Create the modal element.
            var modal = document.createElement('div');
            modal.id = 'customMapEditorModal';
            modal.innerHTML = `
                <div id="customMapEditorContent">
                    <span id="customMapEditorClose">&times;</span>
                    <h2>Custom Map Data Editor</h2>
                    <div id="mapListContainer"></div>
                    <textarea id="addMapTextbox" placeholder='Paste JSON object here to add (e.g., {"New Map": {"gridLines": [...], "gridTiles": [...], "imgurLink": "..."}})'></textarea>
                </div>
            `;
            document.body.appendChild(modal);

            // Function to update the list of maps.
            function updateMapList() {
                var container = document.getElementById('mapListContainer');
                container.innerHTML = '';
                if (combinedGridData.length === 0) {
                    container.textContent = 'No map data available.';
                    return;
                }
                combinedGridData.forEach(function(item, index) {
                    var mapName = Object.keys(item)[0];
                    var div = document.createElement('div');
                    div.className = 'mapItem';
                    div.innerHTML = `<span>${mapName}</span> <button data-index="${index}">x</button>`;
                    container.appendChild(div);
                });
            }
            updateMapList();

            // Listen for clicks on remove (x) buttons.
            document.getElementById('mapListContainer').addEventListener('click', function(e) {
                if (e.target && e.target.tagName === 'BUTTON') {
                    var index = parseInt(e.target.getAttribute('data-index'), 10);
                    if (!isNaN(index)) {
                        combinedGridData.splice(index, 1);
                        saveCombinedGridData();
                        updateMapList();
                    }
                }
            });

            // When a JSON object is pasted into the textarea, try to add it.
            var addMapTextbox = document.getElementById('addMapTextbox');
            addMapTextbox.addEventListener('paste', function(e) {
                setTimeout(function() {
                    try {
                        var pastedText = addMapTextbox.value.trim();
                        if (!pastedText) return;
                        var newMapData = JSON.parse(pastedText);
                        if (typeof newMapData === 'object' && newMapData !== null && Object.keys(newMapData).length === 1) {
                            combinedGridData.push(newMapData);
                            saveCombinedGridData();
                            updateMapList();
                            addMapTextbox.value = '';
                        } else {
                            alert('Invalid format. The JSON object must have exactly one key (the map name).');
                        }
                    } catch (err) {
                        alert('Error parsing JSON: ' + err);
                    }
                }, 100);
            });

            // Open the modal when clicking the editor button.
            editorButton.addEventListener('click', function() {
                modal.style.display = 'flex';
            });

            // Close the modal when clicking the close (×) button.
            document.getElementById('customMapEditorClose').addEventListener('click', function() {
                modal.style.display = 'none';
            });

            // Also close the modal if clicking outside the content.
            modal.addEventListener('click', function(e) {
                if (e.target === modal) {
                    modal.style.display = 'none';
                }
            });
        } // End non-/game branch

    }); // End tagpro.ready
})(); // End IIFE