- // ==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">×</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