Skip to content

Instantly share code, notes, and snippets.

@nabbynz
Last active February 20, 2025 03:07
Show Gist options
  • Save nabbynz/18064d818c5d9e39e65d7682e1d826dd to your computer and use it in GitHub Desktop.
Save nabbynz/18064d818c5d9e39e65d7682e1d826dd to your computer and use it in GitHub Desktop.
TagPro Highlight Flag Holder + Follow NF Flag in Spec
// ==UserScript==
// @name TagPro Highlight Flag Holder + Follow NF Flag in Spec
// @description Adds a circle to the flag holder. Follows the NF Flag Holder when spectating
// @version 1.3.0
// @match *://*.koalabeast.com/game
// @match *://*.koalabeast.com/game?*
// @updateURL https://gist.github.com/nabbynz/18064d818c5d9e39e65d7682e1d826dd/raw/TagPro_Highlight_Flag_Holder.user.js
// @downloadURL https://gist.github.com/nabbynz/18064d818c5d9e39e65d7682e1d826dd/raw/TagPro_Highlight_Flag_Holder.user.js
// @grant none
// @author nabby
// ==/UserScript==
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')');
'use-strict';
/* globals tagpro, tagproConfig, PIXI */
/* eslint-disable no-multi-spaces */
/* eslint-disable dot-notation */
// Options...
let USE_GLOW_CIRCLES = true; // apply the circle highlight
let ADD_CIRCLE_TO_FLAG = false; // true = add circle to flag; false = add circle to ball
let USE_FOLLOW_YELLOW_FLAG_IN_SPEC = true; // follow the flag holder in NF
tagpro.ready(function() {
let tr = tagpro.renderer;
let flagGlowTexture;
let createFlagGlowTexture = function() {
const canvasSize = 64;
const canvasCenter = canvasSize / 2;
const glowOpacity = ADD_CIRCLE_TO_FLAG ? 0.8 : 1;
const glowBlur = ADD_CIRCLE_TO_FLAG ? 2 : 0;
const glowRadius = ADD_CIRCLE_TO_FLAG ? 17 : 12;
const holeRadius = ADD_CIRCLE_TO_FLAG ? 13 : 8;
const holeOpacity = ADD_CIRCLE_TO_FLAG ? 1 : 1;
const holeBlur = ADD_CIRCLE_TO_FLAG ? 1 : 0;
let canvas = createCanvas(canvasSize, canvasSize);
let ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.globalAlpha = glowOpacity;
ctx.filter = 'blur(' + glowBlur + 'px)';
ctx.beginPath();
ctx.arc(canvasCenter, canvasCenter, glowRadius, 0, Math.PI * 2);
ctx.fill();
if (holeRadius > 0 && holeOpacity > 0) {
ctx.globalCompositeOperation = 'destination-out';
ctx.globalAlpha = holeOpacity;
ctx.filter = 'blur(' + holeBlur + 'px)';
ctx.beginPath();
ctx.arc(canvasCenter, canvasCenter, holeRadius, 0, Math.PI * 2);
ctx.fill();
}
flagGlowTexture = PIXI.Texture.from(canvas);
};
createFlagGlowTexture();
const glowColors = {
'redflag': 0xffd100,
'blueflag': 0xffd100,
'yellowflag': 0xffd100,
'redpotato': 0xffd100,
'bluepotato': 0xffd100,
'yellowpotato': 0xffff00,
'redclutch': 0xff44ff,
'blueclutch': 0xff44ff,
'yellowclutch': 0xff44ff,
'none': 0xffffff,
'glowFlag': 0xff00ff,
};
let addFlagGlowSprite = function(player) {
if (player.sprites.glowFlag) {
player.sprites.glowFlag.destroy();
}
player.sprites.glowFlag = new PIXI.Sprite(flagGlowTexture);
player.sprites.glowFlag.name = 'glowFlag';
player.sprites.glowFlag.anchor.set(0.5, 0.5);
player.sprites.glowFlag.name = player.sprites.flag ? player.sprites.flag.name : 'glowFlag';
player.sprites.glowFlag.tint = glowColors[player.sprites.glowFlag.name];
player.sprites.glowFlag.visible = false;
if (ADD_CIRCLE_TO_FLAG) {
player.sprites.glowFlag.position.set(33, -12); // behind flag
player.sprites.flagLayer.addChild(player.sprites.glowFlag);
} else {
player.sprites.glowFlag.position.set(20, 20); // ball centre
player.sprites.ball.addChild(player.sprites.glowFlag);
}
};
let currentSpecPlayerId = tagpro.playerId;
let flagTypes = { yellow: [], red: [], blue: [] };
getMapType = function() {
if (!tagpro.map || !tagpro.map[0].length) {
setTimeout(getMapType, 20);
return;
}
for (let x = 0; x < tagpro.map.length; x++) {
for (let y = 0; y < tagpro.map[0].length; y++) {
const baseTileId = Math.floor(tagpro.map[x][y]);
if (baseTileId === 3) {
flagTypes.red.push({ x: x, y: y });
} else if (baseTileId === 4) {
flagTypes.blue.push({ x: x, y: y });
} else if (baseTileId === 16) {
flagTypes.yellow.push({ x: x, y: y });
}
}
}
};
getMapType(); // for auto-follow yellow flag
let uPF = tr.updatePlayerFlag;
tr.updatePlayerFlag = function(player) {
uPF(player);
if (USE_GLOW_CIRCLES) {
if (!player.sprites.glowFlag || player.sprites.flag && (player.sprites.glowFlag.name !== player.sprites.flag.name)) {
addFlagGlowSprite(player);
}
player.sprites.glowFlag.visible = !!player.flag;
}
// this is to always follow the yellow flag carrier in spec...
if (USE_FOLLOW_YELLOW_FLAG_IN_SPEC && tagpro.spectator && !tagpro.viewport.centerLock && player.flag === 3 && player.id !== currentSpecPlayerId) {
tagpro.viewport.followPlayer = true;
if (player.team === 1) {
if (tagproConfig.replay) {
tagpro.replayActions.redflagcarrier(true);
} else {
tagpro.socket.emit('redflagcarrier');
}
} else if (player.team === 2) {
if (tagproConfig.replay) {
tagpro.replayActions.blueflagcarrier(false);
} else {
tagpro.socket.emit('blueflagcarrier');
}
}
currentSpecPlayerId = player.id;
}
};
if (USE_FOLLOW_YELLOW_FLAG_IN_SPEC && (tagpro.spectator || tagproConfig.replay)) {
document.addEventListener('keydown', (e) => {
if (e.which === 70) { // uses the 'f' key to enable/disable auto-follow (TODO: add visual indicator)
if (USE_FOLLOW_YELLOW_FLAG_IN_SPEC) {
USE_FOLLOW_YELLOW_FLAG_IN_SPEC = false;
} else {
USE_FOLLOW_YELLOW_FLAG_IN_SPEC = true;
if (tagpro.spectator || tagproConfig.replay) { // in case we moved from spec to playing
for (let playerId in tagpro.players) {
if (tagpro.players[playerId].flag === 3) {
tr.updatePlayerFlag(tagpro.players[playerId]);
break;
}
}
}
}
}
});
let lastScoreRed, lastScoreBlue;
if (flagTypes.yellow.length === 1 && !flagTypes.red.length && !flagTypes.blue.length) { // single yellow flag only
tagpro.socket.on('score', function(data) { // sent at the start of the match (initialize), a cap being scored (move camera), or when a spectator joins (ignore)
if (lastScoreRed === undefined || lastScoreBlue === undefined || !USE_FOLLOW_YELLOW_FLAG_IN_SPEC) { // initialize
lastScoreRed = data.r;
lastScoreBlue = data.b;
return;
}
let centerCamera = true;
if (lastScoreRed !== data.r || lastScoreBlue !== data.b) { // cap
setTimeout(() => { // player.flag (and tagpro.state at game end) sometimes hasn't been set yet - need a delay
if (tagpro.state !== 2) {
currentSpecPlayerId = null;
for (let playerId in tagpro.players) {
if (tagpro.players[playerId].flag === 3) {
tr.updatePlayerFlag(tagpro.players[playerId]);
centerCamera = false;
break;
}
}
if (centerCamera) {
setTimeout(() => {
for (let playerId in tagpro.players) { // check again for grabs within 500ms
if (tagpro.players[playerId].flag === 3) {
tr.updatePlayerFlag(tagpro.players[playerId]);
centerCamera = false;
break;
}
}
if (centerCamera) {
tagpro.viewport.followPlayer = false;
tr.centerCameraPosition(0);
}
}, 500);
}
lastScoreRed = data.r;
lastScoreBlue = data.b;
}
}, 100)
}
});
}
}
});
let createCanvas = function(width, height, forceDOM=false) {
if (typeof OffscreenCanvas !== 'undefined' && !forceDOM) {
return new OffscreenCanvas(width, height);
} else {
let canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment