Last active
March 16, 2025 21:29
-
-
Save nabbynz/9ca59e661ea4577a6087835a93cd184d to your computer and use it in GitHub Desktop.
TagPro Telestrator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name TagPro Telestrator | |
// @version 2.3.0 | |
// @description Use a telestrator while spectating TagPro! | |
// @include *.koalabeast.com/game | |
// @include *.koalabeast.com/game?* | |
// @updateURL https://gist.github.com/nabbynz/9ca59e661ea4577a6087835a93cd184d/TagPro Telestrator.user.js | |
// @downloadURL https://gist.github.com/nabbynz/9ca59e661ea4577a6087835a93cd184d/TagPro Telestrator.user.js | |
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html | |
// @grant none | |
// @author BBQchicken | |
// ==/UserScript== | |
console.log('START: ' + GM_info.script.name + ' (v' + GM_info.script.version + ' by ' + GM_info.script.author + ')'); | |
/* globals tagpro, tagproConfig, PIXI */ | |
/* eslint-disable no-multi-spaces */ | |
/* eslint-disable dot-notation */ | |
tagpro.ready(function init() { | |
if (!tagpro.playerId || !tagpro.renderer.layers.background || (tagpro.state !== 1 && tagpro.state !== 5)) { | |
return setTimeout(init, 200); | |
} | |
if (!tagpro.spectator) { | |
return false; | |
} | |
var traceLayer = tagpro.renderer.layers.midground; | |
var drawingLayer = tagpro.renderer.layers.foreground; | |
var redBallColor = 0xFF0000; | |
var blueBallColor = 0x0000FF; | |
// ---------- OPTIONS ---------- \\ | |
var kickClick = false; | |
var traceLength = Infinity; | |
var blinkTime = 250; | |
var circleColor = 0xFF6600; | |
var pathColor = 0xFFFF00; | |
var arrowColor = 0xC800FA; | |
var autoTrace = true; | |
var traceDelay = 2000; | |
var matchBallColors = true; | |
// ---------- PATH -------------- \\ | |
var dashOn = true; | |
function Path(start, color, alpha, dashed, layer) { | |
this.points = [start, start, start]; | |
this.color = color; | |
this.alpha = alpha || 0.6; | |
this.dashed = dashed; | |
this.graphics = new PIXI.Graphics(); | |
this.layer = layer || drawingLayer; | |
this.layer.addChild(this.graphics); | |
} | |
Path.prototype.update = function(point) { | |
var points = this.points; | |
this.graphics.lineStyle(6, this.color, this.alpha); | |
var from = points.shift(); | |
points.push(point); | |
this.graphics.moveTo(from.x, from.y); | |
// this.graphics.quadraticCurveTo(points[0].x, points[0].y, points[1].x, points[1].y); | |
// this.graphics.bezierCurveTo(points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y); | |
(!this.dashed || dashOn) && this.graphics.lineTo(points[0].x, points[0].y); | |
} | |
Path.prototype.clear = function(delayMS) { | |
var delay = delayMS || 0; | |
var graphics = this.graphics; | |
var layer = this.layer; | |
setTimeout(function() { | |
graphics.clear(); | |
layer.removeChild(graphics); | |
}, delayMS); | |
} | |
// ---------- TRACE -------------- \\ | |
function Trace(player, auto) { | |
this.player = player; | |
this.path = new Path({x: player.x + 20, y: player.y + 20}, player.team === 1 ? redBallColor : blueBallColor, 0.4, true, traceLayer); | |
this.active = true; | |
this.flaccid = auto; | |
var thisThing = this; //because javascript scope rules | |
this.flaccid && setTimeout(function() { thisThing.flaccid = false; }, 3000); | |
} | |
Trace.prototype.update = function() { | |
this.active && this.path.update({x: this.player.x + 20, y: this.player.y + 20}); | |
}; | |
Trace.prototype.clear = function(delayMS) { | |
this.path.clear(this.flaccid ? 0 : delayMS); | |
}; | |
Trace.prototype.stop = function() { | |
this.active = false; | |
} | |
// ---------- CIRCLE -------------- \\ | |
function Circle(center, color) { | |
this.radius = 0; | |
this.color = color || 0; | |
this.center = center; | |
this.graphics = new PIXI.Graphics(); | |
drawingLayer.addChild(this.graphics); | |
} | |
Circle.prototype.update = function(point) { | |
this.radius = Math.sqrt(Math.pow((point.x - this.center.x), 2) + Math.pow(point.y - this.center.y, 2)); | |
this.graphics.clear(); | |
this.graphics.lineStyle(10, this.color, 0.6); | |
this.graphics.drawCircle(this.center.x, this.center.y, this.radius); | |
} | |
Circle.prototype.clear = function() { | |
this.graphics.clear(); | |
drawingLayer.removeChild(this.graphics); | |
} | |
// ---------- ARROW -------------- \\ | |
function Arrow(start, color) { | |
console.log("Arrow constructor start"); | |
this.start = new PIXI.Point(start.x, start.y); | |
this.wingLength = 45; | |
this.headAngle = .4; | |
this.leftWing = this.start.clone(); | |
this.rightWing = this.start.clone(); | |
this.angle = 0; | |
this.color = color || 0; | |
console.log("Arrow Graphics creation"); | |
this.graphics = new PIXI.Graphics(); | |
drawingLayer.addChild(this.graphics); | |
console.log("Arrow constructor end"); | |
} | |
Arrow.prototype.rotateHead = function(end) { | |
console.log("Arrow rotate right wing"); | |
var phiRight = this.angle + this.headAngle; | |
this.rightWing.x = end.x - this.wingLength * Math.cos(phiRight); | |
this.rightWing.y = end.y - this.wingLength * Math.sin(phiRight); | |
console.log("Arrow rotate left wing"); | |
var phiLeft = this.angle - this.headAngle; | |
this.leftWing.x = end.x - this.wingLength * Math.cos(phiLeft); | |
this.leftWing.y = end.y - this.wingLength * Math.sin(phiLeft); | |
} | |
Arrow.prototype.draw = function(end) { | |
console.log("Arrow draw begin"); | |
this.graphics.clear(); | |
this.graphics.lineStyle(10, this.color, .6); | |
this.graphics.moveTo(this.start.x, this.start.y); | |
this.graphics.lineTo(end.x, end.y); | |
this.graphics.moveTo(this.rightWing.x, this.rightWing.y); | |
this.graphics.lineTo(end.x, end.y); | |
this.graphics.lineTo(this.leftWing.x, this.leftWing.y); | |
console.log("Arrow draw end"); | |
} | |
Arrow.prototype.update = function(end) { | |
console.log("Arrow update begin"); | |
//this.end = end; | |
this.angle = Math.atan2(end.y - this.start.y, end.x - this.start.x); | |
this.rotateHead(end); | |
this.draw(end); | |
console.log("Arrow update end"); | |
} | |
Arrow.prototype.clear = function() { | |
this.graphics.clear(); | |
drawingLayer.removeChild(this.graphics); | |
} | |
// ---------- LOGIC -------------- \\ | |
var current = null, drawing = false, drawings = [], traces = {}; | |
var shift = false, alt = false; | |
var stage = tagpro.renderer.stage; | |
function addUniqueTrace(player, auto) { | |
console.log('trace construction attempted'); | |
if (!traces[player.id]) { | |
traces[player.id] = new Trace(player, auto || false); | |
console.log('trace construction succeeded') | |
} | |
} | |
function removeTrace(id) { | |
if (!traces[id]) { | |
return false; | |
} | |
traces[id].stop(); | |
traces[id].clear(traceDelay); | |
setTimeout(function() { delete traces[id]; }, traceDelay); | |
} | |
tagpro.socket.on('p', function (event) { | |
var events = (tagproConfig.serverHost.search("map") !== -1) ? event.u : event; | |
for (var idx in events) { | |
var e = events[idx]; | |
if ((typeof(e.flag) !== 'undefined') && e.flag) { | |
autoTrace && addUniqueTrace(tagpro.players[e.id], true); | |
} else if((typeof(e['s-pops']) !== 'undefined') || (typeof(e['s-captures']) !== 'undefined')) { | |
removeTrace(e.id); | |
} | |
} | |
}); | |
tpKick = tagpro.kick.player; | |
tagpro.kick.player = function (player) { | |
console.log("kick.player called"); | |
var shiftAlt = (alt && shift); | |
if (kickClick || !(tagpro.spectator || shiftAlt)) { | |
tpKick(player); | |
} | |
if (shiftAlt) { | |
console.log("trace added"); | |
addUniqueTrace(player, false); | |
} | |
} | |
$(document).on("keydown keyup", function (event) { | |
shift = event.shiftKey; | |
alt = event.altKey; | |
}); | |
$(document).dblclick(function(event) { | |
window.getSelection().removeAllRanges(); | |
for (var i in drawings) { | |
drawings[i].clear(); | |
} | |
drawings = []; | |
if (!event.shiftKey) { | |
return false; | |
} | |
for (var i in traces) { | |
traces[i] && traces[i].clear(); | |
} | |
traces = {}; | |
}); | |
//thanks to ProfessorTag | |
function canvasMousePosition(e) { | |
var tr = tagpro.renderer; | |
var resizeScaleFactor = tr.options.disableViewportScaling ? 1 : (tr.vpHeight / tr.canvas_height).toFixed(2), | |
scale = (tagpro.zoom / resizeScaleFactor), | |
x1 = tr.gameContainer.x * scale, | |
x2 = x1 - tr.vpWidth * scale, | |
x = - Math.round((x1 + x2) / 2 - (e.x - innerWidth / 2) * scale), | |
y1 = tr.gameContainer.y * scale, | |
y2 = y1 - tr.vpHeight * scale, | |
y = - Math.round((y1 + y2) / 2 - (e.y - innerHeight / 2) * scale); | |
return {x: x, y: y}; | |
} | |
if (matchBallColors) { | |
let canvas = new OffscreenCanvas(80, 40); | |
let ctx = canvas.getContext("2d"); | |
ctx.drawImage(tagpro.tiles.image, 560,0,80,40, 0,0,80,40); | |
let rgbToHex = function(r, g, b) { | |
return "0x" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); | |
}; | |
let getAverageColor = function(data, asHex=false, ignoreGrey=false, pixelInterval=1) { | |
let rgb = { r:null, g:null, b:null }; | |
let length = data.length; | |
let count = 0; | |
let i = -4; | |
while ((i += pixelInterval * 4) < length) { | |
//let b = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114; | |
let isGrey = ignoreGrey && (Math.abs(data[i] - data[i+1]) + Math.abs(data[i+1] - data[i+2]) < 10); | |
if (!isGrey && data[i+3]) { | |
rgb.r += data[i]; | |
rgb.g += data[i+1]; | |
rgb.b += data[i+2]; | |
count++; | |
} | |
} | |
if (count === 0) return null; //transparent or all grey | |
rgb.r = Math.floor(rgb.r / count); | |
rgb.g = Math.floor(rgb.g / count); | |
rgb.b = Math.floor(rgb.b / count); | |
return rgbToHex(rgb.r, rgb.g, rgb.b).slice(0, 8); | |
}; | |
let ballPixelData = ctx.getImageData(5, 5, 30, 30).data; | |
redBallColor = getAverageColor(ballPixelData, true, true) || redBallColor; | |
ballPixelData = ctx.getImageData(45, 5, 30, 30).data; | |
blueBallColor = getAverageColor(ballPixelData, true, true) || blueBallColor; | |
} | |
window.onmousedown = function (click) { | |
drawing = true; | |
} | |
window.onmousemove = function(click) { | |
if (!drawing) { | |
return false; | |
} else if (current) { | |
current.update(canvasMousePosition(click)); | |
} else { | |
if (shift && alt) { | |
drawing = false; | |
} else if (shift) { | |
current = new Arrow(canvasMousePosition(click), arrowColor); | |
} else if (alt) { | |
current = new Circle(canvasMousePosition(click), circleColor); | |
} else { | |
current = new Path(canvasMousePosition(click), pathColor); | |
} | |
} | |
} | |
window.onmouseup = function(click) { | |
console.log("mouseup"); | |
drawing = false; | |
current && drawings.push(current); | |
current = null; | |
} | |
function drawAll() { | |
for (var idx in traces) { | |
trace = traces[idx]; | |
trace && trace.update(); | |
} | |
requestAnimationFrame(drawAll); | |
} | |
requestAnimationFrame(drawAll); | |
setInterval(function() { | |
dashOn = !dashOn; | |
}, blinkTime); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment