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.

  1. // ==UserScript==
  2. // @name TagPro Custom Map Tiles (Anchored PIXI Overlay & Editor)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @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.
  6. // @author
  7. // @match https://tagpro.koalabeast.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13.  
  14. // Run once TagPro is ready.
  15. tagpro.ready(function() {
  16.  
  17. ////////////////////////////////
  18. // Setup Combined Grid Data
  19. ////////////////////////////////
  20.  
  21. var defaultCombinedGridData = [
  22. {
  23. "Asida": {
  24. "gridLines": [
  25. { "start": { "x": 16.5, "y": 17.5 }, "end": { "x": 6, "y": 9.75 }, "color": "red" },
  26. { "start": { "x": 6, "y": 9.75 }, "end": { "x": 6.75, "y": 9 }, "color": "red" },
  27. { "start": { "x": 6.75, "y": 9 }, "end": { "x": 6.75, "y": 9.5 }, "color": "red" },
  28. { "start": { "x": 6.75, "y": 9 }, "end": { "x": 6.25, "y": 9 }, "color": "red" },
  29. { "start": { "x": 16.5, "y": 17.5 }, "end": { "x": 16.25, "y": 17.25 }, "color": "red" },
  30. { "start": { "x": 16.5, "y": 17.5 }, "end": { "x": 11, "y": 6 }, "color": "#f79999" },
  31. { "start": { "x": 11, "y": 6.25 }, "end": { "x": 10.75, "y": 7 }, "color": "#f79999" },
  32. { "start": { "x": 11, "y": 6 }, "end": { "x": 12, "y": 6.5 }, "color": "#f79999" },
  33. { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 30.75, "y": 16.75 }, "color": "blue" },
  34. { "start": { "x": 30.75, "y": 16.75 }, "end": { "x": 31, "y": 17 }, "color": "blue" },
  35. { "start": { "x": 30.75, "y": 17.25 }, "end": { "x": 30, "y": 18 }, "color": "blue" },
  36. { "start": { "x": 30, "y": 18 }, "end": { "x": 30.5, "y": 18 }, "color": "blue" },
  37. { "start": { "x": 30, "y": 17.75 }, "end": { "x": 30, "y": 17.75 }, "color": "blue" },
  38. { "start": { "x": 30, "y": 18 }, "end": { "x": 30, "y": 17.5 }, "color": "blue" },
  39. { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 20.25, "y": 9.25 }, "color": "#99c1f1" },
  40. { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 20.25, "y": 9.25 }, "color": "#99c1f1" },
  41. { "start": { "x": 20.25, "y": 9.25 }, "end": { "x": 26, "y": 21 }, "color": "#99c1f1" },
  42. { "start": { "x": 26, "y": 21 }, "end": { "x": 26, "y": 20.25 }, "color": "#99c1f1" },
  43. { "start": { "x": 26, "y": 21 }, "end": { "x": 25.25, "y": 20.5 }, "color": "#99c1f1" }
  44. ],
  45. "gridTiles": [
  46. { "x": 18, "y": 13, "tileIndex": 0 },
  47. { "x": 16, "y": 12, "tileIndex": 0 },
  48. { "x": 17, "y": 15, "tileIndex": 0 },
  49. { "x": 20, "y": 14, "tileIndex": 0 },
  50. { "x": 20, "y": 12, "tileIndex": 0 },
  51. { "x": 18, "y": 14, "tileIndex": 1 },
  52. { "x": 17, "y": 12, "tileIndex": 1 },
  53. { "x": 19, "y": 12, "tileIndex": 1 },
  54. { "x": 20, "y": 13, "tileIndex": 1 },
  55. { "x": 20, "y": 15, "tileIndex": 1 },
  56. { "x": 16, "y": 15, "tileIndex": 1 },
  57. { "x": 19, "y": 14, "tileIndex": 2 },
  58. { "x": 17, "y": 14, "tileIndex": 2 },
  59. { "x": 18, "y": 12, "tileIndex": 2 },
  60. { "x": 19, "y": 13, "tileIndex": 3 },
  61. { "x": 17, "y": 13, "tileIndex": 3 },
  62. { "x": 18, "y": 15, "tileIndex": 3 },
  63. { "x": 16, "y": 14, "tileIndex": 3 },
  64. { "x": 19, "y": 15, "tileIndex": 4 },
  65. { "x": 16, "y": 13, "tileIndex": 4 },
  66. { "x": 19, "y": 11, "tileIndex": 4 },
  67. { "x": 17, "y": 11, "tileIndex": 4 },
  68. { "x": 18, "y": 11, "tileIndex": 4 },
  69. { "x": 18, "y": 11, "tileIndex": 0 },
  70. { "x": 20, "y": 11, "tileIndex": 2 },
  71. { "x": 16, "y": 11, "tileIndex": 2 }
  72. ],
  73. "imgurLink": "https://i.imgur.com/oZDnzgO.png"
  74. }
  75. }
  76. ];
  77.  
  78. // Load any saved data from localStorage (or use the default).
  79. var combinedGridData;
  80. try {
  81. var stored = localStorage.getItem("tagproCombinedGridData");
  82. if (stored) {
  83. combinedGridData = JSON.parse(stored);
  84. } else {
  85. combinedGridData = defaultCombinedGridData;
  86. }
  87. } catch (e) {
  88. console.error("Error loading combinedGridData from localStorage", e);
  89. combinedGridData = defaultCombinedGridData;
  90. }
  91.  
  92. // Helper function to save the grid data.
  93. function saveCombinedGridData() {
  94. localStorage.setItem("tagproCombinedGridData", JSON.stringify(combinedGridData));
  95. }
  96.  
  97. ////////////////////////////////
  98. // Branch based on current page:
  99. ////////////////////////////////
  100.  
  101. if (window.location.pathname.includes("/game")) {
  102. // If we are on a game page, wait until players are loaded.
  103. var waitForPlayers = setInterval(function(){
  104. if (tagpro.players && Object.keys(tagpro.players).length > 0) {
  105. clearInterval(waitForPlayers);
  106.  
  107. // Use the first available player as a reference.
  108. var firstPlayerId = Object.keys(tagpro.players)[0];
  109. var playerContainer = tagpro.players[firstPlayerId].sprite.parent;
  110.  
  111. // Wait until the map is available.
  112. var checkMapInterval = setInterval(function(){
  113. if (tagpro.map && tagpro.map.name) {
  114. clearInterval(checkMapInterval);
  115.  
  116. // Find matching grid data using tagpro.map.name.
  117. var currentMapData = null;
  118. for (var i = 0; i < combinedGridData.length; i++) {
  119. var mapNameKey = Object.keys(combinedGridData[i])[0];
  120. if (mapNameKey === tagpro.map.name) {
  121. currentMapData = combinedGridData[i][mapNameKey];
  122. break;
  123. }
  124. }
  125. if (!currentMapData) {
  126. return;
  127. }
  128.  
  129. var gridLines = currentMapData.gridLines;
  130. var gridTiles = currentMapData.gridTiles;
  131. var spriteSheetUrl = currentMapData.imgurLink;
  132.  
  133. // Get TagPro's background container.
  134. var bgContainer = tagpro.renderer.stage.children[0];
  135. if (!bgContainer) {
  136. return;
  137. }
  138.  
  139. var customOverlay = new PIXI.Container();
  140. var tileSize = 40;
  141. var spriteSheetTexture = PIXI.Texture.from(spriteSheetUrl);
  142.  
  143. // Add custom tiles.
  144. gridTiles.forEach(function(tile) {
  145. var posX = tile.x * tileSize;
  146. var posY = tile.y * tileSize;
  147. var index = tile.tileIndex;
  148. var srcX = (index % 10) * tileSize;
  149. var srcY = Math.floor(index / 10) * tileSize;
  150. var tileTexture = new PIXI.Texture(spriteSheetTexture.baseTexture, new PIXI.Rectangle(srcX, srcY, tileSize, tileSize));
  151. var sprite = new PIXI.Sprite(tileTexture);
  152. sprite.x = posX;
  153. sprite.y = posY;
  154. customOverlay.addChild(sprite);
  155. });
  156.  
  157. // Draw custom lines.
  158. var lineGraphics = new PIXI.Graphics();
  159. gridLines.forEach(function(line) {
  160. var lineColor = (typeof line.color === "string")
  161. ? PIXI.utils.string2hex(line.color)
  162. : (line.color !== undefined ? line.color : 0xFFFFFF);
  163. lineGraphics.lineStyle(2, lineColor, 1);
  164. var startX = line.start.x * tileSize;
  165. var startY = line.start.y * tileSize;
  166. var endX = line.end.x * tileSize;
  167. var endY = line.end.y * tileSize;
  168. lineGraphics.moveTo(startX, startY);
  169. lineGraphics.lineTo(endX, endY);
  170. });
  171. customOverlay.addChild(lineGraphics);
  172.  
  173. // Insert the overlay into the player container so it is rendered behind all players.
  174. if (playerContainer) {
  175. playerContainer.addChildAt(customOverlay, 0);
  176. } else {
  177. console.error("Player container not found.");
  178. }
  179. }
  180. }, 100);
  181. }
  182. }, 100);
  183. } else {
  184. // Non-game pages (e.g., the homepage): Set up the modal editor immediately.
  185.  
  186. // Inject CSS for the modal editor.
  187. var style = document.createElement('style');
  188. style.textContent = `
  189. #customMapEditorModal {
  190. position: fixed;
  191. top: 0;
  192. left: 0;
  193. width: 100%;
  194. height: 100%;
  195. background: rgba(0, 0, 0, 0.6);
  196. display: none;
  197. justify-content: center;
  198. align-items: center;
  199. z-index: 10000;
  200. font-family: Arial, sans-serif;
  201. }
  202.  
  203. #customMapEditorContent {
  204. background: #f9f9f9;
  205. padding: 20px;
  206. border-radius: 12px;
  207. max-width: 500px;
  208. width: 90%;
  209. max-height: 80%;
  210. overflow-y: auto;
  211. position: relative;
  212. box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
  213. border: 1px solid #ddd;
  214. }
  215.  
  216. #customMapEditorContent h2 {
  217. margin-top: 0;
  218. font-size: 20px;
  219. color: #333;
  220. text-align: center;
  221. }
  222.  
  223. .mapItem {
  224. display: flex;
  225. justify-content: space-between;
  226. align-items: center;
  227. padding: 10px;
  228. background: #ffffff;
  229. border-radius: 6px;
  230. margin-bottom: 8px;
  231. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  232. }
  233.  
  234. .mapItem span {
  235. font-size: 16px;
  236. color: #444;
  237. }
  238.  
  239. .mapItem button {
  240. background: #ff4d4d;
  241. color: white;
  242. border: none;
  243. padding: 5px 10px;
  244. cursor: pointer;
  245. border-radius: 4px;
  246. transition: background 0.2s ease-in-out;
  247. }
  248.  
  249. .mapItem button:hover {
  250. background: #e60000;
  251. }
  252.  
  253. #addMapTextbox {
  254. width: 100%;
  255. height: 100px;
  256. margin-top: 10px;
  257. border-radius: 6px;
  258. border: 1px solid #ccc;
  259. padding: 8px;
  260. font-size: 14px;
  261. }
  262.  
  263. #customMapEditorClose {
  264. position: absolute;
  265. top: 10px;
  266. right: 15px;
  267. cursor: pointer;
  268. font-size: 24px;
  269. color: #666;
  270. transition: color 0.2s;
  271. }
  272.  
  273. #customMapEditorClose:hover {
  274. color: #222;
  275. }
  276.  
  277. #customMapEditorButton {
  278. position: fixed;
  279. bottom: 15px;
  280. right: 15px;
  281. z-index: 10000;
  282. padding: 12px 18px;
  283. background: #007bff;
  284. color: white;
  285. border: none;
  286. border-radius: 8px;
  287. cursor: pointer;
  288. font-size: 14px;
  289. transition: background 0.2s ease-in-out;
  290. box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2);
  291. }
  292.  
  293. #customMapEditorButton:hover {
  294. background: #0056b3;
  295. }`;
  296. document.head.appendChild(style);
  297.  
  298. // Create a fixed-position button that opens the modal.
  299. var editorButton = document.createElement('button');
  300. editorButton.id = 'customMapEditorButton';
  301. editorButton.textContent = 'Edit Map Data';
  302. document.body.appendChild(editorButton);
  303.  
  304. // Create the modal element.
  305. var modal = document.createElement('div');
  306. modal.id = 'customMapEditorModal';
  307. modal.innerHTML = `
  308. <div id="customMapEditorContent">
  309. <span id="customMapEditorClose">&times;</span>
  310. <h2>Custom Map Data Editor</h2>
  311. <div id="mapListContainer"></div>
  312. <textarea id="addMapTextbox" placeholder='Paste JSON object here to add (e.g., {"New Map": {"gridLines": [...], "gridTiles": [...], "imgurLink": "..."}})'></textarea>
  313. </div>
  314. `;
  315. document.body.appendChild(modal);
  316.  
  317. // Function to update the list of maps.
  318. function updateMapList() {
  319. var container = document.getElementById('mapListContainer');
  320. container.innerHTML = '';
  321. if (combinedGridData.length === 0) {
  322. container.textContent = 'No map data available.';
  323. return;
  324. }
  325. combinedGridData.forEach(function(item, index) {
  326. var mapName = Object.keys(item)[0];
  327. var div = document.createElement('div');
  328. div.className = 'mapItem';
  329. div.innerHTML = `<span>${mapName}</span> <button data-index="${index}">x</button>`;
  330. container.appendChild(div);
  331. });
  332. }
  333. updateMapList();
  334.  
  335. // Listen for clicks on remove (x) buttons.
  336. document.getElementById('mapListContainer').addEventListener('click', function(e) {
  337. if (e.target && e.target.tagName === 'BUTTON') {
  338. var index = parseInt(e.target.getAttribute('data-index'), 10);
  339. if (!isNaN(index)) {
  340. combinedGridData.splice(index, 1);
  341. saveCombinedGridData();
  342. updateMapList();
  343. }
  344. }
  345. });
  346.  
  347. // When a JSON object is pasted into the textarea, try to add it.
  348. var addMapTextbox = document.getElementById('addMapTextbox');
  349. addMapTextbox.addEventListener('paste', function(e) {
  350. setTimeout(function() {
  351. try {
  352. var pastedText = addMapTextbox.value.trim();
  353. if (!pastedText) return;
  354. var newMapData = JSON.parse(pastedText);
  355. if (typeof newMapData === 'object' && newMapData !== null && Object.keys(newMapData).length === 1) {
  356. combinedGridData.push(newMapData);
  357. saveCombinedGridData();
  358. updateMapList();
  359. addMapTextbox.value = '';
  360. } else {
  361. alert('Invalid format. The JSON object must have exactly one key (the map name).');
  362. }
  363. } catch (err) {
  364. alert('Error parsing JSON: ' + err);
  365. }
  366. }, 100);
  367. });
  368.  
  369. // Open the modal when clicking the editor button.
  370. editorButton.addEventListener('click', function() {
  371. modal.style.display = 'flex';
  372. });
  373.  
  374. // Close the modal when clicking the close (×) button.
  375. document.getElementById('customMapEditorClose').addEventListener('click', function() {
  376. modal.style.display = 'none';
  377. });
  378.  
  379. // Also close the modal if clicking outside the content.
  380. modal.addEventListener('click', function(e) {
  381. if (e.target === modal) {
  382. modal.style.display = 'none';
  383. }
  384. });
  385. } // End non-/game branch
  386.  
  387. }); // End tagpro.ready
  388. })(); // End IIFE