TagPro Player Monitor

Shows an on-screen list of players in the game and their current status

// ==UserScript==
// @name          TagPro Player Monitor
// @version       4.4
// @author        bash# ; Ko
// @description   Shows an on-screen list of players in the game and their current status
// @match         *://*.koalabeast.com/*
// @match         *://*.jukejuice.com/*
// @match         *://*.newcompte.fr/*
// @namespace     https://greasyfork.org/users/152992
// @icon          https://github.com/wilcooo/TagPro-ScriptResources/raw/master/playermonitor.png
// @supportURL    https://www.reddit.com/message/compose/?to=Wilcooo
// @website       https://redd.it/6pe5e9
// @require       https://greasyfork.org/scripts/371240/code/TagPro%20Userscript%20Library.js
// @grant         GM_getValue
// @grant         GM_setValue
// @license       MIT
// ==/UserScript==







var short_name = 'monitor';            // An alphabetic (no spaces/numbers) distinctive name for the script.
var version = GM_info.script.version;  // The version number is automatically fetched from the metadata.
tagpro.ready(function(){ if (!tagpro.scripts) tagpro.scripts = {}; tagpro.scripts[short_name]={version:version};});
console.log('START: ' + GM_info.script.name + ' (v' + version + ' by ' + GM_info.script.author + ')');




// SETTINGS (edit them on the TagPro homepage or on the scoreboard

var settings = tpul.settings.addSettings({
    id: 'monitor',
    title: "Configure the Player Monitor",
    tooltipText: "Player Monitor",
    icon: "https://github.com/wilcooo/TagPro-ScriptResources/raw/master/playermonitor.png",

    fields: {
        hide_flagTaken: {
            label: 'Hide the default taken flag indicators',
            section: ['','Note: You might need to refresh for some settings to take effect.\n\n<br><br>By default, TagPro will show you what flags are taken, and the number of players on each team. This script does the same (but more), so you probably want to hide the default indicators.'],
            type: 'checkbox',
            default: true
        },
        hide_playerIndicators: {
            label: 'Hide the default team count indicators',
            type: 'checkbox',
            default: true
        },
        position: {
            label: 'The position preset',
            section: ['','Choose one of these eleven position presets; so Top/Middle/Bottom, Left/Middle/Right or Split. "Split" means that the blue red team is shown on the left side, and the blue team on the right side of the screen.'],
            type: 'select',
            // add your-preset to the following list if you want to use it LEOPARD
            options: ['top-left', 'top-mid', 'top-right', 'mid-left', 'mid-right', 'bot-left', 'bot-mid', 'bot-right','top-split', 'mid-split', 'bot-split'],
            default: 'bot-mid'
        },
        order: {
            label: 'Sorting order of the players',
            type: 'select',
            options: ['constant','alphabetic','score'],
            default: 'constant'
        },
        show_hold: {
            label: 'Show a timer that counts how long the FC has been holding',
            type: 'checkbox',
            default: false
        },
        show_honk: {
            label: 'Show when a (nearby) player is honking. Only works when the Honk script is installed.',
            type: 'checkbox',
            default: true
        }
    },

    events: {
        save: function(){
            hide_flagTaken = settings.get("hide_flagTaken")
            hide_playerIndicators = settings.get("hide_playerIndicators")
            position = settings.get("position")
            order = settings.get("order")
            show_honk = settings.get("show_honk")
            show_hold = settings.get("show_hold")

            if (tpul.playerLocation == 'game'){
                preset = presets[position] || presets["bot-mid"];
                tagpro.ui.alignUI();
            }
        }
    }
});

var hide_flagTaken = settings.get("hide_flagTaken"),
    hide_playerIndicators = settings.get("hide_playerIndicators"),
    position = settings.get("position"),
    order = settings.get("order"),
    show_honk = settings.get("show_honk"),
    show_hold = settings.get("show_hold");




// CONSTANTS

const size        = 16;     // Size of a ball icon
const space       = 18;     // Vertical space per name+icon
const textVshift  = 0;      // relative vertical shift of text
const textHLshift = -2;     // relative horizontal shift of text on the left of a ball
const textHRshift = 25;     // relative horizontal shift of text on the right of a ball

const show_ball = true;     // Whether to show the ball icon
                            //   (if you disable this, you might want to disable show_grip, show_speed, show_tagpro, show_bomb)
const show_dead = true;     // Whether to fade the ball when its dead/spawning
const show_name = true;     // Shows the playernames next to the corresponding balls (recommended)

const show_flag = true;     // Show a flag next to players with a flag
const flag_size = size;     // size of the flag icon next to an FC
const flag_x    = 10;       // Position of the flag, relative to the ball
const flag_y    = 0;

const show_grip = true;     // Show a JJ pup on balls with Juke Juice
const grip_size = 10;       // size of the Juke Juice icon
const grip_x    = -1;       // relative position
const grip_y    = 8;

const show_speed  = false;  // This won't work unless they put the topspeed pup back in the game (and even then probably not)
const speed_size  = 10;     // size of the Top Speed icon (a deprecated TagPro powerup)
const speed_x     = -1;     // relative position
const speed_y     = -3;

const show_tagpro = true;       // Show a green circle on balls with a TP
const tagpro_color = 0x00FF00;  // Color of the (usually green) TagPro powerup circle
const tagpro_thick = 1.5;       // Thickness of that circle

                                // Tip, use : https://www.google.com/search?q=pick+color

const show_bomb = true;         // Flash balls with a Rolling Bomb
const bomb_color = 0xFFFF00;    // Color of the flashing RollingBomb

const style =     // The style of the text (1: red, 2: blue)
      {
          1: {
              fontSize:        "8pt",
              fontWeight:      "bold",
              strokeThickness: 3,
              fill:            0xFFB5BD,       // text-color (Tip: https://www.google.com/search?q=pick+color)
          },
          2: {
              fontSize:        "8pt",
              fontWeight:      "bold",
              strokeThickness: 3,
              fill:            0xCFCFFF,
          },
      };

const presets = {    // 1: red team ,  2: blue team
    'top-left' : {
        1 : {  x:10, y:10,  },
        2 : {  x:10, y:10 + 4.5*space,  },
    },
    'mid-left' : {
        1 : {  x:10, y:-0.75*space, anchor : {x:0, y:0.5}, bottomToTop: true,  },
        2 : {  x:10, y: 0.75*space, anchor : {x:0, y:0.5},  },
    },
    'bot-left' : {
        1 : {  x:10, y:-10 - 5.5*space, anchor : {x:0, y:1}, bottomToTop: true,  },
        2 : {  x:10, y:-10 - space,     anchor : {x:0, y:1}, bottomToTop: true,  },
    },
    'top-mid' : {
        1 : {  x:-25, y:10, anchor : {x:0.5, y:0}, leftText: true,  },
        2 : {  x:5,   y:10, anchor : {x:0.5, y:0},  },
    },
    'bot-mid' : {
        1 : {  x:-135, y:-10 - space, anchor : {x:0.5, y:1}, bottomToTop: true, leftText: true,  },
        2 : {  x: 115, y:-10 - space, anchor : {x:0.5, y:1}, bottomToTop: true,  },
    },
    'top-right' : {
        1 : {  x:-30, y:10,             anchor : {x:1, y:0}, leftText: true,  },
        2 : {  x:-30, y:10 + 4.5*space, anchor : {x:1, y:0}, leftText: true,  },
    },
    'mid-right' : {
        1 : {  x:-30, y:-0.75*space, anchor : {x:1, y:0.5}, bottomToTop: true, leftText: true,  },
        2 : {  x:-30, y: 0.75*space, anchor : {x:1, y:0.5},                    leftText: true,  },
    },
    'bot-right' : {
        1 : {  x:-30, y:-10 - 5.5*space, anchor : {x:1, y:1}, bottomToTop: true, leftText: true,  },
        2 : {  x:-30, y:-10 - space,     anchor : {x:1, y:1}, bottomToTop: true, leftText: true,  },
    },
    'top-split' : {
        1 : {  x:10,  y:10,  },
        2 : {  x:-30, y:10, anchor : {x:1, y:0}, leftText: true, },
    },
    'mid-split' : {
        1 : {  x:10,  y:-2*space, anchor : {x:0, y:0.5},  },
        2 : {  x:-30, y:-2*space, anchor : {x:1, y:0.5}, leftText: true,  },
    },
    'bot-split' : {
        1 : {  x:10,  y:-10 - space, anchor : {x:0, y:1}, bottomToTop: true,  },
        2 : {  x:-30, y:-10 - space, anchor : {x:1, y:1}, bottomToTop: true, leftText: true,  },
    },
    'your-preset' : {             // A preset to experiment with!
                                  // Don't forget to add your-preset to the options list, search this script (ctrl+F) for LEOPARD
        1 : {  x:0, y:0,  },
        2 : {  x:0, y:0,  },
    },
};

// EXPLANATION OF THE PRESETS:
//   'x' and 'y' form the position of the player monitor
//   'anchor' is the reference point from which the 'x' and 'y' above are calculated.
//     anchors 'x' and 'y' are position of the reference point, relative to the viewport size. (so 0.5 is in the middle of the screen)
//
//   The position described above is the position of the first ball in the player monitor.
//     The next balls are usually drawn below it, but when 'bottomToTop' is true, they are drawn above the first one.
//
//   'leftText' makes the name of the ball appear on the left side of the ball.
//
//   You may add a preset to the list, and select it in the options on the homepage





var preset = presets[position] || presets["bot-mid"];   // If no valid preset is chosen, fallback to 'bot-mid'

if (tpul.playerLocation == 'game') {

    const flagsprite =
          {
              1: "red",      // Note: 'flag' or 'potato' gets added to this later in this script
              2: "blue",
              3: "yellow",
          };
    const ballsprite =
          {
              1: "redball",
              2: "blueball",
          };

    const honksprite = PIXI.Texture.fromImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTZEaa/1AAACiUlEQVR4Xu3bMa7bQAwG4RwkZe5/s5zBiYoAgjFFJJPL1fgvPjxgDAjksn4/Xq/XV/n989ffP/ybEUar47j/vP9mhdEqB5bLgeVyYLkcWC4HlsuB5XJguRxYLgeWy4HlcmC5HFguB5bLgW940qM9ZdbKOTFecR6mYqBOT5jzPGPFnBiveB+oYqguu894nq9qToxXdQzWYef5zrNVzojxjq4Bq+041/u7Vc6I8a7OQa263wzjJ7oHNlnxVhg/tWLwp1v1RhgrrFrgiVa+DcYqKxd5itVvgrHS6oV2NvEWGKtNLLabqTfA2GFqwR1M7o6xy+SiU6Z3xthpctkJ0/ti7Da58GrTu2IMD4zhgTE8MIYHxvDAGB4YwwNjeGAMD4zhgTE8MIYHxvDAGB4YwwNjeGAMD4zhgTE8MIYHxvDAGB4YwwNjeGAMD4zhgTHmnP/V5VPH91o+GvfQe36q5aPvg8f/o/e86/hey0djHxjDA2N4YAwPjOGBMTwwhgfG8MAYHhjDA2N4YAwPjOGBMTwwhgfG8MAYHhjDA2N4YAwPjOGBMTwwhgfG8MAYHhjDA2N4YOz2Tf/qMr0rxk7nhaeWXml6X4xd3pedWHi16Z0xdphedNLk7hirTS64i6k3wFhparEdTbwFxioTC+1u9ZtgrLB6kSdZ+TYYP7Vygada9UYYP7FqcIMVb4XxrhUD23S/GcY7ugetsONMh/d3q5wT41WdA1bZebbDeb7KOTFe0TVYtd3nO5xnrJoV4xXVA3V5woyH85wVs2K8onKYTk+Z81A5K0ajykd7EoxGObBcDiyXA8vlwHI5sFwOLJcDy+XAcjmwXA4slwPL5cByObBcDvwFvu24r9frxx9ThG6n1YCdggAAAABJRU5ErkJggg==");

    const refresh_rate = 3e3;   // An interval (milliseconds), after which the order of the playerlists gets updated.

    tagpro.ready(function () {




        // Hide the flagTaken indicators

        if (hide_flagTaken)
            tagpro.renderer.updateFlagsFromPlayer = function() {
                console.warn('The flag indicators are blocked by the TagPro Player Monitor script');
                tagpro.renderer.updateFlagsFromPlayer = ()=>0;
            };

        // Hide TagPro's new Player Indicators

        if (hide_playerIndicators) {
            tagpro.ui.updatePlayerIndicators = function() {
                console.warn('The player indicators are blocked by the TagPro Player Monitor script');
                tagpro.ui.updatePlayerIndicators = ()=>0;
            };
        }


        // This function returns a summary of a player.
        // To be used to check whether something has changed.

        function getPlayer(player) {

            var state = {
                team: player.team,
                id:   player.id,
            };

            if (show_name)   state.name      = player.name;
            if (show_dead)   state.dead      = player.dead;
            if (show_flag)   state.flag      = player.flag;
            if (show_bomb)   state.bomb      = player.bomb;
            if (show_tagpro) state.tagpro    = player.tagpro;
            if (show_grip)   state.grip      = player.grip;
            if (show_speed)  state.speed     = player.speed;
            if (show_honk)   state.isHonking = player.isHonking;

            return state;
        }





        // Create PIXI containers for both player lists

        var redList = new PIXI.Container();
        tagpro.renderer.layers.ui.addChild(redList);

        var blueList = new PIXI.Container();
        tagpro.renderer.layers.ui.addChild(blueList);

        var teamLists =
            {
                1: redList,
                2: blueList,
            };

        tagpro.ui.sprites.redPlayerMonitor = redList;
        tagpro.ui.sprites.bluePlayerMonitor = blueList;


        // This function gets called when the browser window resizes
        // It moves the playerlists to the right location

        var org_alignUI = tagpro.ui.alignUI;
        tagpro.ui.alignUI = function() {
            redList.x = ( preset[1].anchor ? (tagpro.renderer.vpWidth * preset[1].anchor.x) : 0 ) + preset[1].x;
            redList.y = ( preset[1].anchor ? (tagpro.renderer.vpHeight * preset[1].anchor.y) : 0 ) + preset[1].y;
            blueList.x = ( preset[2].anchor ? (tagpro.renderer.vpWidth * preset[2].anchor.x) : 0 ) + preset[2].x;
            blueList.y = ( preset[2].anchor ? (tagpro.renderer.vpHeight * preset[2].anchor.y) : 0 ) + preset[2].y;

            // Move a few pixels down when overlapping with the FPS/ping
            if (tagpro.settings.ui.performanceInfo && ['top-left','top-split'].includes(position)) {
                redList.y += 20
                blueList.y += 20
            }
            org_alignUI();
        };

        tagpro.ui.alignUI();



        function format_time(seconds=0) { // example: 179 seconds will return (2:59)
            var minutes = Math.floor(seconds / 60),
                seconds = ("0" + seconds % 60).slice(-2)
            return " (" + minutes + ":" + seconds + ") "
        }





        // The rolling_bomb graphics are stored here, so that they can be updated (animated) seperately
        if (show_bomb) {
            var rolling_bombs = {};

            // Rewriting the TagPro's ui.update function to include the rendering of the RBs
            tagpro.ui.org_update = tagpro.ui.update;
            tagpro.ui.update = function () {
                for (var b in rolling_bombs) {
                    rolling_bombs[b].alpha = (show_dead && tagpro.players[b] && tagpro.players[b].dead ? 0.375 : 0.75)*Math.abs(Math.sin(performance.now() / 150));
                }
                return tagpro.ui.org_update(...arguments)
            };
        }


        var hold_update_functions = {};


        // Update a single player

        function drawPlayer(player) {

            if (typeof player.monitor === 'undefined') {
                player.monitor = new PIXI.Container();
                player.monitor.hold_time = null;
            }

            player.monitor.removeChildren();

            // Draw ball
            if (show_ball) tagpro.tiles.draw(player.monitor, ballsprite[player.team], { x: 0, y: 0 }, size, size, player.dead ? 0.5 : 1);

            // Draw bomb (rolling bomb)
            if (show_bomb && player.bomb) {
                var bomb = new PIXI.Graphics();
                bomb.beginFill(bomb_color, (player.dead ? 0.375 : 0.75)*Math.abs(Math.sin(performance.now() / 150)) );
                bomb.drawCircle(size/2, size/2, size/2);

                player.monitor.addChild(bomb);

                rolling_bombs[player.id] = bomb;
            } else if (show_bomb) delete rolling_bombs[player.id];

            // Draw tagpro
            if (show_tagpro && player.tagpro) {
                var tp = new PIXI.Graphics();
                tp.lineStyle(tagpro_thick, tagpro_color, player.dead ? 0.5 : 1 );
                tp.drawCircle(size/2, size/2, size/2);

                player.monitor.addChild(tp);
            }

            // Draw honk
            if (show_honk && player.isHonking) {
                var honk = new PIXI.Sprite(honksprite);
                honk.width  = honksprite.width  * (size/40);
                honk.height = honksprite.height * (size/40);
                honk.x = ( -honk.width  + size ) / 2;
                honk.y = ( -honk.height + size ) / 2;

                player.monitor.addChild(honk);
            }

            // Draw grip (juke juice)
            if (show_grip && player.grip) {
                tagpro.tiles.draw(player.monitor, 'grip' , { x: grip_x, y: grip_y }, grip_size, grip_size, show_dead && player.dead ? 0.5 : 1);
            }

            // Draw speed (a deprecated powerup)
            if (show_speed && player.speed) {
                tagpro.tiles.draw(player.monitor, 'speed' , { x: speed_x, y: speed_y }, speed_size, speed_size, show_dead && player.dead ? 0.5 : 1);
            }

            // Draw name
            if (show_name) {
                var name = new PIXI.Text(player.name, style[player.team]);

                if (preset[player.team].leftText)   name.x = textHLshift - name.width;
                else                                name.x = textHRshift;

                name.y     = textVshift;
                name.alpha = (show_dead && player.dead) ? 0.5 : 1;

                player.monitor.addChild(name);
            }

            // Draw flag/potato
            if (show_flag && player.flag && !player.dead) {
                tagpro.tiles.draw(player.monitor, flagsprite[player.flag]+(player.potatoFlag ? 'potato':'flag') , { x: flag_x, y: flag_y }, flag_size, flag_size);

                // Draw hold time
                if (show_hold) {

                    var hold = new PIXI.Text(format_time(player.monitor.hold_time), style[player.team])

                    if (preset[player.team].leftText)   hold.x = textHLshift - hold.width - (name ? name.width:0);
                    else                                hold.x = textHRshift + (name ? name.width:0);

                    hold.y = textVshift;
                    hold.alpha = (show_dead && player.dead) ? 0.5 : 1;

                    player.monitor.addChild(hold)

                    // Update the hold time every second

                    player.monitor.update_hold = function(t){
                        if (!(player.id in tagpro.players)) return
                        if (tagpro.state == 2) return

                        hold.text = format_time(++player.monitor.hold_time)
                        if (preset[player.team].leftText) hold.x = textHLshift - hold.width - (name ? name.width:0)

                        setTimeout( player.monitor.update_hold, 1000 - (Date.now()-t)%1000, t )
                    }

                    if (player.monitor.hold_time == null) {
                        setTimeout( player.monitor.update_hold, 1000, Date.now()%1000 )
                    }
                }
            } else {
                player.monitor.hold_time = null
                player.monitor.update_hold = ()=>{}
            }

        }






        // Update either the red or blue list

        function orderTeamList(team) {

            var teamList = teamLists[team];

            teamList.removeChildren();

            var teamPlayers = [];

            for (let p in tagpro.players) {

                let player = tagpro.players[p];

                if (player.team != team)    continue;

                if (!player.monitor) {
                    drawPlayer(player);
                }

                teamPlayers.push(player);
            }


            if (preset[team].bottomToTop) var sign = -1
            else var sign = 1

            switch (order) {
                case 'score':
                    teamPlayers.sort( (p1,p2) => sign * ( p2.score - p1.score ) );
                    // This sorts the teamPlayers list based on the .score of every player (desc.)
                    break;
                case 'alphabetic':
                    teamPlayers.sort( (p1,p2) => sign * ( p1.name.toLowerCase() > p2.name.toLowerCase() || -(p1.name.toLowerCase() < p2.name.toLowerCase()) ) );
                    // This sorts the teamPlayers list based on the .score of every player (asc.)
                    break;
                default:
                    // When something else, or 'constant' is chosen, the order of player id's is conserved
                    // which is the order that they joined the game.
            }

            var count = 0;

            for (let player of teamPlayers) {
                teamList.addChild(player.monitor);
                player.monitor.y = sign * space * (count++);
            }

        }





        tagpro.socket.on("p", function(data) {

            if (data instanceof Array) {
                let player = tagpro.players[data[0].id];
                drawPlayer(player);
                orderTeamList(player.team);
                return;
            }

            for (var p in data.u) {
                let player = tagpro.players[data.u[p].id];
                var old_json = player.json;
                player.json = JSON.stringify(getPlayer(player));
                if (player.json != old_json) {
                    drawPlayer(player);
                }
            }

        });

        // When tagpro receives a playerLeft event, it deletes the player.
        // We want to know what team the player was on so we can update only the necessary teamlist.
        // So instead of using tagpro.socket.on, we use a trick to insert our listener
        // BEFORE all other listeners for the playerLeft event.
        tagpro.rawSocket.listeners('playerLeft').unshift( function(data) {
            let player = tagpro.players[data]; // < now we know for sure that the player still exists!
            //delete player.monitor;
            setTimeout(orderTeamList(player.team)) // update the teamlist AFTER TagPro has removed the player
        } )

        /*tagpro.socket.on('playerLeft', function(p) {
            orderTeamList(1); orderTeamList(2);
        }*/

        org_drawHonk = tagpro.drawHonk || (() => 0);
        tagpro.drawHonk = function(player, remove) {
            org_drawHonk(player, remove);
            drawPlayer(player);
        };

        // Just in case "something" happens, we update the teamlists every once in a while.
        setInterval(function() {orderTeamList(1); orderTeamList(2);}, refresh_rate);

    });

}