~client/404.html:
`
<!--
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
-->

<!DOCTYPE html>

<html>
    <head>
        <link href="https://fonts.googleapis.com/css?family=Ubuntu:700" rel="stylesheet" type="text/css" />
        <style>
            body {
                background-color: black;
            }

            .err {
                color: red;
                font-family: 'Ubuntu';
                font-size: 11rem;
                text-align: center;
                margin: auto;
            }

            .msg {
                text-align: center;
                font-family: 'Ubuntu';
                font-size: 1.6rem;
                margin: auto;
                width: 75%;
                color: white;
            }

            a {
                text-decoration: none;
                color: rgb(0, 102, 255);
            }

            a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    
    <body>
        <div class="err">404</div>
        <div class="msg">The file you were looking for does not exist<p>Let's go <a href="/">home</a> and try from there</p></div>
    </body>
</html>
`

~client/config.js:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

const BUILD = "6f59094d60f98fafc14371671d3ff31ef4d75d9e";
const CDN = "https://static.diep.io/";
const API_URL = `${window.location.href}api/`;

const CHANGELOG = [
    "Updated recently",
    "",
    "Check out the GitHub repository!: github.com/ABCxFF/diepcustom",
    "Join our Discord server: discord.gg/SyxWdxgHnT :)"
];

const ASM_CONSTS = {
    1024: "createCanvasCtxWithAlpha",
    3781: "createImage",
    4211: "websocketSend",
    4388: "wipeContext",
    4528: "modulo",
    4551: "wipeSocket",
    4777: "setTextInput",
    4827: "wipeImage",
    4902: "reloadWindowTimeout",
    4968: "existsInWindowObject",
    5011: "empty", // ads
    5093: "empty", // ads
    5124: "empty", // ads
    5443: "empty", // ads
    5506: "empty", // ads
    5563: "empty", // ads
    5827: "getQueries",
    6547: "empty", // ads
    6927: "empty", // ads
    7068: "getLocalStorage",
    7293: "deleteLocalStorage",
    7343: "removeChildNode",
    7449: "checkElementProperty",
    7675: "existsQueryOrIsBlank",
    7886: "empty", // ads
    8008: "setLocalStorage",
    8254: "empty", // ads
    8472: "empty", // ads
    9178: "empty", // ads
    9362: "getGamepad",
    9558: "toggleFullscreen",
    10188: "getCanvasSize",
    10299: "setCursorDefault",
    10363: "setCursorPointer",
    10427: "setCursorText",
    10488: "getTextInput",
    10530: "enableTyping",
    11181: "disableTyping",
    11296: "focusCanvas",
    11444: "setCanvasSize",
    11529: "empty", // anti cheat
    11586: "copyUTF8",
    11637: "alert",
    11884: "saveContext",
    11952: "restoreContext",
    12020: "scaleContextAlpha",
    12063: "empty", // ads
    12501: "empty", // ads
    12976: "empty", // ads
    13247: "empty", // ads
    13331: "empty", // ads
    13414: "setContextFillStyle",
    13493: "setContextTransform",
    13560: "contextFillRect",
    13611: "contextBeginPath",
    13645: "contextClip",
    13674: "contextFill",
    13703: "setContextLineJoinRound",
    13744: "setContextLineJoinBevel",
    13785: "setContextLineJoinMiter",
    13826: "setContextLineWidth",
    13863: "setContextStrokeStyle",
    13939: "setContextTransformBounds",
    14002: "contextStroke",
    14033: "contextRect",
    14072: "getFontsLoaded",
    14174: "setContextFont",
    14240: "measureContextTextWidth",
    14308: "setContextAlpha",
    14347: "contextFillText",
    14402: "contextStrokeText",
    14459: "setContextTextBaselineTop",
    14502: "setContextTextBaselineHanging",
    14549: "setContextTextBaselineMiddle",
    14595: "setContextTextBaselineAlphabetic",
    14645: "setContextTextBaselineIdeographic",
    14696: "setContextTextBaselineBottom",
    15084: "setContextTransformNormalize",
    15137: "contextMoveTo",
    15178: "contextLineTo",
    15215: "contextClosePath",
    15288: "contextArc",
    16282: "copyToKeyboard",
    16505: "setLocation",
    16959: "contextDrawImage",
    24403: "getImage",
    24601: "contextDrawCanvas",
    24706: "setContextLineCapButt",
    24745: "setContextLineCapRound",
    24785: "setContextLineCapSquare",
    25012: "contextStrokeRect",
    25057: "contextDrawFullCanvas",
    25399: "isContextPatternAvailable",
    25462: "createContextPattern",
    25748: "contextGetPixelColor",
    25862: "contextDrawCanvasSourceToPixel",
    25954: "contextFillRectWithPattern",
    26051: "wipePattern",
    26281: "empty", // ?
    26345: "empty", // ?
    26430: "existsQuery",
    26890: "empty", // anti cheat
    26958: "canvasHasSamePropertyAsDocumentBody",
    27065: "existsDocumentBodyProperty",
    27120: "existsDocumentBodyProperty2",
    27166: "existsDivPropertyAndEqualsPropertyOnDocumentBody",
    27326: "empty", // anti cheat
    27575: "empty", // anti cheat
    27656: "empty", // anti cheat
    27778: "acCheckWindow", // anti cheat
    27886: "getDocumentBody",
    27945: "empty", // anti cheat
    28057: "empty", // anti cheat
    28125: "getUserAgent",
    28186: "empty", // anti cheat
    28407: "getQuerySelectorToString",
    28509: "getFillTextToString",
    28632: "getStrokeRectToString",
    28757: "getStrokeTextToString",
    28882: "getScaleToString",
    29002: "getTranslateToString",
    29126: "getFillRectToString",
    29249: "getRotateToString",
    29370: "getGetImageDataToString",
    29518: "empty", // ads
    29633: "contextClearRect",
    29740: "createCanvasCtx",
    29980: "setContextMiterLimit",
    30178: "getWindowLocation",
    30225: "setLoadingStatus",
    30352: "m28nReply",
    30767: "isSSL",
    30836: "createWebSocket",
    31548: "findServerById",
    31923: "invalidPartyId",
    31954: "wipeLocation",
    32047: "getGamepadAxe",
    32156: "getGamepadButtonPressed",
    32290: "pollWebSocketEvent",
    32446: "updateToNewVersion",
    32506: "empty", // pow
    32813: "reloadWindow",
    32840: "getWindowLocationSearch",
    32889: "getWindowReferrer",
    33085: "empty", // fingerprinting
    33169: "empty", // fingerprinting
    33222: "empty", // fingerprinting
    33330: "empty", // fingerprinting
    33425: "empty", // fingerprinting
    33488: "empty", // fingerprinting
    33528: "empty", // fingerprinting
};

const WASM_IMPORTS = {
    "i": "assertFail",
    "q": "mapFile",
    "p": "sysMunmap",
    "b": "abort",
    "d": "asmConstsDII",
    "a": "asmConstsIII",
    "j": "exitLive",
    "m": "exitForce",
    "g": "getNow",
    "n": "memCopyBig",
    "e": "random",
    "f": "resizeHeap",
    "r": "setMainLoop",
    "k": "envGet",
    "l": "envSize",
    "h": "fdWrite",
    "c": "roundF",
    "o": "timeString",
    "memory": "wasmMemory",
    "table": "wasmTable"
};

const WASM_EXPORTS = {
    "s": "wasmCallCtors", // constructors
    "t": "connect", // used to connect to ip, unused
    "u": "hasTank", // does player have a tank
    "v": "setConvar", // sets console var
    "w": "getConvar", // gets console var
    "x": "execute", // execute console cmd
    "y": "printConsoleHelp", // prints console help
    "z": "main", // main function
    "A": "checkWS", // polls ws
    "B": "free", // frees memory
    "C": "videoAdsLoaded", // event for ads loaded, unused
    "D": "videoAdsDone", // event for ad done, unused
    "E": "mouse", // sets mouse pos
    "F": "keyDown", // sets key down
    "G": "keyUp", // sets key up
    "H": "resetKeys", // resets key listeners
    "I": "preventRightClick", // should prevent right click if true
    "J": "flushInputHooks", // flushes all inputs (keys, mouse, gamepad)
    "K": "mouseWheel", // sets mouse wheel delta
    "L": "cp5Idle", // idle mode, unused
    "M": "cp5Destroy", // destroy cp5 obj, unused
    "N": "powResult", // pow result, unused 
    "O": "restReply", // server finder reply 
    "P": "getErrorLocation", // gets pointer for setting error code
    "Q": "malloc", // allocates memory
    "R": "dynCallVI", // dynamic func call (v = void, i = integer as arg, d = double as arg, f = float as arg)
    "S": "dynCallV" // dynamic func call (v = void, i = integer as arg, d = double as arg, f = float as arg)
};

const MOD_CONFIG = {
    "wasmFunctions": {
        "loadGamemodeButtons": 296,
        "loadVectorDone": 22,
        "loadChangelog": 447,
        "loadTankDefinitions": 277,
        "getTankDefinition": 101,
        "findCommand": 496,
        "decodeComponentList": 221,
        "createEntityAtIndex": 114
    },
    "memory": {
        "gamemodeDisabledText": 16420,
        "gamemodeButtons": 113480,
        "changelog": 167328,
        "changelogLoaded": 168632,
        "tankDefinitions": 166572,
        "tankDefinitionsCount": 166576,
        "commandList": 53064,
        "netColorTable": 49584
    },
    "wasmFunctionHookOffset": {
        "gamemodeButtons": 33,
        "changelog": 28
    }
};

const ADDON_MAP = {
    "trapLauncher": 147,
    "auto3": 148,
    "smasher": 149,
    "pronounced": 150,
    "landmine": 151,
    "auto5": 153,
    "autoturret": 154, // Auto Trapper (154) & Auto Gunner (152)
    "autosmasher": 155,
    "spike": 156,
    "launcher": 157, // Skimmer (157) & Rocketeer (158)
    "dombase": 159,
    "dompronounced": 160 // Dom1 (160) & Dom2 (161) 
};

const CUSTOM_ADDONS = {
    // This is a tutorial addon made for showcasing how custom addon renders are to be defined.
    "tutorial": entity => {
        // This if statement isnt totally necessary but might help your IDE recognize the type of "entity" which simplifies development later. It can be removed.
        if(!(entity instanceof $Entity)) return;
        
        /*
        We are currently on the root level meaning we can only access the "entity" the addon is placed upon. This means that "entity" is either a barrel or a tank.
        It is possible to change its data even at the root level.

        Each instance of "$Entity" has the following properties:
        - positionData
            -> Stores data about the entitie's position and angle, if the entity has a parent the x and y coordinates will be relative to the parents position.
        - physicsData
            -> Stores data about the entitie's collision properties (its size, sides, width, if it is a trapezoid).
        - styleData
            -> Stores data about how the entity is rendered, whether or not its visible, its color and so on.
        - barrelData
            -> This field is only defined if the entity actually is a barrel. It only has the shootingAngle property.
        - parent
            -> If this $Entity instance has been created via a createChild call, then this property stores a reference to its creator / parent. (ONLY DIRECT PARENT!)
        - children
            -> If this entity created children via createChild calls, then this property stores references to those children. (ONLY DIRECT CHILDREN!)
        As well as the following methods:
        - clone(source: $Entity)
            -> Clones the sources position, physics, style and barrelData to the current entity.
        - createChild(isBarrel: boolean)
            -> Creates either a regular object entity or a barrel entity and sets its parent as this entity.
        - defaults()
            -> Sets the default values for position, physics, style and barrelData. Check out the default values in "./dma.js".
        
        Each of the "[...]Data" fields has similarly .clone(source: Self) and defaults() methods.
        
        You may not set the properties of $Entity manually. Instead use the "[...]Data" fields or .clone() / .defaults().
        So don't do this: "someEntity.positionData = someOtherEntity.positionData;", but instead do this: "someEntity.positionData.clone(someOtherEntity.positionData);".
        */
    },
    "auto2": entity => {
        if(!(entity instanceof $Entity)) return;
                
        const rotator = entity.createChild(false);
        rotator.defaults();
        rotator.physicsData.size = 5;
        rotator.positionData.angle = 0.01;
        rotator.positionData.isAngleAbsolute = true;
        rotator.styleData.isVisible = false;

        const count = 2;
        for(let i = 0; i < count; ++i) {
            const socket = rotator.createChild(false);
            socket.defaults();
            
            socket.positionData.angle = i * Math.PI * 2 / count;
            socket.positionData.x = Math.cos(socket.positionData.angle) * 40;
            socket.positionData.y = Math.sin(socket.positionData.angle) * 40;
            socket.physicsData.size = 25;
            // Color.Barrel
            socket.styleData.color = 1;

            const barrel = socket.createChild(true);
            barrel.defaults();
            barrel.physicsData.size = 55;
            barrel.physicsData.sides = 2;
            barrel.physicsData.width = 0.7 * 42;
            // angle + shootingAngle
            barrel.positionData.angle = 0;
            // Math.cos(angle) * (size / 2 + distance) - Math.sin(angle) * offset
            barrel.positionData.x = Math.cos(0) * (barrel.physicsData.size / 2 + 0) - Math.sin(0) * 0;
            // Math.sin(angle) * (size / 2 + distance) - Math.cos(angle) * offset
            barrel.positionData.y = Math.sin(0) * (barrel.physicsData.size / 2 + 0) - Math.cos(0) * 0;
            // Color.Barrel
            barrel.styleData.color = 1;
        }
    },
    "arrasspawnerbarrel": entity => {
        if(!(entity instanceof $Entity)) return;

        const rect1 = entity.createChild(false);
        rect1.defaults();
        rect1.styleData.color = 1;
        rect1.styleData.showsAboveParent = true;
        rect1.physicsData.sides = 2;
        rect1.physicsData.width = entity.physicsData.width * 1.25;
        rect1.physicsData.size = entity.physicsData.size * (10 / 50);
        rect1.positionData.x = (entity.physicsData.size - rect1.physicsData.size) / 2;

        const rect2 = entity.createChild(false);
        rect2.defaults();
        rect2.styleData.color = 1;
        rect2.styleData.showsAboveParent = true;
        rect2.physicsData.sides = 2;
        rect2.physicsData.width = entity.physicsData.width * 1.25;
        rect2.physicsData.size = entity.physicsData.size * (35 / 50);
        rect2.positionData.x = (-entity.physicsData.size + rect2.physicsData.size) / 2;
    }
}

const CUSTOM_COMMANDS = [
    {
        "id": "util_test",
        "description": "Test command to check if custom commands are working, prints 'Hello World' to the console",
        "callback": args => { // array of strings, you need to parse them yourself
            console.log("Hello World");
        }
    }, {
        "id": "util_set_dev_password",
        "usage": "[password]",
        "description": "Sets the dev password (reconnect required)",
        "callback": args => {
            if(!args[0]) return;
            window.localStorage.setItem("password", args[0]);
        }
    }, {
        "id": "util_change_scheduler",
        "usage": "[?scheduler]",
        "description": "Changes the game's frame scheduler (default: requestAnimationFrame)",
        "callback": args => {
            // possible alternative would be setTimeout eg.
            Module.scheduler = typeof window[args[0]] === "function" ? window[args[0]] : window.requestAnimationFrame;
        }
    }, {
        "id": "util_reload_servers",
        "usage": "[?interval]",
        "description": "Sets the interval in which gamemodes are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadServersInterval = -1;
                        case "connect":
                            return Module.reloadServersInterval = -2;
                    }
                }
                return Module.reloadServersInterval = num;
            }
            Game.reloadServers();
        }
    }, {
        "id": "util_reload_tanks",
        "usage": "[?interval]",
        "description": "Sets the interval in which tanks are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadTanksInterval = -1;
                        case "connect":
                            return Module.reloadTanksInterval = -2;
                    }
                }
                return Module.reloadTanksInterval = num;
            }
            Game.reloadTanks();
        }
    }, {
        "id": "util_reload_commands",
        "usage": "[?interval]",
        "description": "Sets the interval in which commands are reloaded automatically (milliseconds, 'never' or 'connect') or reloads once if no interval is given",
        "callback": args => {
            if(args[0]) {
                const num = parseInt(args[0]);
                if(isNaN(num)) {
                    switch(args[0]) {
                        case "never":
                            return Module.reloadCommandsInterval = -1;
                        case "connect":
                            return Module.reloadCommandsInterval = -2;
                    }
                }
                return Module.reloadCommandsInterval = num;
            }
            Game.reloadCommands();
        }
    }, {
        "id": "util_set_changelog",
        "usage": "[line 1\\n] [line 2] ...",
        "description": "Sets the changelog to the given text, remember to use \\n before and after each line",
        "callback": args => {
            Game.changeChangelog(args.join(' ').split("\\n"));
        }
    }
];

const COMMANDS_LOOKUP = {
    "con_toggle": 52952,
    "game_spawn": 52992,
    "help": 49956,
    "lb_reconnect": 50056,
    "net_replace_color": 50152,
    "net_replace_colors": 50192,
    "ui_replace_colors": 49916
};

const WASM_TABLE = {
    "initial": 687,
    "element": "anyfunc"
};

const INITIAL_MEMORY = 67108864;
const WASM_PAGE_SIZE = 65536; // A WebAssembly page has a constant size of 65,536 bytes, i.e., 64KiB

const DYNAMIC_BASE = 5426112; // start of dynmic memory
const DYNAMIC_TOP_PTR = 183072; // points to start of dynamic memory

const WASM_MEMORY = {
    "initial": INITIAL_MEMORY / WASM_PAGE_SIZE,
    "maximum": INITIAL_MEMORY / WASM_PAGE_SIZE
};

const FIELD_OFFSETS = {
    basic: {
        owner: 20,
        parent: 32
    },
    position: {
        y: 8,
        x: 40,
        angle: 72,
        flags: 104
    },
    collidable: {
        size: 16,
        sides: 48,
        width: 64,
        flags: 104
    },
    renderable: {
        color: 12,
        flags: 20,
        borderWidth: 32,
        opacity: 64
    },
    cannon: {
        shootingAngle: 8
    }
};

const FLAGS = {
    absoluteRotation: 1 << 0,
    isTrapezoid: 1 << 0,
    isVisible: 1 << 0,
    renderFirst: 1 << 3,
    isStar: 1 << 4,
    isCachable: 1 << 5,
    showsAboveParent: 1 << 6
};
`

~client/dma.js
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

class $Entity {
    #$;
    #$basic;
    #$position;
    #$collidable;
    #$renderable;
    #$cannon;
    constructor($, $parent = null) {
        this.#$ = $;
        this.parent = null;
        this.children = [];

        this.#$basic = $[0x10][0x20][0x18][0 * 4].$;
        this.#$position = $[0x10][0x20][0x18][10 * 4].$;
        this.#$collidable = $[0x10][0x20][0x18][3 * 4].$;
        this.#$renderable = $[0x10][0x20][0x18][11 * 4].$;
        this.#$cannon = $[0x10][0x20][0x18][2 * 4].$;
        
        if($parent) {
            if (!this.#$basic.at) throw "Invalid Basic component reference.";
            this.#$basic[FIELD_OFFSETS.basic.parent].u32 = $parent[52].u32;
            this.#$basic[FIELD_OFFSETS.basic.parent][4].u16 = $parent[56].u16;
            this.#$basic[FIELD_OFFSETS.basic.owner].u32 = $parent[52].u32;
            this.#$basic[FIELD_OFFSETS.basic.owner][4].u16 = $parent[56].u16;
        }
    }

    createChild(isCannon = false) {
        const ptr = $EntityManager.createNewEntity(this.#$[48].at);
        if (!ptr) throw "Out of entityIds";

        const componentList = $EntityManager.createComponentList({
            createBasic: true,
            createCollidable: true,
            createPosition: true,
            createRenderable: true,
            createCannon: isCannon
        });
        if (!componentList) throw "Invalid component list reference.";
        Module.rawExports.decodeComponentList(ptr, componentList);
        Module.exports.free($(componentList)[0].i32);
        Module.exports.free(componentList);

        const entity = new $Entity($(ptr), this.#$);
        entity.parent = this;
        this.children.push(entity);

        return entity;
    }

    get positionData() {
        return this.#$position ? new $Position(this.#$position) : null;
    }

    set positionData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.positionData.clone(yourFirstEntity.positionData); instead.";
    }

    get physicsData() {
        return this.#$collidable ? new $Collidable(this.#$collidable) : null;
    }

    set physicsData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.physicsData.clone(yourFirstEntity.physicsData); instead.";
    }

    get styleData() {
        return this.#$renderable ? new $Renderable(this.#$renderable) : null;
    }

    set styleData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.styleData.clone(yourFirstEntity.styleData); instead.";
    }

    get barrelData() {
        return this.#$cannon ? new $Cannon(this.#$collidable) : null;
    }

    set barrelData(component) {
        throw "Setting components by reference is not allowed!\nTry using yourSecondEntity.barrelData.clone(yourFirstEntity.barrelData); instead.";
    }

    clone(source) {
        if (!(source instanceof $Entity)) throw "Failed to clone data from an unknown object, please provide an instance of $Entity.";
        if(source.positionData) this.positionData?.clone(source.positionData);
        if(source.physicsData) this.physicsData?.clone(source.physicsData);
        if(source.styleData) this.styleData?.clone(source.styleData);
        if(source.barrelData) this.barrelData?.clone(source.barrelData);
    }

    defaults() {
        this.positionData?.defaults();
        this.physicsData?.defaults();
        this.styleData?.defaults();
        this.barrelData?.defaults();
    }
}

class $Component {
    #$;
    constructor($) {
        this.entity = new $Entity($.$);
        this.#$ = $;
    }
}

class $Position extends $Component {
    #$x;
    #$y;
    #$angle;
    #$flags;
    constructor($) {
        super($);
        this.#$x = $[FIELD_OFFSETS.position.x];
        this.#$y = $[FIELD_OFFSETS.position.y];
        this.#$angle = $[FIELD_OFFSETS.position.angle];
        this.#$flags = $[FIELD_OFFSETS.position.flags];
    }

    get x() {
        return this.#$x.f32;
    }

    set x(value) {
        this.#$x.f32 = value;
    }

    get y() {
        return this.#$y.f32;
    }

    set y(value) {
        this.#$y.f32 = value;
    }

    get angle() {
        return this.#$angle.f32;
    }

    set angle(value) {
        this.#$angle.f32 = value;
    }

    get isAngleAbsolute() {
        return Boolean(this.#$flags.u32 & FLAGS.absoluteRotation);
    }

    set isAngleAbsolute(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.absoluteRotation;
        else this.#$flags.u32 &= ~FLAGS.absoluteRotation;
    }

    clone(source) {
        if (!(source instanceof $Position)) throw "Failed to clone data from an unknown object, please provide an instance of $Position.";
        this.x = source.x;
        this.y = source.y;
        this.angle = source.angle;
        this.isAngleAbsolute = source.isAngleAbsolute;
    }

    defaults() {
        this.x = 0;
        this.y = 0;
        this.angle = 0;
        this.isAngleAbsolute = false;
    }
}

class $Collidable extends $Component {
    #$sides;
    #$size;
    #$width;
    #$flags;
    constructor($) {
        super($);
        this.#$sides = $[FIELD_OFFSETS.collidable.sides];
        this.#$size = $[FIELD_OFFSETS.collidable.size];
        this.#$width = $[FIELD_OFFSETS.collidable.width];
        this.#$flags = $[FIELD_OFFSETS.collidable.flags];
    }

    get sides() {
        return this.#$sides.u32;
    }

    set sides(value) {
        this.#$sides.u32 = value;
    }

    get size() {
        return this.#$size.f32;
    }

    set size(value) {
        this.#$size.f32 = value;
    }

    get width() {
        return this.#$width.f32;
    }

    set width(value) {
        this.#$width.f32 = value;
    }

    get isTrapezoid() {
        return Boolean(this.#$flags.u32 & FLAGS.isTrapezoid);
    }

    set isTrapezoid(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isTrapezoid;
        else this.#$flags.u32 &= ~FLAGS.isTrapezoid;
    }

    clone(source) {
        if (!(source instanceof $Collidable)) throw "Failed to clone data from an unknown object, please provide an instance of $Collidable.";
        this.sides = source.sides;
        this.size = source.size;
        this.width = source.width;
        this.isTrapezoid = source.isTrapezoid;
    }

    defaults() {
        this.sides = 1;
        this.size = 50.0;
        this.width = 0;
        this.isTrapezoid = false;
    }
}

class $Renderable extends $Component {
    #$color;
    #$borderWidth;
    #$opacity;
    #$flags;
    constructor($) {
        super($);
        this.#$color = $[FIELD_OFFSETS.renderable.color];
        this.#$borderWidth = $[FIELD_OFFSETS.renderable.borderWidth];
        this.#$opacity = $[FIELD_OFFSETS.renderable.opacity];
        this.#$flags = $[FIELD_OFFSETS.renderable.flags];
    }

    get color() {
        return this.#$color.u32;
    }

    set color(value) {
        this.#$color.u32 = value;
    }

    get borderWidth() {
        return this.#$borderWidth.f32;
    }

    set borderWidth(value) {
        this.#$borderWidth.f32 = value;
    }

    get opacity() {
        return this.#$opacity.f32;
    }

    set opacity(value) {
        this.#$opacity.f32 = value;
    }

    get isVisible() {
        return Boolean(this.#$flags.u32 & FLAGS.isVisible);
    }

    set isVisible(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isVisible;
        else this.#$flags.u32 &= ~FLAGS.isVisible;
    }

    get renderFirst() {
        return Boolean(this.#$flags.u32 & FLAGS.renderFirst);
    }

    set renderFirst(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.renderFirst;
        else this.#$flags.u32 &= ~FLAGS.renderFirst;
    }

    get isStar() {
        return Boolean(this.#$flags.u32 & FLAGS.isStar);
    }

    set isStar(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isStar;
        else this.#$flags.u32 &= ~FLAGS.isStar;
    }

    get isCachable() {
        return Boolean(this.#$flags.u32 & FLAGS.isCachable);
    }

    set isCachable(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.isCachable;
        else this.#$flags.u32 &= ~FLAGS.isCachable;
    }

    get showsAboveParent() {
        return Boolean(this.#$flags.u32 & FLAGS.showsAboveParent);
    }

    set showsAboveParent(value) {
        value = Boolean(value);
        if (value === true) this.#$flags.u32 |= FLAGS.showsAboveParent;
        else this.#$flags.u32 &= ~FLAGS.showsAboveParent;
    }

    clone(source) {
        if (!(source instanceof $Renderable)) throw "Failed to clone data from an unknown object, please provide an instance of $Renderable.";
        this.color = source.color;
        this.borderWidth = source.borderWidth;
        this.opacity = source.borderWidth;
        this.isVisible = source.isVisible;
        this.renderFirst = source.renderFirst;
        this.isStar = source.isStar;
        this.isCachable = source.isCachable;
        this.showsAboveParent = source.showsAboveParent;
    }

    defaults() {
        this.color = 0;
        this.borderWidth = 7.5;
        this.opacity = 1;
        this.isVisible = true;
        this.renderFirst = false;
        this.isStar = false;
        this.isCachable = true;
        this.showsAboveParent = false;
    }
}

class $Cannon extends $Component {
    #$shootingAngle;
    constructor($) {
        super($);
        this.#$shootingAngle = $[FIELD_OFFSETS.cannon.shootingAngle];
    }

    get shootingAngle() {
        return this.#$shootingAngle.f32;
    }

    set shootingAngle(value) {
        this.#$shootingAngle.f32 = value;
    }

    clone(source) {
        if (!(source instanceof $Cannon)) throw "Failed to clone data from an unknown object, please provide an instance of $Cannon.";
        this.shootingAngle = source.shootingAngle;
    }

    defaults() {
        this.shootingAngle = 0;
    }
}

class $LinkedList {
    constructor(ptr, type, typeSize) {
        this.ptr = ptr;
        this.type = type;
        this.typeSize = typeSize;
    }

    push(...entries) {
        for (const entry of entries) this._append(this._end, entry);
        this.length += entries.length;
    }

    forEach(cb) {
        let current = $(this.ptr);
        while (current.i32) {
            cb(current.i32);
            current = current.$;
        }
    }

    get length() {
        return $(this.ptr)[4].i32;
    }

    set length(val) {
        $.write(this.ptr + 4, "u32", val);
    }

    get _end() {
        let current = $(this.ptr);
        while (current.i32) current = current.$;
        return current.at;
    }

    _append(last, entry) {
        const ptr = Module.exports.malloc(this.typeSize);
        $.write(ptr, this.type, entry);
        $.write(last, "$", ptr);
    }
}

class $Vector {
    constructor(ptr, type, typeSize) {
        this.ptr = ptr;
        this.type = type;
        this.typeSize = typeSize;
        this.start = $(ptr).i32;
        this.end = $(ptr)[4].i32;
        this.maxCapacity = $(ptr)[8].i32;
        this.maxEntries = (this.maxCapacity - this.start) / this.typeSize;
        this.totalSize = (this.end - this.start) / this.typeSize;
        this.entries = [];
    }

    push(...entries) {
        this.entries.push(...entries);
        this.totalSize = this.entries.length * this.typeSize;
        if (this.entries.length > this.maxEntries) this._malloc();
        else this._write();
    }

    clear() {
        this.entries = [];
        this.end = this.start;
        Module.HEAPU8.subarray(this.start, this.maxCapacity).fill(0);
    }

    destroy() {
        Module.exports.free(this.start);
        this.start = 0;
        this.end = 0;
        this.maxCapacity = 0;
        $.write(this.ptr, "$", this.start);
        $.write(this.ptr + 4, "$", this.end);
        $.write(this.ptr + 8, "$", this.maxCapacity);
        this.maxEntries = 0;
        this.totalSize = 0;
        this.entries = [];
    }

    _write() {
        for (const entry of this.entries) {
            $.write(this.end, this.type, entry);
            this.end += this.typeSize;
        }
        $.write(this.ptr + 4, "$", this.end);
    }

    _malloc() {
        this.maxEntries = (this.maxCapacity - this.start) / this.typeSize;
        this.totalSize = this.entries.length * this.typeSize;
        this.start = Module.exports.malloc(this.totalSize);
        this.end = this.start;
        this.maxCapacity = this.start + this.totalSize;
        Module.HEAPU8.subarray(this.start, this.maxCapacity).fill(0);
        $.write(this.ptr, "$", this.start);
        this._write();
        $.write(this.ptr + 8, "$", this.maxCapacity);
    }
}

class $EntityManager {
    static createNewEntity(simulation) {
        for (let i = 0; i < 16385; ++i) {
            if (!$(simulation).$[i][720].u8) {
                return Module.rawExports.createEntityAtIndex($(simulation).i32 + 12, i);
            }
        }
        return 0;
    }

    static getComponent(entity, name) {
        // These offsets don't change over versions (unless the components are reordered)
        switch (name) {
            case "Basic":
                return $(entity)[0x10][0x20][0x18][0 * 4].i32;
            case "CPUControllable":
                return $(entity)[0x10][0x20][0x18][1 * 4].i32;
            case "Cannon":
                return $(entity)[0x10][0x20][0x18][2 * 4].i32;
            case "Collidable":
                return $(entity)[0x10][0x20][0x18][3 * 4].i32;
            case "Destroyable":
                return $(entity)[0x10][0x20][0x18][4 * 4].i32;
            case "Dominator":
                return $(entity)[0x10][0x20][0x18][5 * 4].i32;
            case "ExampleComponent":
                return $(entity)[0x10][0x20][0x18][6 * 4].i32;
            case "MapInfo":
                return $(entity)[0x10][0x20][0x18][7 * 4].i32;
            case "Namable":
                return $(entity)[0x10][0x20][0x18][8 * 4].i32;
            case "PlayerInfo":
                return $(entity)[0x10][0x20][0x18][9 * 4].i32;
            case "Position":
                return $(entity)[0x10][0x20][0x18][10 * 4].i32;
            case "Renderable":
                return $(entity)[0x10][0x20][0x18][11 * 4].i32;
            case "Tagged":
                return $(entity)[0x10][0x20][0x18][12 * 4].i32;
            case "Tank":
                return $(entity)[0x10][0x20][0x18][13 * 4].i32;
            case "TeamInfo":
                return $(entity)[0x10][0x20][0x18][14 * 4].i32;
            default:
                return 0;
        }
    }


    /**
     * You need to free the returned ptr and ptr.$
     */
    static createComponentList({
        createBasic,
        createCPUControllable,
        createCannon,
        createCollidable,
        createDestroyable,
        createDominator,
        createExampleComponent,
        createMapInfo,
        createNamable,
        createPlayerInfo,
        createPosition,
        createRenderable,
        createTagged,
        createTank,
        createTeamInfo
    }) {
        let at = -1;
        const data = [];
        if (createBasic) {
            data.push((0 - at) ^ 1);
            at = 0;
        }
        if (createCPUControllable) {
            data.push((1 - at) ^ 1);
            at = 1;
        }
        if (createCannon) {
            data.push((2 - at) ^ 1);
            at = 2;
        }
        if (createCollidable) {
            data.push((3 - at) ^ 1);
            at = 3;
        }
        if (createDestroyable) {
            data.push((4 - at) ^ 1);
            at = 4;
        }
        if (createDominator) {
            data.push((5 - at) ^ 1);
            at = 5;
        }
        if (createExampleComponent) {
            data.push((6 - at) ^ 1);
            at = 6;
        }
        if (createMapInfo) {
            data.push((7 - at) ^ 1);
            at = 7;
        }
        if (createNamable) {
            data.push((8 - at) ^ 1);
            at = 8;
        }
        if (createPlayerInfo) {
            data.push((9 - at) ^ 1);
            at = 9;
        }
        if (createPosition) {
            data.push((10 - at) ^ 1);
            at = 10;
        }
        if (createRenderable) {
            data.push((11 - at) ^ 1);
            at = 11;
        }
        if (createTank) {
            data.push((12 - at) ^ 1);
            at = 12;
        }
        if (createTagged) {
            data.push((13 - at) ^ 1);
            at = 13;
        }
        if (createTeamInfo) {
            data.push((14 - at) ^ 1);
            at = 14;
        }
        data.push(1);
        const binview = Module.exports.malloc(12);
        $(binview)[0].i32 = Module.exports.malloc(data.length);
        Module.HEAPU8.set(data, $(binview)[0].i32);
        $(binview)[4].i32 = data.length;
        $(binview)[8].i32 = 0;
        return binview;
    }
}

const Decoder = new TextDecoder();
const Encoder = new TextEncoder();

const getter = ({ ptr }, prop) => {
    switch(prop) {
        case "at":
        case "ptr": return ptr;
        case "u8": return Module.HEAPU8[ptr];
        case "i8": return Module.HEAP8[ptr];
        case "u16": return Module.HEAPU16[ptr >> 1];
        case "i16": return Module.HEAP16[ptr >> 1];
        case "u32": return Module.HEAPU32[ptr >> 2];
        case "i32": return Module.HEAP32[ptr >> 2];
        case "u64": return Module.HEAPU64[ptr >> 3];
        case "f32": return Module.HEAPF32[ptr >> 2];
        case "f64": return Module.HEAPF64[ptr >> 3];
        case "i64": return Module.HEAP64[ptr >> 3];
        case "u64": return Module.HEAPU64[ptr >> 3];
        case "utf8": return Decoder.decode(Module.HEAPU8.subarray(ptr, Module.HEAPU8.indexOf(0, ptr)));
        case "cstr": {
            let strAt = ptr;
            let length = Module.HEAPU8[ptr + 11];
            if (length === 0x80) {
                length = Module.HEAP32[(ptr + 4) >> 2];
                strAt = Module.HEAP32[ptr >> 2];
            }
            return Decoder.decode(Module.HEAPU8.subarray(strAt, strAt + length));
        }
        case "vector": return (type, size, args = null) => {
            const vector = [];
            for(let i = Module.HEAPU32[ptr >> 2]; i < Module.HEAPU32[(ptr + 4) >> 2]; i += size) vector.push(args ? $(i)[type](args) : $(i)[type])
            return vector;
        }
        case "struct": return fields => Object.fromEntries(fields.map(({name, offset, type, args}) => [name, args ? $(ptr + offset)[type](...args) : $(ptr + offset)[type]]));
        case "sll": return nodeOffset => { // singly linked list
            let node = $(ptr);
            const nodes = [];
            while(node[nodeOffset].u32 !== 0) nodes.push((node = node[nodeOffset].$));
            return nodes;
        }
        case "$cb": return cb => cb($(ptr));
        case "$": return $(Module.HEAPU32[ptr >> 2]);
    }
    const offset = parseInt(prop, 10);
    if(!Number.isNaN(offset)) return $(ptr + offset);
}

const setter = ({ ptr }, prop, to) => {
    if (to === undefined || to === null) return true;
    switch(prop) {
        case "u8":
            Module.HEAPU8[ptr] = to;
            return true;
        case "i8": 
            Module.HEAP8[ptr] = to;
            return true;
        case "u16":
            Module.HEAPU16[ptr >> 1] = to;
            return true;
        case "i16":
            Module.HEAP16[ptr >> 1] = to;
            return true;
        case "u32": 
            Module.HEAPU32[ptr >> 2] = to;
            return true;
        case "$": 
        case "i32":
            Module.HEAP32[ptr >> 2] = to;
            return true;
        case "u64": 
            Module.HEAPU64[ptr >> 3] = to;
            return true;
        case "f32": 
            Module.HEAPF32[ptr >> 2] = to;
            return true;
        case "f64":
            Module.HEAPF64[ptr >> 3] = to;
            return true;
        case "i64": 
            Module.HEAP64[ptr >> 3] = to;
            return true;
        case "u64": 
            Module.HEAPU64[ptr >> 3] = to;
            return true;
        case "utf8": {
            const buf = Encoder.encode(to.toString());
            Module.HEAPU8.set(buf, ptr);
            Module.HEAPU8[ptr + buf.byteLength] = 0;
            return true;
        }
        case "cstr": {
            const textBuf = Encoder.encode(to.toString());
            const len = textBuf.length;
            if(Module.HEAPU8[(ptr + 11) >> 0] === 0x80 && Module.HEAP32[ptr >> 2] < Module.HEAPU8 && Module.HEAP32[ptr >> 2] > DYNAMIC_TOP_PTR) Module.exports.free(Module.HEAP32[ptr >> 2]);
            Module.HEAPU8.set(new Uint8Array(12), ptr);
            if(len < 11) {
                Module.HEAPU8.set(textBuf, ptr);
                Module.HEAPU8[ptr + 11] = len;
                return true;
            }
            const strPtr = Module.exports.malloc(len + 1);
            Module.HEAPU8.set(textBuf, strPtr);
            Module.HEAPU8[strPtr + len] = 0;
            Module.HEAP32.set([strPtr, len, -0x7FFFFFE0], ptr >> 2);
            return true;
        }
        default:
            console.warn("Invalid property " + prop);
            return true;
    }
}

window.$ = ptr => new Proxy({ ptr }, { get: getter, set: setter });

$.write = (ptr, type, value) => {
    switch (type) {
        case "struct":
            return $.writeStruct(ptr, value);
        case "vector":
            return new $Vector(ptr, value.type, value.typeSize).push(...value.entries);
        case "cstr":
            Module.HEAP32.set([0, 0, 0], )
        default:
            return $(ptr)[type] = value;
    }
}

$.writeStruct = (ptr, fields) => {
    for (const {
            offset,
            type,
            value
        }
        of fields)
        $.write(ptr + offset, type, value);
}
`

~client/index.html
`
<!--
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
-->

<!DOCTYPE html>

<html>
    <head>
        <link rel="icon" href="https://raw.githubusercontent.com/ABCxFF/diepcustom/main/icon.png"/>
        <title>diep.io custom-server</title>
        
        <style>
          body {
            background-color: #000000;
          }

          #canvas {
            border: 0;
            margin: 0;
            padding: 0;
            overflow: hidden;
          }
    
          #loading {
            color: #FFFFFF;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 48pt;
            font-family: sans-serif;
            font-weight: bold;
            cursor: default;
          }
    
          #canvas {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            width: 100%;
            height: 100%;
            cursor: default;
          }
    
          #textInputContainer {
            display: none;
            position: absolute;
          }
    
          #textInput {
            background-color: transparent;
            font-family: "Ubuntu";
            padding: 0;
            border: 0;
            outline: none;
          }
        </style>

        <link href="https://fonts.googleapis.com/css?family=Ubuntu:700" rel="stylesheet" type="text/css" />
    </head>

    <body>
        <span id="loading">Loading...</span>
        <canvas id="canvas" width="800" height="800"></canvas>
        <div style="font-family:'Ubuntu'">&nbsp;</div>
        <div id="textInputContainer"><input id="textInput"></div>
        
        <!-- Third party library for dynmically modifying wasm files on the fly-->
        <script src ="https://cdn.jsdelivr.net/gh/Qwokka/wail.min.js@5e32d36bd7a5e0830d1ff4b64d3587aea13f77da/wail.min.js" crossorigin="anonymous"></script>

        <script src="./config.js"></script>
        <script src="./input.js"></script>
        <script src="./dma.js"></script>
        <script src="./loader.js"></script>
    </body>
</html>
`

~client/input.js:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

window.setupInput = () => {
    window.input = {
        mouse: Module.exports.mouse,
        keyDown: Module.exports.keyDown,
        keyUp: Module.exports.keyUp,
        blur: Module.exports.resetKeys,
        wheel: Module.exports.mouseWheel,
        prevent_right_click: Module.exports.preventRightClick,
        flushInputHooks: Module.exports.flushInputHooks,
        print_convar_help: Module.exports.printConsoleHelp,
        should_prevent_unload: Module.exports.hasTank,
        get_convar: key => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const res = Module.exports.getConvar(keyPtr);
            Module.exports.free(keyPtr);
            return res ? Module.UTF8ToString(res) : null;
        },
        set_convar: (key, val) => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const valPtr = Module.allocateUTF8(val.toString());
            const res = Boolean(Module.exports.setConvar(keyPtr, valPtr));
            Module.exports.free(keyPtr);
            Module.exports.free(valPtr);
            return res;
        },
        execute: cmd => {
            const cmdPtr = Module.allocateUTF8(cmd.toString());
            Module.exports.execute(cmdPtr);
            Module.exports.free(cmdPtr);
        }
    };

    const onMouseWheel = e => window.input.wheel(e.wheelDelta / -120);

    /firefox/i.test(navigator.userAgent) ? document.addEventListener("DOMMouseScroll", onMouseWheel) : document.body.onmousewheel = onMouseWheel;

    let isTyping = false;

    const scale = window.localStorage.getItem("no_retina") ? 1 : window.devicePixelRatio;
    const canvas = document.getElementById("canvas");
    const loading = document.getElementById('loading');

    canvas.onmousemove = e => window.input.mouse(e.clientX * scale, e.clientY * scale);
    
    canvas.onmousedown = e => {
        window.input.flushInputHooks();
        window.input.keyDown(e.button + 1);
    }

    canvas.onmouseup = e => {
        window.input.flushInputHooks();
        window.input.keyUp(e.button + 1);
    }

    window.onkeydown = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyDown(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    window.onkeyup = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyUp(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    canvas.onclick = window.onclick = () => window.input.flushInputHooks();

    canvas.ondragstart = e => e.preventDefault();

    canvas.oncontextmenu = e => window.input.prevent_right_click() ? e.preventDefault() : null;
    
    window.setLoadingStatus = str => loading.innerText = str;

    window.setTyping = val => isTyping = val;

    window.unscale = val => val / scale;

    window.onresize = () => {
        canvas.width = window.innerWidth * scale;
        canvas.height = window.innerHeight * scale;
    }

    window.onresize()
}
`

~client/input.js:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

window.setupInput = () => {
    window.input = {
        mouse: Module.exports.mouse,
        keyDown: Module.exports.keyDown,
        keyUp: Module.exports.keyUp,
        blur: Module.exports.resetKeys,
        wheel: Module.exports.mouseWheel,
        prevent_right_click: Module.exports.preventRightClick,
        flushInputHooks: Module.exports.flushInputHooks,
        print_convar_help: Module.exports.printConsoleHelp,
        should_prevent_unload: Module.exports.hasTank,
        get_convar: key => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const res = Module.exports.getConvar(keyPtr);
            Module.exports.free(keyPtr);
            return res ? Module.UTF8ToString(res) : null;
        },
        set_convar: (key, val) => {
            const keyPtr = Module.allocateUTF8(key.toString());
            const valPtr = Module.allocateUTF8(val.toString());
            const res = Boolean(Module.exports.setConvar(keyPtr, valPtr));
            Module.exports.free(keyPtr);
            Module.exports.free(valPtr);
            return res;
        },
        execute: cmd => {
            const cmdPtr = Module.allocateUTF8(cmd.toString());
            Module.exports.execute(cmdPtr);
            Module.exports.free(cmdPtr);
        }
    };

    const onMouseWheel = e => window.input.wheel(e.wheelDelta / -120);

    /firefox/i.test(navigator.userAgent) ? document.addEventListener("DOMMouseScroll", onMouseWheel) : document.body.onmousewheel = onMouseWheel;

    let isTyping = false;

    const scale = window.localStorage.getItem("no_retina") ? 1 : window.devicePixelRatio;
    const canvas = document.getElementById("canvas");
    const loading = document.getElementById('loading');

    canvas.onmousemove = e => window.input.mouse(e.clientX * scale, e.clientY * scale);
    
    canvas.onmousedown = e => {
        window.input.flushInputHooks();
        window.input.keyDown(e.button + 1);
    }

    canvas.onmouseup = e => {
        window.input.flushInputHooks();
        window.input.keyUp(e.button + 1);
    }

    window.onkeydown = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyDown(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    window.onkeyup = e => {
        window.input.flushInputHooks();
        if(e.keyCode >= 112 && e.keyCode <= 130 && e.keyCode !== 113) return;
        window.input.keyUp(e.keyCode);
        if(e.keyCode === 9 || !isTyping && e.ctrlKey && e.metaKey) e.preventDefault();
    }

    canvas.onclick = window.onclick = () => window.input.flushInputHooks();

    canvas.ondragstart = e => e.preventDefault();

    canvas.oncontextmenu = e => window.input.prevent_right_click() ? e.preventDefault() : null;
    
    window.setLoadingStatus = str => loading.innerText = str;

    window.setTyping = val => isTyping = val;

    window.unscale = val => val / scale;

    window.onresize = () => {
        canvas.width = window.innerWidth * scale;
        canvas.height = window.innerHeight * scale;
    }

    window.onresize()
}
`

~client/loader.js:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

// make module globally accessable
window.Module = {};

// todolist
Module.todo = [];

// todo status
Module.status = null;

// is the todo list done?
Module.isRunning = false;

// has the module been aborted?
Module.isAborted = false;

// exception name
Module.exception = null;

// function index for dynamic calling of the main func
Module.mainFunc = null;

// content contexts
Module.cp5 = null;

// client input
window.input = null;

// arenas
Module.servers = null;

// colors
Module.colors = null;

// tanks
Module.tankDefinitions = null;
Module.tankDefinitionsTable = null;

// commands
Module.executeCommandFunctionIndex = null;
Module.executionCallbackMap = {};
Module.commandDefinitions = null;

// name input
Module.textInput = document.getElementById("textInput");
Module.textInputContainer = document.getElementById("textInputContainer");

// permission level is sent to client in the accept packet
Module.permissionLevel = -1;

// (polling) intervals, can be a number (ms), -1 aka never or -2 aka whenever a new connection is initiated  
Module.reloadServersInterval = 60000;
Module.reloadTanksInterval = -2;
Module.reloadCommandsInterval = -2;
Module.reloadColorsInterval = -2;

// Run frames via requestAnimationFrame or setTimeout
Module.scheduler = window.requestAnimationFrame;

// abort client
Module.abort = cause => {
    Module.isAborted = true;
    Module.isRunning = false;
    throw new WebAssembly.RuntimeError(`abort(${cause})`);
};

// run ASMConst method, basically replaces a lot of "real wasm imports"
Module.runASMConst = (code, sigPtr, argbuf) => {
    const args = [];
    let char;
    argbuf >>= 2;
    while(char = Module.HEAPU8[sigPtr++]) {
        const double = char < 105;
        if(double && argbuf & 1) argbuf++;
        args.push(double ? Module.HEAPF64[argbuf++ >> 1] : Module.HEAP32[argbuf])
        ++argbuf;
    }
    return ASMConsts[ASM_CONSTS[code]].apply(null, args);
};

// initializing the looper
Module.setLoop = func => {
    if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
    Module.mainFunc = func;
    Module.scheduler.apply(null, [Module.loop]);
};

// process todo
Module.run = async () => {
    let args = [];
    while(Module.todo.length) {
        const [func, isAsync] = Module.todo.shift();
        if(isAsync) args = await Promise.all(func(...args));
        else args = func(...args);
        console.log(`Running stage ${Module.status} done`);
    }
};

// looper, 1 animation frame = 1 main call, except for stack unwinds
Module.loop = () => {
    if(!Module.isRunning || Module.isAborted || Module.exception === "quit") return;
    switch(Module.exception) {
        case null:
            Module.exports.dynCallV(Module.mainFunc);
            return Module.scheduler.apply(null, [Module.loop]);
        case "quit":
            return;
        case "unwind":
            Module.exception = null;
            return Module.scheduler.apply(null, [Module.loop]);
    }
};

// exit runtime (no unwind, originally unwind would be catched here)
Module.exit = status => {
    Module.exception = "quit";
    Module.isRunning = false;
    throw `Stopped runtime with status ${status}`;
};

// read utf8 from memory
Module.UTF8ToString = ptr => ptr ? Decoder.decode(Module.HEAPU8.subarray(ptr, Module.HEAPU8.indexOf(0, ptr))) : "";

// i/o write used for console, not fully understood
Module.fdWrite = (stream, ptr, count, res) => {
    let out = 0;
    for(let i = 0; i < count; i++) out += Module.HEAP32[(ptr + (i * 8 + 4)) >> 2];
    Module.HEAP32[res >> 2] = out;
};

// write utf8 to memory
Module.allocateUTF8 = str => {
    if(!str) return 0;
    const encoded = Encoder.encode(str);
    const ptr = Module.exports.malloc(encoded.byteLength + 1); // stringNT aka *char[]
    if(!ptr) return;
    Module.HEAPU8.set(encoded, ptr);
    Module.HEAPU8[ptr + encoded.byteLength] = 0;
    return ptr;
};

// Refreshes UI Components
Module.loadGamemodeButtons = () => {
    const vec = new $Vector(MOD_CONFIG.memory.gamemodeButtons, "struct", 28);
    if(vec.start) vec.destroy(); // remove old arenas
    // map server response to memory struct
    vec.push(...Module.servers.map(server => ([
        { offset: 0, type: "cstr", value: server.gamemode }, 
        { offset: 12, type: "cstr", value: server.name }, 
        { offset: 24, type: "i32", value: 0 }
    ])));
    Module.rawExports.loadVectorDone(MOD_CONFIG.memory.gamemodeButtons + 12); // toggle vector memory guard
};

// Refreshes UI Components
Module.loadChangelog = (changelog) => {
    const vec = new $Vector(MOD_CONFIG.memory.changelog, "cstr", 12);
    if(vec.start) vec.destroy(); // remove old changelog
    vec.push(...(changelog || CHANGELOG)); // either load custom or default
    $(MOD_CONFIG.memory.changelogLoaded).i8 = 1; // not understood
};

// Replaces current colors with serverside ones
Module.loadColors = () => {
    if(!window.input || !Module.colors) return;
    for(const [idx, color] of Object.entries(Module.colors)) {
        window.input.execute(`net_replace_color ${idx} ${color}`);
    }
};

// Ignore Hashtable, instead read from custom table
Module.getTankDefinition = tankId => {
    if(!Module.tankDefinitions) return 0;
    if(!Module.tankDefinitionsTable) Module.loadTankDefinitions(); // load tankdefs dynmically when requested
    if(!Module.tankDefinitionsTable[tankId]) return 0;
    return Module.tankDefinitionsTable[tankId] + 12; // 12 bytes for tankIds
};

Module.getCommand = cmdIdPtr => COMMANDS_LOOKUP[$(cmdIdPtr).cstr] || 0;

Module.loadTankDefinitions = () => {
    const writeTankDef = (ptr, tank) => {
        // Please note that this is not the full tank/barrel struct but just the portion needed for the client to function properly
        const barrels = tank.barrels ? tank.barrels.map(barrel => { // barrel fields
            return [
                { offset: 0, type: "f32", value: barrel.angle },
                { offset: 4, type: "f32", value: barrel.delay },
                { offset: 8, type: "f32", value: barrel.size },
                { offset: 12, type: "f32", value: barrel.offset },
                { offset: 16, type: "u8", value: Number(barrel.isTrapezoid) },
                { offset: 24, type: "f32", value: barrel.width / 42 },
                { offset: 56, type: "f32", value: barrel.bullet.sizeRatio },
                { offset: 60, type: "f32", value: barrel.trapezoidDirection },
                { offset: 64, type: "f32", value: barrel.reload },
                { offset: 96, type: "u32", value: ADDON_MAP[barrel.addon] || 0 }
            ];
        }) : [];

        const fields = [ // tankdef fields
            { offset: 4, type: "u32", value: tank.id },
            { offset: 8, type: "u32", value: tank.id },
            { offset: 12, type: "u32", value: tank.id },
            { offset: 16, type: "cstr", value: tank.name.toString() || "" },
            { offset: 28, type: "cstr", value: tank.upgradeMessage.toString() || "" },
            { offset: 40, type: "vector", value: { type: "u32", typeSize: 4, entries: tank.upgrades || [] } },
            { offset: 52, type: "vector", value: { type: "struct", typeSize: 100, entries: barrels } },
            { offset: 64, type: "u32", value: tank.levelRequirement || 0 },
            { offset: 76, type: "u8", value: Number(tank.sides === 4) },
            { offset: 93, type: "u8", value: Number(tank.sides === 16) },
            { offset: 96, type: "u32", value: ADDON_MAP[tank.preAddon] || 0 },
            { offset: 100, type: "u32", value: ADDON_MAP[tank.postAddon] || 0 },
        ];

        $.writeStruct(ptr, fields);
    };

    // TODO Rewrite with new $LinkedList datastructure
    Module.tankDefinitionsTable = new Array(Module.tankDefinitions.length).fill(0); // clear memory
    let lastPtr = MOD_CONFIG.memory.tankDefinitions;
    for(const tank of Module.tankDefinitions) {
        if(!tank) continue;
        const ptr = Module.exports.malloc(244); // length of a tankdef
        Module.HEAPU8.subarray(ptr, ptr + 244).fill(0);
        $(lastPtr).i32 = ptr;
        writeTankDef(ptr, tank);
        Module.tankDefinitionsTable[tank.id] = ptr;
        lastPtr = ptr;
    }

    $(MOD_CONFIG.memory.tankDefinitionsCount).i32 = Module.tankDefinitions.filter(e => Boolean(e)).length; // tankId xor based off this
};

// Executes a command callback from a command context
Module.executeCommand = execCtx => {
    const cmd = $(execCtx)[0].cstr;
    const tokens = $(execCtx)[12].vector("cstr", 12);
    
    if(!cmd || !tokens.length) throw `Invalid execution context (ptr: ${execCtx}) received`;
    if(typeof Module.executionCallbackMap[tokens[0]] !== "function") {
        if(!Module.commandDefinitions.find(({ id }) => id === tokens[0])) {
            throw `${Module.executionCallbackMap[tokens]} for command ${cmd} is an invalid callback`;
        }
        return Game.socket.send(new Uint8Array([
            6,
            ...Encoder.encode(tokens[0]), 0,
            tokens.slice(1).length,
            ...tokens.slice(1).flatMap(token => [...Encoder.encode(token), 0])
        ]));
    }

    // [id, ...args], we only need args
    Module.executionCallbackMap[tokens[0]](tokens.slice(1));
};

/*
    Command object: { id, usage, description, callback }
    The execute command function will not check for validity of arguments, you need to do that on your own
*/
Module.loadCommands = (commands = CUSTOM_COMMANDS) => {
    const cmdList = new $LinkedList(MOD_CONFIG.memory.commandList, "struct", 24);
    for(let { id, usage, description, callback, permissionLevel } of commands) {
        if(COMMANDS_LOOKUP[id] || permissionLevel > Module.permissionLevel) continue; // ignore duplicates

        // allocate Command
        const cmdPtr = Module.exports.malloc(40);
        $.writeStruct(cmdPtr, [
            { offset: 0, type: "cstr", value: id },
            { offset: 12, type: "cstr", value: usage || "" },
            { offset: 24, type: "cstr", value: description || "" },
            { offset: 36, type: "u32", value: Module.executeCommandFunctionIndex } // we handle every custom command with the same function
        ]);

        COMMANDS_LOOKUP[id] = cmdPtr;
        if(callback) Module.executionCallbackMap[id] = callback;

        // allocate HashNode
        cmdList.push([
            { offset: 0, type: "u32", value: 0 }, // next node
            { offset: 4, type: "u32", value: 0 }, // hash
            { offset: 8, type: "cstr", value: id }, // command id
            { offset: 20, type: "$", value: cmdPtr } // command def ptr
        ]);
    }
};

const wasmImports = {
    assertFail: (condition, filename, line, func) => Module.abort("Assertion failed: " + Module.UTF8ToString(condition) + ", at: " + [filename ? Module.UTF8ToString(filename) : "unknown filename", line, func ? Module.UTF8ToString(func) : "unknown function"]),
    mapFile: () => -1, // unused
    sysMunmap: (addr, len) => addr === -1 || !len ? -28 : 0, // not really used
    abort: Module.abort,
    asmConstsDII: Module.runASMConst,
    asmConstsIII: Module.runASMConst,
    exitLive: () => Module.exception = "unwind", // unwind stack
    exitForce: () => Module.exit(1), // exit / quit
    getNow: () => performance.now(),
    memCopyBig: (dest, src, num) => { Module.HEAPU8.copyWithin(dest, src, src + num) }, // for large packets
    random: () => Math.random(),
    resizeHeap: () => Module.abort("OOM"), // unable to resize wasm memory
    setMainLoop: Module.setLoop,
    envGet: () => 0, // unused
    envSize: () => 0, // unused
    fdWrite: Module.fdWrite, // used for diep client console
    roundF: d => d >= 0 ? Math.floor(d + 0.5) : Math.ceil(d - 0.5), // no, default Math.round doesn't work :D
    timeString: () => 0, // unused
    wasmMemory: new WebAssembly.Memory(WASM_MEMORY),
    wasmTable: new WebAssembly.Table(WASM_TABLE)
};

Module.todo.push([() => {
    Module.status = "PREPARE";
    // map imports to config
    Module.imports = { a: Object.fromEntries(Object.entries(WASM_IMPORTS).map(([key, name]) => [key, wasmImports[name]])) };
    return [];
}, false]);

Module.todo.push([() => {
    Module.status = "FETCH";
    // fetch necessary info and build
    return [
        fetch(`${CDN}build_${BUILD}.wasm.wasm`).then(res => res.arrayBuffer()),
        fetch(`${API_URL}servers`).then(res => res.json()),
        fetch(`${API_URL}tanks`).then(res => res.json())
    ];
}, true]);

Module.todo.push([(dependency, servers, tanks) => {
    Module.status = "INSTANTIATE";
    Module.servers = servers;
    Module.tankDefinitions = tanks;
    
    const parser = new WailParser(new Uint8Array(dependency));
    
    // original function, we want to modify these
    const originalVectorDone = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadVectorDone);
    const originalLoadChangelog = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadChangelog);
    const originalLoadGamemodeButtons = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadGamemodeButtons);
    const originalLoadTankDefs = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.loadTankDefinitions);
    const originalGetTankDef = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.getTankDefinition);
    const originalFindCommand = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.findCommand);
    const originalDecodeComponentList = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.decodeComponentList);
    const originalCreateEntityAtIndex = parser.getFunctionIndex(MOD_CONFIG.wasmFunctions.createEntityAtIndex);

    // function types
    const types = {
        // void []
        vn: parser.addTypeEntry({
            form: "func",
            params: [],
            returnType: null
        }),
        // void [int]
        vi: parser.addTypeEntry({
            form: "func",
            params: ["i32"],
            returnType: null
        }),
        // int [int]
        ii: parser.addTypeEntry({
            form: "func",
            params: ["i32"],
            returnType: "i32"
        })
    }

    // custom imports
    const imports = {
        loadGamemodeButtons: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "loadGamemodeButtons",
            kind: "func",
            type: types.vn 
        }),
        loadChangelog: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "loadChangelog",
            kind: "func",
            type: types.vn
        }),
        getTankDefinition: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "getTankDefinition",
            kind: "func",
            type: types.ii
        }),
        findCommand: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "findCommand",
            kind: "func",
            type: types.ii
        }),
        executeCommand: parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "executeCommand",
            kind: "func",
            type: types.vi
        })
    };

    Module.imports.mods = {
        loadGamemodeButtons: Module.loadGamemodeButtons,
        loadChangelog: Module.loadChangelog,
        getTankDefinition: Module.getTankDefinition,
        findCommand: Module.getCommand,
        executeCommand: Module.executeCommand
    };

    for(const addonId of Object.keys(CUSTOM_ADDONS)) {
        imports["_addon_" + addonId] = parser.addImportEntry({
            moduleStr: "mods",
            fieldStr: "_addon_" + addonId,
            kind: "func",
            type: types.vi
        });

        parser.addExportEntry(imports["_addon_" + addonId], {
            fieldStr: "_addon_" + addonId,
            kind: "func"
        });

        Module.imports.mods["_addon_" + addonId] = (ptr) => {
            const input = $(ptr);
            if(!ptr || !input.i32) throw "Invalid pointer received on addon callback";
            CUSTOM_ADDONS[addonId](new $Entity(input.i32 !== ptr ? input.$ : input));
        };
    }

    parser.addExportEntry(imports.executeCommand, {
        fieldStr: "executeCommand",
        kind: "func"
    });

    parser.addExportEntry(originalVectorDone, {
        fieldStr: "loadVectorDone",
        kind: "func"
    });
    
    parser.addExportEntry(originalCreateEntityAtIndex, {
        fieldStr: "createEntityAtIndex",
        kind: "func"
    });

    parser.addExportEntry(originalDecodeComponentList, {
        fieldStr: "decodeComponentList",
        kind: "func"
    });

    const findConsecutiveSequenceIndex = (array, sequence) => {
        const indexes = [];
        for(let i = 0; i < array.length - sequence.length + 1; i++) {
            let found = true;
            for(let j = 0; j < sequence.length; j++) {
                if(array[i + j] !== sequence[j]) {
                    found = false;
                    break;
                }
            }
            if(found) {
                indexes.push(i);
            }
        }
        return indexes;
    }
    
    // parses & modifies code function by function
    parser.addCodeElementParser(null, function({ index, bytes }) {
        const ptrPattern = VarUint32ToArray(MOD_CONFIG.memory.netColorTable);
        const geuPattern = [OP_I32_CONST, 19, OP_I32_GE_U];
        const ltuPattern = [OP_I32_CONST, 19, OP_I32_LT_U];
        for(const idx of findConsecutiveSequenceIndex(bytes, ptrPattern)) {
            const arr = Array.from(bytes); // convert to normal array for splicing
            arr.splice(idx, ptrPattern.length, ...VarUint32ToArray(DYNAMIC_TOP_PTR + 4)); // using "empty" space
            bytes = new Uint8Array(arr); // convert back to u8[]
            let check = findConsecutiveSequenceIndex(bytes, geuPattern); // <= kMaxColors
            if(!check.length) {
                check = findConsecutiveSequenceIndex(bytes, ltuPattern); // invert, > kMaxColors
                for(const idx of check) {
                    const arr = Array.from(bytes);
                    arr.splice(idx, ltuPattern.length, OP_I32_CONST, ...VarSint32ToArray(-2), OP_I32_NE); // color code -2 = kMaxColors
                    bytes = new Uint8Array(arr);
                }
            } else {
                for(const idx of check) {
                    const arr = Array.from(bytes);
                    arr.splice(idx, geuPattern.length, OP_I32_CONST, ...VarSint32ToArray(-2), OP_I32_EQ); // color code -2 = kMaxColors
                    bytes = new Uint8Array(arr);
                }
            }
        }
        
        switch(index) {
            // modify load changelog function
            case originalLoadChangelog.i32(): // we only need the part where it checks if the changelog is already loaded to avoid too many import calls
                return new Uint8Array([
                    ...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.changelog),
                    OP_CALL, ...VarUint32ToArray(imports.loadChangelog.i32()),
                    OP_RETURN,
                    ...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.changelog)
                ]);
            // modify load gamemode buttons function
            case originalLoadGamemodeButtons.i32(): // we only need the part where it checks if the buttons are already loaded to avoid too many import calls
                return new Uint8Array([
                    ...bytes.subarray(0, MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons),
                    OP_CALL, ...VarUint32ToArray(imports.loadGamemodeButtons.i32()),
                    OP_RETURN,
                    ...bytes.subarray(MOD_CONFIG.wasmFunctionHookOffset.gamemodeButtons)
                ]);
            // overwrite get tankdef function
            case originalGetTankDef.i32(): // we modify this to call a js function which then returns the tank def ptr from a table
                return new Uint8Array([
                    OP_GET_LOCAL, 0,
                    OP_CALL, ...VarUint32ToArray(imports.getTankDefinition.i32()),
                    OP_RETURN,
                    OP_END
                ]);
            // overwrite find command function
            case originalFindCommand.i32():
                return new Uint8Array([
                    OP_GET_LOCAL, 0,
                    OP_CALL, ...VarUint32ToArray(imports.findCommand.i32()),
                    OP_RETURN,
                    OP_END
                ]);
            // delete tankdefs loading function
            case originalLoadTankDefs.i32(): // we dont want this to run anymore because it will call the original tank wrapper function
                return new Uint8Array([
                    OP_END
                ]);
            // no interesting index
            default:
                return bytes;
        }
    });

    // parse modded wasm
    parser.parse();
    // instantiate
    return [new Promise(resolve => WebAssembly.instantiate(parser.write(), Module.imports).then(res => resolve(res.instance), reason => Module.abort(reason)))];
}, true]);

Module.todo.push([instance => {
    Module.status = "INITIALIZE";
    // Exports
    Module.exports = Object.fromEntries(Object.entries(instance.exports).map(([key, func]) => [WASM_EXPORTS[key], func]));    
    Module.rawExports = instance.exports;
    // Memory
    Module.memBuf = wasmImports.wasmMemory.buffer,
    Module.HEAPU8 = new Uint8Array(Module.memBuf);
    Module.HEAP8 = new Int8Array(Module.memBuf);
    Module.HEAPU16 = new Uint16Array(Module.memBuf);
    Module.HEAP16 = new Int16Array(Module.memBuf);
    Module.HEAPU32 = new Uint32Array(Module.memBuf);
    Module.HEAP32 = new Int32Array(Module.memBuf);
    Module.HEAPF32 = new Float32Array(Module.memBuf);
    Module.HEAPF64 = new Float64Array(Module.memBuf);
    Module.HEAPU64 = new BigUint64Array(Module.memBuf);
    Module.HEAP64 = new BigInt64Array(Module.memBuf);
    // Cp5 Contexts
    Module.cp5 = {
        contexts: [],
        images: [],
        sockets: [],
        patterns: []
    };
    // window.input & misc, see input.js
    window.setupInput();
    return [];
}, false]);

Module.todo.push([() => {
    window.Game = {
        reloadColors: async () => {
            Module.colors = await fetch(`${API_URL}colors`).then(res => res.json());
            Module.loadColors();
        },
        // refetches servers & resets gamemode buttons
        reloadServers: async () => {
            Module.servers = await fetch(`${API_URL}servers`).then(res => res.json());
            Module.loadGamemodeButtons();
        },
        // refetches tankdefs & resets them
        reloadTanks: async () => {
            Module.tankDefinitions = await fetch(`${API_URL}tanks`).then(res => res.json());
            if(Module.tankDefinitionsTable) {
                for(const tankDef of Module.tankDefinitionsTable) {
                    if(tankDef) Module.exports.free(tankDef);
                }
            }
            Module.loadTankDefinitions();
        },
        reloadCommands: async () => {
            Module.commandDefinitions = await fetch(`${API_URL}commands`).then(res => res.json());
            Module.loadCommands(Module.commandDefinitions); // remote
            Module.loadCommands(); // local
        },
        // sets changelog (input: [...""])
        changeChangelog: (lines) => Module.loadChangelog(lines),
        // main socket, see also Module.cp5.sockets[0]
        get socket() {
            return Module.cp5.sockets[0];
        },
        // executes spawn command
        spawn: name => window.input.execute(`game_spawn ${name}`),
        // executes reconnect command
        reconnect: () => window.input.execute(`lb_reconnect`)
    };

    // custom commands
    Module.executeCommandFunctionIndex = Module.imports.a.table.grow(1);
    Module.imports.a.table.set(Module.executeCommandFunctionIndex, Module.rawExports.executeCommand);

    // custom addons
    for(const addonId of Object.keys(CUSTOM_ADDONS)) {
        ADDON_MAP[addonId] = Module.imports.a.table.grow(1);
        Module.imports.a.table.set(ADDON_MAP[addonId],  Module.rawExports["_addon_" + addonId]);
    }

    Module.status = "START";
    // emscripten requirements
    Module.HEAP32[DYNAMIC_TOP_PTR >> 2] = DYNAMIC_BASE;
    Module.isRunning = true;
    Module.exports.wasmCallCtors();
    Module.exports.main();


    const reloadServersInterval = () => setTimeout(() => {
        reloadServersInterval();
        if(Module.reloadServersInterval < 0) return;
        Game.reloadServers(); 
    }, Module.reloadServersInterval);
    reloadServersInterval();

    const reloadTanksInterval = () => setTimeout(() => {
        reloadTanksInterval();
        if(Module.reloadTanksInterval < 0) return;
        Game.reloadTanks();
    }, Module.reloadTanksInterval);
    reloadTanksInterval();

    const reloadCommandsInterval = () => setTimeout(() => {
        reloadCommandsInterval();
        if(Module.reloadCommandsInterval < 0) return;
        Game.reloadCommands();
    }, Module.reloadCommandsInterval);
    reloadCommandsInterval();
    
    const reloadColorsInterval = () => setTimeout(() => {
        reloadColorsInterval();
        if(Module.reloadColorsInterval < 0) return;
        Game.reloadColors();
    }, Module.reloadColorsInterval);
    reloadColorsInterval();
}, false]);


// Part of the original emscripten bootstrap
class ASMConsts {
    static createCanvasCtxWithAlpha(canvasId, alpha) {
        const canvas = document.getElementById(Module.UTF8ToString(canvasId));
        if(!canvas) return -1;
        const ctx = canvas.getContext("2d", {
            alpha: Boolean(alpha)
        });
        for (let i = 0; i < Module.cp5.contexts.length; ++i) {
            if (Module.cp5.contexts[i] !== null) continue;
            Module.cp5.contexts[i] = ctx;
            return i;
        }
        Module.cp5.contexts.push(ctx);
        return Module.cp5.contexts.length - 1;
    }

    static createImage(src) {
        const img = new Image;
        img.isLoaded = false;
        img.onload = () => img.isLoaded = true;
        img.src = `${CDN}${Module.UTF8ToString(src)}`;
        for (let i = 0; i < Module.cp5.images.length; ++i) {
            if (Module.cp5.images[i] !== null) continue;
            Module.cp5.images[i] = img;
            return i;
        }
        Module.cp5.images.push(img);
        return Module.cp5.images.length - 1;
    }

    static websocketSend(socketId, packetStart, packetLength) {
        const socket = Module.cp5.sockets[socketId];
        if(!socket || socket.readyState !== 1) return 0;
        try {
            socket.send(Module.HEAP8.subarray(packetStart, packetStart + packetLength));
        } catch(e) {}
        return 1;
    }

    static wipeContext(index) {
        Module.cp5.contexts[index] = null;
    }

    static modulo(a, b) {
        return a % b;
    }

    static wipeSocket(index) {
        const socket = Module.cp5.sockets[index];
        socket.onopen = socket.onclose = socket.onmessage = socket.onerror = function() {};
        for(let i = 0; i < socket.events.length; ++i) Module.exports.free(socket.events[i][1]);
        socket.events = null;
        try {
            socket.close();
        } catch(e) {}
        Module.cp5.sockets[index] = null;
    }

    static setTextInput(value) {
        Module.textInput.value = Module.UTF8ToString(value);
    }

    static wipeImage(index) {
        Module.cp5.images[index] = null;
    }

    static reloadWindowTimeout() {
        //setTimeout(() => window.location.reload(), 100);
    }

    static existsInWindowObject(key) {
        return Boolean(window[Module.UTF8ToString(key)]);
    }

    // 6 (ads)

    static getQueries() {
        const queryString = window.location.href.split("?")[0];
        return Module.allocateUTF8(queryString.slice(0, queryString.lastIndexOf("/")));
    }

    // 2 (ads)

    static getLocalStorage(key, length) {
        const keyStr = Module.UTF8ToString(key);
        let str = window.localStorage[keyStr] || "";
        if(
            keyStr === "gamemode"
            && Module.servers
            && Module.servers.length
            && !Module.servers.find(({ gamemode }) => window.localStorage[keyStr] === gamemode)
        ) str = Module.servers[0].gamemode;
        Module.HEAPU32[length >> 2] = str.length;
        return Module.allocateUTF8(str);
    }

    static deleteLocalStorage(key) {
        delete window.localStorage[Module.UTF8ToString(key)];
    }

    static removeChildNode(nodeId) {
        const node = document.getElementById(Module.UTF8ToString(nodeId));
        if(node && node.parentNode) node.parentNode.removeChild(node);
    }

    static checkElementProperty(elementId, propertyKey, propertyIndex, value) {
        const element = document.getElementById(Module.UTF8ToString(elementId));
        const key = Module.UTF8ToString(propertyKey);
        if(!element || !element[key]) return true;
        return element[key][Module.UTF8ToString(propertyIndex)] === Module.UTF8ToString(value);
    }

    static existsQueryOrIsBlank(query) {
        const elements = document.querySelectorAll(Module.UTF8ToString(query));
        for(let i = 0; i < elements.length; ++i)
            if(elements[i].src === "about:blank") return true;
        return elements.length === 0;
    }

    // 1 (ads)

    static setLocalStorage(key, valueStart, valueLength) {
        window.localStorage[Module.UTF8ToString(key)] = Decoder.decode(Module.HEAPU8.subarray(valueStart, valueStart + valueLength));
    }

    // 3 (ads)

    static getGamepad() {
        return window.navigator.getGamepads && window.navigator.getGamepads()[0]?.mapping === "standard";
    }

    static toggleFullscreen() {
        const requestMethod = document.body.requestFullScreen || document.body.webkitRequestFullScreen || document.body.mozRequestFullScreen || document.body.msRequestFullScreen;
        const cancelMethod = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen || document.msExitFullscreen;
        if(!requestMethod && !cancelMethod) return;
        requestMethod ? requestMethod.call(document.body) : cancelMethod.call(document);
    }

    static getCanvasSize(ctxId, width, height) {
        const canvas = Module.cp5.contexts[ctxId].canvas;
        Module.HEAP32[width >> 2] = canvas.width;
        Module.HEAP32[height >> 2] = canvas.height;
    }

    static setCursorDefault() {
        document.getElementById("canvas").style.cursor = "default";
    }

    static setCursorPointer() {
        document.getElementById("canvas").style.cursor = "pointer";
    }

    static setCursorText() {
        document.getElementById("canvas").style.cursor = "text";
    }

    static getTextInput() {
        return Module.allocateUTF8(Module.textInput.value);
    }

    static enableTyping(left, top, width, height, enabled) {
        window.setTyping(true);
        Module.textInputContainer.style.display = "block";
        Module.textInputContainer.style.position = "absolute";
        Module.textInputContainer.style.left = window.unscale(left) + "px";
        Module.textInputContainer.style.top = window.unscale(top) + "px";
        Module.textInput.style.width = window.unscale(width * 0.96) + "px";
        Module.textInput.style.height = window.unscale(height) + "px";
        Module.textInput.style.lineHeight = window.unscale(height * 0.9) + "px";
        Module.textInput.style.fontSize = window.unscale(height * 0.9) + "px";
        Module.textInput.style.paddingLeft = "5px";
        Module.textInput.style.paddingRight = "5px";
        Module.textInput.disabled = !enabled;
        Module.textInput.focus();
    }

    static disableTyping() {
        window.setTyping(false);
        Module.textInput.blur();
        Module.textInput.value = "";
        Module.textInputContainer.style.display = "none";
    }

    static focusCanvas() {
        const canvas = document.getElementById("canvas");
        if(document.activeElement && document.activeElement !== canvas) document.activeElement.blur()
        canvas.focus();
    }

    static setCanvasSize(ctxId, width, height) {
        const canvas = Module.cp5.contexts[ctxId].canvas;
        canvas.width = width;
        canvas.height = height;
    }

    // 1 (ads)

    static copyUTF8(original) {
        return Module.allocateUTF8(Module.UTF8ToString(original));
    }

    static alert(text) {
        window.alert(Module.UTF8ToString(text));
    }

    static saveContext(ctxId) {
        Module.cp5.contexts[ctxId].save();
    }

    static restoreContext(ctxId) {
        Module.cp5.contexts[ctxId].restore();
    }

    static scaleContextAlpha(ctxId, alpha) {
        Module.cp5.contexts[ctxId].globalAlpha *= alpha;
    }

    // 5 (ads)

    static setContextFillStyle(ctxId, r, g, b) {
        Module.cp5.contexts[ctxId].fillStyle = "rgb(" + r + "," + g + "," + b + ")";
    }

    static setContextTransform(ctxId, a, b, c, d, e, f) {
        Module.cp5.contexts[ctxId].setTransform(a, b, c, d, e, f);
    }

    static contextFillRect(ctxId) {
        Module.cp5.contexts[ctxId].fillRect(0, 0, 1, 1);
    }

    static contextBeginPath(ctxId) {
        Module.cp5.contexts[ctxId].beginPath();
    }

    static contextClip(ctxId) {
        Module.cp5.contexts[ctxId].clip();
    }

    static contextFill(ctxId) {
        Module.cp5.contexts[ctxId].fill();
    }

    static setContextLineJoinRound(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "round";
    }

    static setContextLineJoinBevel(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "bevel";
    }

    static setContextLineJoinMiter(ctxId) {
        Module.cp5.contexts[ctxId].lineJoin = "miter";
    }

    static setContextLineWidth(ctxId, width) {
        Module.cp5.contexts[ctxId].lineWidth = width;
    }

    static setContextStrokeStyle(ctxId, r, g, b) {
        Module.cp5.contexts[ctxId].strokeStyle = "rgb(" + r + "," + g + "," + b + ")";
    }

    static setContextTransformBounds(ctxId, a, b, c, d) {
        Module.cp5.contexts[ctxId].setTransform(a, b, c, d, 0, 0);
    }

    static contextStroke(ctxId) {
        Module.cp5.contexts[ctxId].stroke();
    }

    // draws one pixel
    static contextRect(ctxId) {
        Module.cp5.contexts[ctxId].rect(0, 0, 1, 1);
    }

    static getFontsLoaded() {
        return document.fonts.check("1px Ubuntu");
    }

    static setContextFont(ctxId, fontSize) {
        Module.cp5.contexts[ctxId].font = fontSize + "px Ubuntu";
    }

    static measureContextTextWidth(ctxId, text) {
        return Module.cp5.contexts[ctxId].measureText(Module.UTF8ToString(text)).width;
    }

    static setContextAlpha(ctxId, alpha) {
        Module.cp5.contexts[ctxId].globalAlpha = alpha;
    }

    static contextFillText(ctxId, text) {
        Module.cp5.contexts[ctxId].fillText(Module.UTF8ToString(text), 0, 0);
    }

    static contextStrokeText(ctxId, text) {
        Module.cp5.contexts[ctxId].strokeText(Module.UTF8ToString(text), 0, 0);
    }

    static setContextTextBaselineTop(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "top";
    }

    static setContextTextBaselineHanging(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "hanging";
    }

    static setContextTextBaselineMiddle(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "middle";
    }

    static setContextTextBaselineAlphabetic(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "alphabetic";
    }

    static setContextTextBaselineIdeographic(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "ideographic";
    }

    static setContextTextBaselineBottom(ctxId) {
        Module.cp5.contexts[ctxId].textBaseline = "bottom";
    }

    static setContextTransformNormalize(ctxId) {
        Module.cp5.contexts[ctxId].setTransform(1, 0, 0, 1, 0, 0);
    }

    static contextMoveTo(ctxId, x, y) {
        Module.cp5.contexts[ctxId].moveTo(x, y);
    }

    static contextLineTo(ctxId, x, y) {
        Module.cp5.contexts[ctxId].lineTo(x, y);
    }

    static contextClosePath(ctxId) {
        Module.cp5.contexts[ctxId].closePath();
    }

    static contextArc(ctxId, startAngle, endAngle, counterclockwise) {
        Module.cp5.contexts[ctxId].arc(0, 0, 1, startAngle, endAngle, counterclockwise)
    } 

    static copyToKeyboard(text) {
        window?.navigator?.clipboard?.writeText(Module.UTF8ToString(text));
    }

    static setLocation(newLocation) {
        // open in new tab instead
        window.open(Module.UTF8ToString(newLocation));
    }

    static contextDrawImage(ctxId, imgId) {
        const img = Module.cp5.images[imgId];
        if(!img.isLoaded || img.width === 0 || img.height === 0) return;
        Module.cp5.contexts[ctxId].drawImage(img, 0, 0, img.width, img.height, 0, 0, 1, 1);
    }

    static getImage(imgId, isLoaded, width, height) {
        const img = Module.cp5.images[imgId];
        Module.HEAPU8[isLoaded >> 0] = img.isLoaded;
        Module.HEAP32[width >> 2] = img.width;
        Module.HEAP32[height >> 2] = img.height;
    }

    static contextDrawCanvas(ctxId, targetCtxId) {
        Module.cp5.contexts[ctxId].drawImage(Module.cp5.contexts[targetCtxId].canvas, 0, 0);
    }

    static setContextLineCapButt(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "butt";
    }

    static setContextLineCapRound(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "round";
    }

    static setContextLineCapSquare(ctxId) {
        Module.cp5.contexts[ctxId].lineCap = "square";
    }

    static contextStrokeRect(ctxId) {
        Module.cp5.contexts[ctxId].strokeRect(0, 0, 1, 1);
    }

    static contextDrawFullCanvas(ctxId, targetCtxId) {
        const canvas = Module.cp5.contexts[targetCtxId].canvas;
        Module.cp5.contexts[ctxId].drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, 1, 1);
    }
    
    static isContextPatternAvailable() {
        return Boolean(CanvasRenderingContext2D.prototype.createPattern);
    }

    static createContextPattern(ctxId, targetCtxId) {
        const pattern = Module.cp5.contexts[ctxId].createPattern(Module.cp5.contexts[targetCtxId].canvas, null);
        for (let i = 0; i < Module.cp5.patterns.length; ++i) {
            if (Module.cp5.patterns[i] !== null) continue;
            Module.cp5.patterns[i] = pattern;
            return i;
        }
        Module.cp5.patterns.push(pattern);
        return Module.cp5.patterns.length - 1;
    }

    static contextGetPixelColor(ctxId, x, y) {
        const data = Module.cp5.contexts[ctxId].getImageData(x, y, 1, 1);
        return data.data[0] << 16 | data.data[1] << 8 | data.data[2];
    }

    static contextDrawCanvasSourceToPixel(ctxId, targetCtxId, x, y, w, h) {
        Module.cp5.contexts[ctxId].drawImage(Module.cp5.contexts[targetCtxId].canvas, x, y, w, h, 0, 0, 1, 1);
    }

    static contextFillRectWithPattern(ctxId, patternId, width, height) {
        Module.cp5.contexts[ctxId].fillStyle = Module.cp5.patterns[patternId];
        Module.cp5.contexts[ctxId].fillRect(0, 0, width, height);
    }

    static wipePattern(patternId) {
        Module.cp5.patterns[patternId] = null;
    }

    // 2 (verifying bootstrap integrity ?)

    static existsQuery(query) {
        return document.querySelector(Module.UTF8ToString(query)) !== null;
    }

    // 1 (anticheat)

    // used for shadow root
    static canvasHasSamePropertyAsDocumentBody(property) {
        const propertyKey = Module.UTF8ToString(property);
        return document.getElementById("canvas")[propertyKey] !== document.body[propertyKey];
    }

    // used for shadow root
    static existsDocumentBodyProperty(property) {
        return document.body[Module.UTF8ToString(property)] !== undefined;
    }

    // used for shadow root
    static existsDocumentBodyProperty2(property) {
        return Boolean(document.body[Module.UTF8ToString(property)]);
    }

    // used for shadow root
    static existsDivPropertyAndEqualsPropertyOnDocumentBody(propertyDiv, propertyBody) {
        const propertyDivKey = Module.UTF8ToString(propertyDiv);
        const div = document.createElement("div");
        if(!div[propertyDivKey]) return;
        return div[propertyDivKey]() === document.body[Module.UTF8ToString(propertyBody)];
    }

    // 3 (anticheat)

    // anticheat but need to be kept
    static acCheckWindow(property) {
        if(Module.UTF8ToString(property) === "navigator") return true;
    }

    static getDocumentBody() {
        return Module.allocateUTF8(document.body.innerHTML);
    }

    // 2 (anticheat)

    static getUserAgent() {
        return Module.allocateUTF8(window.navigator.userAgent);
    }

    // 1 (anticheat)

    static getQuerySelectorToString() {
        return Module.allocateUTF8("function querySelector() { [native code] }");
    }
    
    static getFillTextToString() {
        return Module.allocateUTF8("function fillText() { [native code] }");
    }

    static getStrokeRectToString() {
        return Module.allocateUTF8("function strokeRect() { [native code] }");
    }

    static getStrokeTextToString() {
        return Module.allocateUTF8("function strokeText() { [native code] }");
    }

    static getScaleToString() {
        return Module.allocateUTF8("function scale() { [native code] }");
    }

    static getTranslateToString() {
        return Module.allocateUTF8("function translate() { [native code] }");
    }

    static getFillRectToString() {
        return Module.allocateUTF8("function fillRect() { [native code] }");
    }

    static getRotateToString() {
        return Module.allocateUTF8("function rotate() { [native code] }");
    }

    static getGetImageDataToString() {
        return Module.allocateUTF8("function getImageData() { [native code] }");
    }

    // 1 (ads)

    static contextClearRect(ctxId) {
        const ctx = Module.cp5.contexts[ctxId];
        const canvas = ctx.canvas;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    static createCanvasCtx() {
        const ctx = document.createElement("canvas").getContext("2d");
        for(let i = 0; i < Module.cp5.contexts.length; ++i) {
            if(Module.cp5.contexts[i]) continue;
            Module.cp5.contexts[i] = ctx;
            return i; 
        }
        Module.cp5.contexts.push(ctx);
        return Module.cp5.contexts.length - 1;
    }

    static setContextMiterLimit(ctxId, miterLimit) {
        Module.cp5.contexts[ctxId].miterLimit = miterLimit;
    }

    static getWindowLocation() {
        return Module.allocateUTF8(window.location.hash);
    }

    static setLoadingStatus(status) {
        if(window.setLoadingStatus) window.setLoadingStatus(Module.UTF8ToString(status));
    }

    static m28nReply(requestId, endpoint) {
        if(!Module.servers || !Module.servers.length) return console.warn("No Servers Found");
        const server = Module.servers.find(({ gamemode }) => gamemode === Module.UTF8ToString(endpoint).slice(7)) || Module.servers[0];
        const id = Module.allocateUTF8(server.gamemode);
        const ipv4 = Module.allocateUTF8(server.gamemode);
        const ipv6 = Module.allocateUTF8(server.gamemode);
        Module.exports.restReply(requestId, id, ipv4, ipv6);
        Module.exports.free(id);
        Module.exports.free(ipv4);
        Module.exports.free(ipv6);
    }

    static isSSL() {
        return false;
    }

    static createWebSocket(urlPtr) {
        const url = Module.UTF8ToString(urlPtr);
        const ws = new WebSocket(`ws${location.protocol.slice(4)}//${location.host}/${url.slice(5, url.length - 4)}`);
        ws.binaryType = "arraybuffer";
        ws.events = [];
        ws.onopen = function() {
            ws.events.push([2, 0, 0]);
            Module.exports.checkWS();
        };
        ws.onerror = function() {
            ws.events.push([3, 0, 0]);
            Module.exports.checkWS();
        };
        ws.onclose = function({ reason }) {
            ws.events.push([4, 0, 0]);
            Module.exports.checkWS();
            if(reason) console.log("WebSocket closed due to:", reason);
        };
        ws.onmessage = function(e) {
            const view = new Uint8Array(e.data);
            if(view[0] === 7) {
                let out = 0, i = 0, at = 1;
                while(view[at] & 0x80) {
                    out |= (view[at++] & 0x7f) << i;
                    i += 7;
                }
                out |= (view[at++] & 0x7f) << i;
                Module.permissionLevel = (0 - (out & 1)) ^ (out >>> 1);
                window.Game.reloadCommands();
            }
            const ptr = Module.exports.malloc(view.length);
            Module.HEAP8.set(view, ptr);
            ws.events.push([1, ptr, view.length]);
            Module.exports.checkWS();
        };
        for (let i = 0; i < Module.cp5.sockets.length; ++i) {
            if (Module.cp5.sockets[i] != null)
                continue;
            Module.cp5.sockets[i] = ws;
            return i;
        }

        if(Module.reloadServersInterval === -2) Game.reloadServers();
        if(Module.reloadTanksInterval === -2) Game.reloadTanks(); 
        if(Module.reloadCommandsInterval === -2) Game.reloadCommands();
        if(Module.reloadColorsInterval === -2) Game.reloadColors();

        Module.cp5.sockets.push(ws);
        return Module.cp5.sockets.length - 1;
    }

    static findServerById(requestId, endpoint) {
        Module.exports.restReply(requestId, 0, 0, 0);
    }

    static invalidPartyId() {
        alert("Invalid party ID");
    }

    static wipeLocation() {
        window.location.hash = "";
    }

    static getGamepadAxe(axeId) {
        const axes = window.navigator.getGamepads()[0].axes;
        if(axeId >= axes.length) return;
        return axes[axeId];
    }

    static getGamepadButtonPressed(buttonId) {
        const buttons = window.navigator.getGamepads()[0].buttons;
        if(buttonId >= buttons.length) return;
        return buttons[buttonId].pressed;
    }

    static pollWebSocketEvent(socketId, msg, length) {
        const ws = Module.cp5.sockets[socketId];
        if(ws.events.length === 0) return null;
        const event = ws.events.shift();
        Module.HEAPU32[msg >> 2] = event[1]; // packet ptr
        Module.HEAP32[length >> 2] = event[2]; // packet length
        return event[0]; // type
    }

    static updateToNewVersion(version) {
        console.log(Module.UTF8ToString(version));
        setTimeout(() => window.location.reload());
    }

    // 1 (pow)

    static reloadWindow() {
        setTimeout(() => window.location.reload());
    }

    static getWindowLocationSearch() {
        return Module.allocateUTF8(window.location.search);
    }

    static getWindowReferrer() {
        return Module.allocateUTF8(window.document.referrer);
    }

    // 7 (fingerprinting)

    static empty() {}
}

Module.run();
`

~src/Coder/Reader.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/**
 * UNDOCUMENTED FILE
 **/

const convo = new ArrayBuffer(4);
const u8 = new Uint8Array(convo);
const u16 = new Uint16Array(convo);
const i32 = new Int32Array(convo);
const u32 = new Uint32Array(convo);
const f32 = new Float32Array(convo);

const Decoder = new TextDecoder();

const endianSwap = (num: number) =>
    ((num & 0xff) << 24) |
    ((num & 0xff00) << 8) |
    ((num >> 8) & 0xff00) |
    ((num >> 24) & 0xff);

export default class Reader {
    public at = 0;
    private buffer: Uint8Array;

    public constructor(buf: Uint8Array | ArrayBuffer | Buffer) {
        this.buffer = new Uint8Array(buf instanceof ArrayBuffer ? buf : buf.buffer);
    }

    public u8() {
        return this.buffer[this.at++];
    }

    public u16() {
        u8.set(this.buffer.subarray(this.at, (this.at += 2)));
        return u16[0];
    }

    public u32() {
        u8.set(this.buffer.subarray(this.at, (this.at += 4)));

        return u32[0];
    }

    public vu() {
        let out = 0;
        let i = 0;
        while (this.buffer[this.at] & 0x80) {
            out |= (this.buffer[this.at++] & 0x7f) << i;
            i += 7;
        }
        out |= (this.buffer[this.at++] & 0x7f) << i;

        return out;
    }

    public vi() {
        let out = this.vu();

        return (0 - (out & 1)) ^ (out >>> 1);
    }

    public vf() {
        i32[0] = endianSwap(this.vi());

        return f32[0];
    }

    public stringNT() {
        return Decoder.decode(
            this.buffer.subarray(
                this.at,
                (this.at = this.buffer.indexOf(0, this.at) + 1) - 1
            )
        );
    }

    public string(length = this.vu()) {
        return Decoder.decode(this.buffer.slice(this.at, (this.at += length)));
    }

    public float() {
        u8.set(this.buffer.subarray(this.at, (this.at += 4)));

        return f32[0];
    }
}
`

~src/Coder/Writer.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { writtenBufferChunkSize } from "../config";

/**
 * UNDOCUMENTED FILE
 **/

const convo = new ArrayBuffer(4);
const i32 = new Int32Array(convo);
const f32 = new Float32Array(convo);

const Encoder: TextEncoder = new TextEncoder();

const endianSwap = (num: number) =>
    ((num & 0xff) << 24) |
    ((num & 0xff00) << 8) |
    ((num >> 8) & 0xff00) |
    ((num >> 24) & 0xff);

export default class Writer {
    // TODO(ABC):
    // Code a compressor (The previous was not OOP and used lz4js code)
    // If this code gets out of bounds (only happens if you play around with dev too much), the server crashes [better than out of bounds r/w].
    // 
    // TEMP FIX - 2022/11/11:
    //   OUTPUT_BUFFER now gets resized if running out of space for the packet
    //
    // TEMP FIX 2 - 2022/11/13:
    //   OUTPUT_BUFFER gets sent out in chunks (see Client.ts for more info)
    protected static OUTPUT_BUFFER = Buffer.alloc(writtenBufferChunkSize);
    protected _at: number = 0;

    protected get at() {
        return this._at;
    }

    protected set at(v) {
        this._at = v;

        if (Writer.OUTPUT_BUFFER.length <= this._at + 5) {
            const newBuffer = Buffer.alloc(Writer.OUTPUT_BUFFER.length + writtenBufferChunkSize);
            newBuffer.set(Writer.OUTPUT_BUFFER, 0);

            Writer.OUTPUT_BUFFER = newBuffer;
        }
    }

    public u8(val: number) {
        Writer.OUTPUT_BUFFER.writeUInt8(val >>> 0 & 0xFF, this.at);
        this.at += 1;
        return this;
    }
    public u16(val: number) {
        Writer.OUTPUT_BUFFER.writeUint16LE(val >>> 0 & 0xFFFF, this.at);
        this.at += 2;
        return this;
    }
    public u32(val: number) {
        Writer.OUTPUT_BUFFER.writeUint32LE(val >>> 0, this.at);
        this.at += 4;
        return this;
    }
    public float(val: number) {
        Writer.OUTPUT_BUFFER.writeFloatLE(val, this.at);
        this.at += 4;
        return this;
    }
    public vf(val: number) {
        f32[0] = val;

        return this.vi(endianSwap(i32[0]));
    }
    public vu(val: number) {
        val |= 0;

        do {
            let part = val;
            val >>>= 7;
            if (val) part |= 0x80;
            Writer.OUTPUT_BUFFER.writeUint8(part >>> 0 & 0xFF, this.at);
            this.at += 1;
        } while (val);

        // OUTPUT_BUFFER.set(buf.subarray(0, at), (this.at += at) - at);
        return this;
    }
    public vi(val: number) {
        val |= 0;
        return this.vu((0 - (val < 0 ? 1 : 0)) ^ (val << 1)); // varsint trick
    }
    public bytes(buffer: Uint8Array) {
        Writer.OUTPUT_BUFFER.set(buffer, this.at);
        this.at += buffer.byteLength;
        return this;
    }
    public raw(...data: number[]) {
        Writer.OUTPUT_BUFFER.set(data, this.at)
        this.at += data.length;
        return this;
    }
    public float64Precision(float: number) {
        return this.vi(float * 64)
    }
    public degrees(degrees: number) {
        degrees *= Math.PI / 180
        return this.vi(degrees * 64)
    }
    public stringNT(str: string) {
        const bytes = Encoder.encode(str + "\x00");
        // TODO(ABC): See line 69 Client.ts
        // rn it sends string out in bytes, send it all at once without messing up the chunking
        for (let i = 0; i < bytes.byteLength; ++i) {
            Writer.OUTPUT_BUFFER[this.at] = bytes[i];
            this.at += 1;
        }

        return this;
    }
    public entid(entity: null | {hash: number, id: number}) {
        if (!entity || entity.hash === 0) return this.u8(0);
        return this.vu(entity.hash).vu(entity.id);
    }
    public write(slice: boolean=false) {
        if (slice) return Writer.OUTPUT_BUFFER.slice(0, this.at);
        
        return Writer.OUTPUT_BUFFER.subarray(0, this.at);
    }
}
`

~src/Const/Commands.ts:
`
import Client from "../Client"
import { AccessLevel, maxPlayerLevel } from "../config";
import AbstractBoss from "../Entity/Boss/AbstractBoss";
import Defender from "../Entity/Boss/Defender";
import FallenBooster from "../Entity/Boss/FallenBooster";
import FallenOverlord from "../Entity/Boss/FallenOverlord";
import Guardian from "../Entity/Boss/Guardian";
import Summoner from "../Entity/Boss/Summoner";
import LivingEntity from "../Entity/Live";
import ArenaCloser from "../Entity/Misc/ArenaCloser";
import FallenAC from "../Entity/Misc/Boss/FallenAC";
import FallenSpike from "../Entity/Misc/Boss/FallenSpike";
import Dominator from "../Entity/Misc/Dominator";
import ObjectEntity from "../Entity/Object";
import AbstractShape from "../Entity/Shape/AbstractShape";
import Crasher from "../Entity/Shape/Crasher";
import Pentagon from "../Entity/Shape/Pentagon";
import Square from "../Entity/Shape/Square";
import Triangle from "../Entity/Shape/Triangle";
import AutoTurret from "../Entity/Tank/AutoTurret";
import Bullet from "../Entity/Tank/Projectile/Bullet";
import TankBody from "../Entity/Tank/TankBody";
import { Entity, EntityStateFlags } from "../Native/Entity";
import { saveToVLog } from "../util";
import { Stat, StatCount, StyleFlags, Tank } from "./Enums";
import { getTankByName } from "./TankDefinitions"

const RELATIVE_POS_REGEX = new RegExp(/~(-?\d+)?/);

export const enum CommandID {
    gameSetTank = "game_set_tank",
    gameSetLevel = "game_set_level",
    gameSetScore = "game_set_score",
    gameSetStat = "game_set_stat",
    gameSetStatMax = "game_set_stat_max",
    gameAddUpgradePoints = "game_add_upgrade_points",
    gameTeleport = "game_teleport",
    gameClaim = "game_claim",
    gameGodmode = "game_godmode",
    adminSummon = "admin_summon",
    adminKillAll = "admin_kill_all",
    adminKillEntity = "admin_kill_entity",
    adminCloseArena = "admin_close_arena"
}

export interface CommandDefinition {
    id: CommandID,
    usage?: string,
    description?: string,
    permissionLevel: AccessLevel,
    isCheat: boolean
}

export interface CommandCallback {
    (client: Client, ...args: string[]): string | void 
}

export const commandDefinitions = {
    game_set_tank: {
        id: CommandID.gameSetTank,
        usage: "[tank]",
        description: "Changes your tank to the given class",
        permissionLevel: AccessLevel.BetaAccess,
        isCheat: true
    },
    game_set_level: {
        id: CommandID.gameSetLevel,
        usage: "[level]",
        description: "Changes your level to the given whole number",
        permissionLevel: AccessLevel.BetaAccess,
        isCheat: true
    },
    game_set_score: {
        id: CommandID.gameSetScore,
        usage: "[score]",
        description: "Changes your score to the given whole number",
        permissionLevel: AccessLevel.BetaAccess,
        isCheat: true
    },
    game_set_stat: {
        id: CommandID.gameSetStat,
        usage: "[stat num] [points]",
        description: "Set the value of one of your statuses. Values can be greater than the capacity. [stat num] is equivalent to the number that appears in the UI",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: true
    },
    game_set_stat_max: {
        id: CommandID.gameSetStatMax,
        usage: "[stat num] [max]",
        description: "Set the max value of one of your statuses. [stat num] is equivalent to the number that appears in the UI",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: true
    },
    game_add_upgrade_points: {
        id: CommandID.gameAddUpgradePoints,
        usage: "[points]",
        description: "Add upgrade points",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: true
    },
    game_teleport: {
        id: CommandID.gameTeleport,
        usage: "[x] [y]",
        description: "Teleports you to the given position",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: true
    },
    game_claim: {
        id: CommandID.gameClaim,
        usage: "[entityName]",
        description: "Attempts claiming an entity of the given type",
        permissionLevel: AccessLevel.BetaAccess,
        isCheat: false
    },
    game_godmode: {
        id: CommandID.gameGodmode,
        usage: "[?value]",
        description: "Set the godemode. Toggles if [value] is not specified",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: true
    },
    admin_summon: {
        id: CommandID.adminSummon,
        usage: "[entityName] [?count] [?x] [?y]",
        description: "Spawns entities at a certain location",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: false
    },
    admin_kill_all: {
        id: CommandID.adminKillAll,
        description: "Kills all entities in the arena",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: false
    },
    admin_kill_entity: {
        id: CommandID.adminKillEntity,
        usage: "[entityName]",
        description: "Kills all entities of the given type (might include self)",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: false
    },
    admin_close_arena: {
        id: CommandID.adminCloseArena,
        description: "Closes the current arena",
        permissionLevel: AccessLevel.FullAccess,
        isCheat: false
    }
} as Record<CommandID, CommandDefinition>

export const commandCallbacks = {
    game_set_tank: (client: Client, tankNameArg: string) => {
        const tankDef = getTankByName(tankNameArg);
        const player = client.camera?.cameraData.player;
        if (!tankDef || !Entity.exists(player) || !(player instanceof TankBody)) return;
        if (tankDef.flags.devOnly && client.accessLevel !== AccessLevel.FullAccess) return;
        player.setTank(tankDef.id);
    },
    game_set_level: (client: Client, levelArg: string) => {
        const level = parseInt(levelArg);
        const player = client.camera?.cameraData.player;
        if (isNaN(level) || !Entity.exists(player) || !(player instanceof TankBody)) return;
        const finalLevel = client.accessLevel == AccessLevel.FullAccess ? level : Math.min(maxPlayerLevel, level);
        client.camera?.setLevel(finalLevel);
    },
    game_set_score: (client: Client, scoreArg: string) => {
        const score = parseInt(scoreArg);
        const camera = client.camera?.cameraData;
        const player = client.camera?.cameraData.player;
        if (isNaN(score) || score > Number.MAX_SAFE_INTEGER || score < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
        camera.score = score;
    },
    game_set_stat_max: (client: Client, statIdArg: string, statMaxArg: string) => {
        const statId = StatCount - parseInt(statIdArg);
        const statMax = parseInt(statMaxArg);
        const camera = client.camera?.cameraData;
        const player = client.camera?.cameraData.player;
        if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statMax) || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
        const clampedStatMax = Math.max(statMax, 0);
        camera.statLimits[statId as Stat] = clampedStatMax;
        camera.statLevels[statId as Stat] = Math.min(camera.statLevels[statId as Stat], clampedStatMax);
    },
    game_set_stat: (client: Client, statIdArg: string, statPointsArg: string) => {
        const statId = StatCount - parseInt(statIdArg);
        const statPoints = parseInt(statPointsArg);
        const camera = client.camera?.cameraData;
        const player = client.camera?.cameraData.player;
        if (statId < 0 || statId >= StatCount || isNaN(statId) || isNaN(statPoints) || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
        camera.statLevels[statId as Stat] = statPoints;
    },
    game_add_upgrade_points: (client: Client, pointsArg: string) => {
        const points = parseInt(pointsArg);
        const camera = client.camera?.cameraData;
        const player = client.camera?.cameraData.player;
        if (isNaN(points) || points > Number.MAX_SAFE_INTEGER || points < Number.MIN_SAFE_INTEGER || !Entity.exists(player) || !(player instanceof TankBody) || !camera) return;
        camera.statsAvailable += points;
    },
    game_teleport: (client: Client, xArg: string, yArg: string) => {
        const player = client.camera?.cameraData.player;
        if (!Entity.exists(player) || !(player instanceof ObjectEntity)) return;
        const x = xArg.match(RELATIVE_POS_REGEX) ? player.positionData.x + parseInt(xArg.slice(1) || "0", 10) : parseInt(xArg, 10);
        const y = yArg.match(RELATIVE_POS_REGEX) ? player.positionData.y + parseInt(yArg.slice(1) || "0", 10) : parseInt(yArg, 10);
        if (isNaN(x) || isNaN(y)) return;
        player.positionData.x = x;
        player.positionData.y = y;
        player.setVelocity(0, 0);
        player.entityState |= EntityStateFlags.needsCreate | EntityStateFlags.needsDelete;
    },
    game_claim: (client: Client, entityArg: string) => {
        const TEntity = new Map([
          ["ArenaCloser", ArenaCloser],
          ["Dominator", Dominator],
          ["Shape", AbstractShape],
          ["Boss", AbstractBoss],
          ["AutoTurret", AutoTurret]
        ] as [string, typeof ObjectEntity][]).get(entityArg)

        if (!TEntity || !client.camera?.game.entities.AIs.length) return;

        const AIs = Array.from(client.camera.game.entities.AIs);
        for (let i = 0; i < AIs.length; ++i) {
            if (!(AIs[i].owner instanceof TEntity)) continue;
            client.possess(AIs[i]);
            return;
        }
    },
    game_godmode: (client: Client, activeArg?: string) => {
        const player = client.camera?.cameraData.player;
        if (!Entity.exists(player) || !(player instanceof TankBody)) return;

        switch (activeArg) {
            case "on":
                player.setInvulnerability(true);
                break;
            case "off":
                player.setInvulnerability(false);
                break;
            default:
                player.setInvulnerability(!player.isInvulnerable);
                break;
        }

        const godmodeState = player.isInvulnerable ? "ON" : "OFF";
        return `God mode: ${godmodeState}`;
    },
    admin_summon: (client: Client, entityArg: string, countArg?: string, xArg?: string, yArg?: string) => {
        const count = countArg ? parseInt(countArg) : 1;
        let x = parseInt(xArg || "0", 10);
        let y = parseInt(yArg || "0", 10);

        const player = client.camera?.cameraData.player;
        if (Entity.exists(player) && player instanceof ObjectEntity) {
            if (xArg && xArg.match(RELATIVE_POS_REGEX)) {
                x = player.positionData.x + parseInt(xArg.slice(1) || "0", 10);
            }
            if (yArg && yArg.match(RELATIVE_POS_REGEX)) {
                y = player.positionData.y + parseInt(yArg.slice(1) || "0", 10);
            }
        }

        const game = client.camera?.game;
        const TEntity = new Map([
            ["Defender", Defender],
            ["Summoner", Summoner],
            ["Guardian", Guardian],
            ["FallenOverlord", FallenOverlord],
            ["FallenBooster", FallenBooster],
            ["FallenAC", FallenAC],
            ["FallenSpike", FallenSpike],
            ["ArenaCloser", ArenaCloser],
            ["Crasher", Crasher],
            ["Pentagon", Pentagon],
            ["Square", Square],
            ["Triangle", Triangle]
        ] as [string, typeof ObjectEntity][]).get(entityArg);

        if (isNaN(count) || count < 0 || !game || !TEntity) return;

        for (let i = 0; i < count; ++i) {
            const boss = new TEntity(game);
            if (!isNaN(x) && !isNaN(y)) {
                boss.positionData.x = x;
                boss.positionData.y = y;
            }
        }
    },
    admin_kill_all: (client: Client) => {
        const game = client.camera?.game;
        if(!game) return;
        for (let id = 0; id <= game.entities.lastId; ++id) {
			const entity = game.entities.inner[id];
			if (Entity.exists(entity) && entity instanceof LivingEntity && entity !== client.camera?.cameraData.player) entity.healthData.health = 0;
		}
    },
    admin_close_arena: (client: Client) => {
        client?.camera?.game.arena.close();
    },
    admin_kill_entity: (client: Client, entityArg: string) => {
        const TEntity = new Map([
          ["ArenaCloser", ArenaCloser],
          ["Dominator", Dominator],
          ["Bullet", Bullet],
          ["Tank", TankBody],
          ["Shape", AbstractShape],
          ["Boss", AbstractBoss]
        ] as [string, typeof LivingEntity][]).get(entityArg);
        const game = client.camera?.game;
        if (!TEntity || !game) return;

        for (let id = 0; id <= game.entities.lastId; ++id) {
			const entity = game.entities.inner[id];
			if (Entity.exists(entity) && entity instanceof TEntity) entity.healthData.health = 0;
		}
    }
} as Record<CommandID, CommandCallback>

export const executeCommand = (client: Client, cmd: string, args: string[]) => {
    if (!commandDefinitions.hasOwnProperty(cmd) || !commandCallbacks.hasOwnProperty(cmd)) {
        return saveToVLog(`${client.toString()} tried to run the invalid command ${cmd}`);
    }

    if (client.accessLevel < commandDefinitions[cmd as CommandID].permissionLevel) {
        return saveToVLog(`${client.toString()} tried to run the command ${cmd} with a permission that was too low`);
    }

    const commandDefinition = commandDefinitions[cmd as CommandID];
    if (commandDefinition.isCheat) client.setHasCheated(true);

    const response = commandCallbacks[cmd as CommandID](client, ...args);
    if (response) {
        client.notify(response, 0x00ff00, 5000, `cmd-callback${commandDefinition.id}`);
    }
}
`

~src/Const/DevTankDefinitions.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { PI2 } from "../util";
import { TankDefinition } from "./TankDefinitions";

/**
 * The IDs for all the dev tanks, by name.
 */
export const enum DevTank {
    Developer = -1,
    UsainBolt = -2,
    BigBoi = -3,
    Bouncy = -4,
    Master = -5,
    Musketeer = -6,
    Squirrel = -7,
    Shotgun = -8,
    Flamethrower = -9,
    Builder = -10,
    Goodbye = -11,
    Spectator = -12,
    TheCroc = -13,
    Railgun = -14,
    Commander = -15
};

/**
 * List of all dev tank definitions.
*/
const DevTankDefinitions: TankDefinition[] = [
    {
        id: DevTank.Developer,
        name: "Developer",
        upgradeMessage: "Use your right mouse button to teleport to where your mouse is",
        // upgrades dont have any affect
        upgrades: [],
        barrels: [
            {
                angle: 0,
                delay: 0,
                size: 85,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: .5,
                    damage: .5,
                    health: 0.45,
                    scatterRate: 0.01,
                    lifeLength: 0.3,
                    absorbtionFactor: 1,
                    sizeRatio: 1
                },
                reload: .6,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: Math.PI
            }
        ],
        levelRequirement: 45,
        fieldFactor: .75,
        speed: 1.5,
        absorbtionFactor: 1,
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0,
        invisibilityRate: 0,
        preAddon: "spike",
        postAddon: null,
        maxHealth: 50,
        borderWidth: 15,
        sides: 1,
        stats: [
            {
                name: "Movement Speed",
                max: 9
            },
            {
                name: "Reload",
                max: 9
            },
            {
                name: "Bullet Damage",
                max: 9
            },
            {
                name: "Bullet Penetration",
                max: 9
            },
            {
                name: "Bullet Speed",
                max: 9
            },
            {
                name: "Body Damage",
                max: 9
            },
            {
                name: "Max Health",
                max: 9
            },
            {
                name: "Health Regen",
                max: 9
            }
        ]
    },
    {
        id: DevTank.UsainBolt,
        name: "Usain Bolt",
        upgradeMessage: "Use your right mouse button to look further in the direction you're facing",
        // upgrades dont have any affect
        upgrades: [DevTank.Developer],
        barrels: [
            {
                angle: Math.PI,
                delay: 0,
                size: 65,
                offset: 0,
                recoil: 9,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: .1,
                    damage: 1,
                    health: 1,
                    scatterRate: 0.1,
                    lifeLength: .5,
                    absorbtionFactor: 1,
                    sizeRatio: 1
                },
                reload: .2,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: Math.PI
            },
            { // Sniper Barrel (basically)
                angle: 0,
                delay: 0,
                size: 110,
                offset: 0,
                recoil: .2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1.5,
                    damage: 1.5,
                    health: 1,
                    scatterRate: 3,
                    lifeLength: 1,
                    absorbtionFactor: 1,
                    sizeRatio: 1
                },
                reload: 1.5,
                width: 42,
                isTrapezoid: false,
                trapezoidDirection: 0
            }
        ],
        levelRequirement: 45,
        fieldFactor: .9,
        speed: 1.2,
        absorbtionFactor: 1,
        flags: {
            invisibility: false,
            zoomAbility: true,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0,
        invisibilityRate: 0,
        preAddon: null,
        postAddon: null,
        maxHealth: 50,
        borderWidth: 15,
        sides: 1,
        stats: [
            {
                name: "Movement Speed",
                max: 15
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Bullet Damage",
                max: 0
            },
            {
                name: "Bullet Penetration",
                max: 0
            },
            {
                name: "Bullet Speed",
                max: 0
            },
            {
                name: "Body Damage",
                max: 0
            },
            {
                name: "Max Health",
                max: 0
            },
            {
                name: "Health Regen",
                max: 0
            }
        ]
    },
    {
        id: DevTank.BigBoi,
        name: "Big Boi",
        upgradeMessage: "",
        upgrades: [],
        barrels: [
            {
                angle: 0,
                delay: 0,
                size: 95,
                offset: 0,
                recoil: 20,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 0.7,
                    damage: 5,
                    health: 4,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1,
                    sizeRatio: 1
                },
                reload: 1,
                width: 200,
                isTrapezoid: false,
                trapezoidDirection: 0
            }
        ],
        levelRequirement: 45,
        fieldFactor: 1,
        speed: 1,
        absorbtionFactor: 1,
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0,
        invisibilityRate: 0,
        preAddon: null,
        postAddon: null,
        maxHealth: 50,
        borderWidth: 15,
        sides: 1,
        stats: [
            {
                name: "Movement Speed",
                max: 0
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Bullet Damage",
                max: 10
            },
            {
                name: "Bullet Penetration",
                max: 10
            },
            {
                name: "Bullet Speed",
                max: 0
            },
            {
                name: "Body Damage",
                max: 0
            },
            {
                name: "Max Health",
                max: 0
            },
            {
                name: "Health Regen",
                max: 0
            }
        ]
    },
    {
        id: DevTank.Bouncy,
        name: "Bouncy",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0,
        visibilityRateMoving: 0,
        invisibilityRate: 0,
        fieldFactor: 0.7,
        absorbtionFactor: 1,
        speed: 2,
        maxHealth: 100,
        preAddon: null,
        postAddon: null,
        sides: 7,
        borderWidth: 15,
        barrels: [
            {
                angle: PI2/ 7 * 0,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 16,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 1,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 2,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 3,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 4,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 5,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 7 * 6,
                offset: 0,
                size: 100,
                width: 60,
                delay: 0,
                reload: 0.2, 
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: "trapLauncher",
                bullet: {
                    type: "trap",
                    sizeRatio: 1,
                    health: 1,
                    lifeLength: 0.9,
                    damage: 20,
                    speed: 0,
                    scatterRate: .5,
                    absorbtionFactor: 1
                }
            }
        ],
        stats: [
            {
                name: "have fun",
                max: 5
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Bullet Damage",
                max: 0
            },
            {
                name: "Bullet Penetration",
                max: 0
            },
            {
                name: "Bullet Speed",
                max: 0
            },
            {
                name: "Body Damage",
                max: 0
            },
            {
                name: "Max Health",
                max: 0
            },
            {
                name: "Health Regen",
                max: 0
            }
        ]
    },
    {
        id: DevTank.Master,
        name: "Master",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 0.9,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: 0,
                size: 65,
                width: 42,
                delay: 0,
                reload: 3,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 4,
                canControlDrones: true,
                bullet: {
                    type: "minion",
                    sizeRatio: 1,
                    health: 4,
                    damage: 0.7,
                    speed: 0.56,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2 / 3,
                offset: 0,
                size: 65,
                width: 42,
                delay: 0,
                reload: 3,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 4,
                canControlDrones: true,
                bullet: {
                    type: "minion",
                    sizeRatio: 1,
                    health: 4,
                    damage: 0.7,
                    speed: 0.56,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: Math.PI * 4 / 3,
                offset: 0,
                size: 65,
                width: 42,
                delay: 0,
                reload: 3,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 4,
                canControlDrones: true,
                bullet: {
                    type: "minion",
                    sizeRatio: 1,
                    health: 4,
                    damage: 0.7,
                    speed: 0.56,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            }
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Drone Damage",
                max: 7
            },
            {
                name: "Drone Health",
                max: 7
            },
            {
                name: "Drone Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Musketeer,
        name: "Musketeer",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 0.9,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: "autorocket",
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: -21,
                size: 70,
                width: 28,
                delay: 0,
                reload: 0.75,
                recoil: 0.25,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 0.5,
                    damage: 0.25,
                    speed: 1.5,
                    scatterRate: .3,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: 0,
                offset: 21,
                size: 70,
                width: 28,
                delay: 0,
                reload: 0.75,
                recoil: 0.25,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 0.5,
                    damage: 0.25,
                    speed: 1.5,
                    scatterRate: .3,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: 0,
                offset: 0,
                size: 110,
                width: 42,
                delay: 0,
                reload: 1.5,
                recoil: 2,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 0.9,
                    damage: 0.9,
                    speed: 1.5,
                    scatterRate: .3,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Bullet Damage",
                max: 7
            },
            {
                name: "Bullet Penetration",
                max: 7
            },
            {
                name: "Bullet Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Squirrel,
        name: "Squirrel",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: true,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.4,
        visibilityRateMoving: 0.24,
        invisibilityRate: 0.2,
        fieldFactor: 0.9,
        absorbtionFactor: 0,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: "spiesk",
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: Math.PI,
                offset: 0,
                size: 80,
                width: 50,
                delay: 0,
                reload: 0.2,
                recoil: 0.6,
                isTrapezoid: true,
                trapezoidDirection: Math.PI,
                addon: null,
                droneCount: 4294967295,
                canControlDrones: false,
                bullet: {
                    type: "swarm",
                    sizeRatio: 0.5,
                    health: 1,
                    damage: 0.15,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 0.5,
                    absorbtionFactor: 1
                }
            },
            {
                angle: Math.PI * (1 / 9),
                offset: 35,
                size: 180,
                width: 36,
                delay: Infinity, // no shoot
                reload: 1,
                recoil: 0.25,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 1,
                    damage: 1,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: Math.PI * (-1 / 9),
                offset: -35,
                size: 180,
                width: 36,
                delay: Infinity, // no shoot 
                reload: 1,
                recoil: 1,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 1,
                    damage: 1,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: Math.PI / 4,
                offset: -204,
                size: 65,
                width: 70,
                delay: 0,
                reload: 1,
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 2,
                    health: 5,
                    damage: 1,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 0.05,
                    absorbtionFactor: 0
                }
            },
            {
                angle: - Math.PI / 4,
                offset: 204,
                size: 65,
                width: 70,
                delay: 0.5,
                reload: 1,
                recoil: 0,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 2,
                    health: 5,
                    damage: 1,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 0.05,
                    absorbtionFactor: 0
                }
            },
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Bullet Damage",
                max: 7
            },
            {
                name: "Bullet Penetration",
                max: 7
            },
            {
                name: "Bullet Range",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Shotgun,
        name: "Shotgun",
        upgradeMessage: "",
        upgrades: [],
        barrels: [
            {
                angle: 0,
                delay: 0,
                size: 140,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 140,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 140,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 140,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 140,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 120,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 100,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 80,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
            {
                angle: 0,
                delay: 0,
                size: 80,
                offset: 0,
                recoil: 2,
                addon: null,
                bullet: {
                    type: "bullet",
                    speed: 1,
                    damage: 0.2,
                    health: 5,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 0.05,
                    sizeRatio: 0.5
                },
                reload: 3,
                width: 50,
                isTrapezoid: true,
                trapezoidDirection: 0
            },
        ],
        levelRequirement: 45,
        fieldFactor: 1.1,
        speed: 1,
        absorbtionFactor: 1,
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        preAddon: null,
        postAddon: null,
        maxHealth: 50,
        borderWidth: 15,
        sides: 1,
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Bullet Damage",
                max: 7
            },
            {
                name: "Bullet Penetration",
                max: 7
            },
            {
                name: "Bullet Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Flamethrower,
        name: "Flame Thrower",
        upgradeMessage: "",
        levelRequirement: 30,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 1,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: 0,
                size: 110,
                width: 40,
                delay: 0,
                reload: 0.8,
                recoil: 0.5,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "flame",
                    sizeRatio: 1,
                    health: 200,
                    damage: 0.2,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: 0,
                offset: 0,
                size: 75,
                width: 24,
                delay: 0,
                reload: 0.25,
                recoil: 0.25,
                isTrapezoid: true,
                trapezoidDirection: Math.PI,
                addon: null,
                bullet: {
                    type: "flame",
                    sizeRatio: 1,
                    health: 200,
                    damage: 0.2,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
            }
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Flame Damage",
                max: 7
            },
            {
                name: "Flame Penetration",
                max: 0
            },
            {
                name: "Flame Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Builder,
        name: "Builder",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: true,
            zoomAbility: false,
            devOnly: true
        },
        visibilityRateShooting: 0.09,
        visibilityRateMoving: 0,
        invisibilityRate: 0.06,
        fieldFactor: 0.7,
        absorbtionFactor: 0,
        speed: 1.7,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 5,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: 0,
                size: 80,
                width: 50,
                delay: 0,
                reload: 0.5,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "wall",
                    sizeRatio: 0,
                    health: 0,
                    damage: 0,
                    speed: 0,
                    scatterRate: 1,
                    lifeLength: 0,
                    absorbtionFactor: 0
                }
            }
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 0
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Drone Damage",
                max: 0
            },
            {
                name: "Drone Health",
                max: 0
            },
            {
                name: "Drone Speed",
                max: 0
            },
            {
                name: "Body Damage",
                max: 0
            },
            {
                name: "Max Health",
                max: 0
            },
            {
                name: "Health Regen",
                max: 0
            }
        ]
    },
    {
        id: DevTank.Goodbye,
        name: "Goodbye",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: true
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 1,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 1,
        borderWidth: 15,
        barrels: [
            // 7 ac bullets gg
            {angle:-0.15,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:0.15,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:-0.10,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:0.10,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:-0.05,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:0.05,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
            {angle:0,offset:0,size:75,width:42,delay:0,reload:1,recoil:0.1,isTrapezoid:false,trapezoidDirection:0,addon:null,bullet:{type:"bullet",sizeRatio:1,health:300,damage:7,speed:2,scatterRate:1,lifeLength:1,absorbtionFactor:1}},
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Bullet Damage",
                max: 7
            },
            {
                name: "Bullet Penetration",
                max: 7
            },
            {
                name: "Bullet Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Spectator,
        name: "Spectator",
        upgradeMessage: "",
        levelRequirement: 0,
        upgrades: [],
        flags: {
            invisibility: true,
            zoomAbility: false,
            devOnly: true
        },
        visibilityRateShooting: 0.0,
        visibilityRateMoving: 0.0,
        invisibilityRate: 1.0,
        fieldFactor: 0.3,
        absorbtionFactor: 0,
        speed: 3,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 0,
        borderWidth: 15,
        barrels: [],
        stats: [
            {
                name: "Movement Speed",
                max: 0
            },
            {
                name: "Reload",
                max: 0
            },
            {
                name: "Bullet Damage",
                max: 0
            },
            {
                name: "Bullet Penetration",
                max: 0
            },
            {
                name: "Bullet Speed",
                max: 0
            },
            {
                name: "Body Damage",
                max: 0
            },
            {
                name: "Max Health",
                max: 0
            },
            {
                name: "Health Regen",
                max: 0
            }
        ]
    },
    {
        id: DevTank.TheCroc,
        name: "The Croc",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 0.9,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: "pronounced",
        postAddon: null,
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: 0,
                size: 95,
                width: 42,
                delay: 0,
                reload: 0.7,
                recoil: 0.8,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "croc",
                    sizeRatio: 1,
                    health: 1,
                    damage: 1,
                    speed: 0.8,
                    scatterRate: 0,
                    lifeLength: 1.3,
                    absorbtionFactor: 0.1
                }
            }
        ],
        stats: [
            {
                name: "Movement Speed",
                max: 7
            },
            {
                name: "Reload",
                max: 7
            },
            {
                name: "Bullet Damage",
                max: 7
            },
            {
                name: "Bullet Penetration",
                max: 7
            },
            {
                name: "Bullet Speed",
                max: 7
            },
            {
                name: "Body Damage",
                max: 7
            },
            {
                name: "Max Health",
                max: 7
            },
            {
                name: "Health Regen",
                max: 7
            }
        ]
    },
    {
        id: DevTank.Railgun,
        name: "Railgun",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 0.8,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: "pronounced",
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: 0,
                offset: 0,
                size: 130,
                width: 42,
                delay: Infinity,
                reload: 1.75,
                recoil: 5,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1.25,
                    health: 1.5,
                    damage: 1.8,
                    speed: 3,
                    scatterRate: 1,
                    lifeLength: 1.5,
                    absorbtionFactor: 0.7
                }
            },
            {
                angle: 0,
                offset: 0,
                size: 110,
                width: 42,
                delay: Infinity,
                reload: 1.75,
                recoil: 5,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1.25,
                    health: 1.5,
                    damage: 1.8,
                    speed: 3,
                    scatterRate: 1,
                    lifeLength: 1.5,
                    absorbtionFactor: 0.7
                }
            },
            {
                angle: 0,
                offset: 0,
                size: 90,
                width: 42,
                delay: 0,
                reload: 1.75,
                recoil: 5,
                isTrapezoid: false,
                trapezoidDirection: 0,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1.25,
                    health: 1.5,
                    damage: 1.8,
                    speed: 3,
                    scatterRate: 1,
                    lifeLength: 1.5,
                    absorbtionFactor: 0.7
                }
            },
            {
                angle: PI2 - Math.PI * (1 / 36),
                offset: -20,
                size: 150,
                width: 21,
                delay: Infinity,
                reload: 1,
                recoil: 0.75,
                isTrapezoid: true,
                trapezoidDirection: Math.PI,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 0.9,
                    damage: 0.65,
                    speed: 1,
                    scatterRate: 1,
                    lifeLength: 1,
                    absorbtionFactor: 1
                }
	},
            {
                angle: Math.PI * (1 / 36),
                offset: 20,
                size: 150,
                width: 21,
                delay: Infinity,
                reload: 1,
                recoil: 0.75,
                isTrapezoid: true,
                trapezoidDirection: Math.PI,
                addon: null,
                bullet: {
                    type: "bullet",
                    sizeRatio: 1,
                    health: 0.9,
                    damage: 0.65,
                    speed: 1,
                    lifeLength: 1,
                    scatterRate: 1,
                    absorbtionFactor: 1
                }
            }
        ],
       stats: [
            {
                name: "Movement Speed",
                "max": 7
            },
            {
                name: "Reload",
                "max": 7
            },
            {
                name: "Bullet Damage",
                "max": 7
            },
            {
                name: "Bullet Penetration",
                "max": 7
            },
            {
                name: "Bullet Speed",
                "max": 7
            },
            {
                name: "Body Damage",
                "max": 7
            },
            {
                name: "Max Health",
                "max": 7
            },
            {
                name: "Health Regen",
                "max": 7
            }
        ]
    },
    {
        id: DevTank.Commander,
        name: "Commander",
        upgradeMessage: "",
        levelRequirement: 45,
        upgrades: [],
        flags: {
            invisibility: false,
            zoomAbility: false,
            devOnly: false
        },
        visibilityRateShooting: 0.23,
        visibilityRateMoving: 0.08,
        invisibilityRate: 0.03,
        fieldFactor: 0.9,
        absorbtionFactor: 1,
        speed: 1,
        maxHealth: 50,
        preAddon: null,
        postAddon: null,
        sides: 1,
        borderWidth: 15,
        barrels: [
            {
                angle: PI2/ 6 * 1,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 6 * 3,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 6 * 5,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 6 * 2,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 6 * 4,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
            {
                angle: PI2/ 6 * 6,
                offset: 0,
                size: 70,
                width: 42,
                delay: 0,
                reload: 6,
                recoil: 1,
                isTrapezoid: true,
                trapezoidDirection: 0,
                addon: null,
                droneCount: 2,
                canControlDrones: true,
                bullet: {
                    type: "drone",
                    sizeRatio: 1,
                    health: 2,
                    damage: 0.7,
                    speed: 0.8,
                    scatterRate: 1,
                    lifeLength: -1,
                    absorbtionFactor: 1
                }
            },
        ],
       stats: [
            {
                name: "Movement Speed",
                "max": 7
            },
            {
                name: "Reload",
                "max": 7
            },
            {
                name: "Drone Damage",
                "max": 7
            },
            {
                name: "Drone Health",
                "max": 7
            },
            {
                name: "Drone Speed",
                "max": 7
            },
            {
                name: "Body Damage",
                "max": 7
            },
            {
                name: "Max Health",
                "max": 7
            },
            {
                name: "Health Regen",
                "max": 7
            }
        ]
    }
]

export default DevTankDefinitions;
// export const DevTankCount = DevTankDefinitions.reduce((a, b) => b ? a + 1 : a, 1);
`

~src/Const/Enums.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { maxPlayerLevel } from "../config";

/**
 * The IDs for all the team colors, by name.
 */
export const enum Color {
    Border = 0,
    Barrel = 1,
    Tank = 2,
    TeamBlue = 3,
    TeamRed = 4,
    TeamPurple = 5,
    TeamGreen = 6,
    Shiny = 7,
    EnemySquare = 8,
    EnemyTriangle = 9,
    EnemyPentagon = 10,
    EnemyCrasher = 11,
    Neutral = 12,
    ScoreboardBar = 13,
    Box = 14,
    EnemyTank = 15,
    NecromancerSquare = 16,
    Fallen = 17,

    kMaxColors = 18
}

/**
 * The hex color codes of each color (by ID), expressed as an int (0x00RRGGBB)
 */
export const ColorsHexCode: Record<Color, number> = {
    [Color.Border]: 0x555555,
    [Color.Barrel]: 0x999999,
    [Color.Tank]: 0x00B2E1,
    [Color.TeamBlue]: 0x00B2E1,
    [Color.TeamRed]: 0xF14E54,
    [Color.TeamPurple]: 0xBF7FF5,
    [Color.TeamGreen]: 0x00E16E,
    [Color.Shiny]: 0x8AFF69,
    [Color.EnemySquare]: 0xFFE869,
    [Color.EnemyTriangle]: 0xFC7677,
    [Color.EnemyPentagon]: 0x768DFC,
    [Color.EnemyCrasher]: 0xF177DD,
    [Color.Neutral]: 0xFFE869,
    [Color.ScoreboardBar]: 0x43FF91,
    [Color.Box]: 0xBBBBBB,
    [Color.EnemyTank]: 0xF14E54,
    [Color.NecromancerSquare]: 0xFCC376,
    [Color.Fallen]: 0xC0C0C0,
    [Color.kMaxColors]: 0x000000
}

/**
 * The IDs for all the tanks, by name.
 */
export const enum Tank {
    Basic         = 0,
    Twin          = 1,
    Triplet       = 2,
    TripleShot    = 3,
    QuadTank      = 4,
    OctoTank      = 5,
    Sniper        = 6,
    MachineGun    = 7,
    FlankGuard    = 8,
    TriAngle      = 9,
    Destroyer     = 10,
    Overseer      = 11,
    Overlord      = 12,
    TwinFlank     = 13,
    PentaShot     = 14,
    Assassin      = 15,
    ArenaCloser   = 16,
    Necromancer   = 17,
    TripleTwin    = 18,
    Hunter        = 19,
    Gunner        = 20,
    Stalker       = 21,
    Ranger        = 22,
    Booster       = 23,
    Fighter       = 24,
    Hybrid        = 25,
    Manager       = 26,
    Mothership    = 27,
    Predator      = 28,
    Sprayer       = 29,
    Trapper       = 30,
    GunnerTrapper = 32,
    Overtrapper   = 33,
    MegaTrapper   = 34,
    TriTrapper    = 35,
    Smasher       = 36,
    Landmine      = 37,
    AutoGunner    = 39,
    Auto5         = 40,
    Auto3         = 41,
    SpreadShot    = 42,
    Streamliner   = 43,
    AutoTrapper   = 44,
    DominatorD    = 45,
    DominatorG    = 46,
    DominatorT    = 47,
    Battleship    = 48,
    Annihilator   = 49,
    AutoSmasher   = 50,
    Spike         = 51,
    Factory       = 52,
    Skimmer       = 54,
    Rocketeer     = 55
}

/**
 * The IDs for all the stats, by name.
 */
export const enum Stat {
    MovementSpeed = 0,
    Reload = 1,
    BulletDamage = 2,
    BulletPenetration = 3,
    BulletSpeed = 4,
    BodyDamage = 5,
    MaxHealth = 6,
    HealthRegen = 7
}

/**
 * Total Stat Count
 */
export const StatCount = 8;

/**
 * All the indices available on scoreboard - used for type security
 */
export type ValidScoreboardIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

/**
 * Packet headers for the [serverbound packets](https://github.com/ABCxFF/diepindepth/blob/main/protocol/serverbound.md).
 */
export const enum ServerBound {
    Init            = 0x0,
    Input           = 0x1,
    Spawn           = 0x2,
    StatUpgrade     = 0x3,
    TankUpgrade     = 0x4,
    Ping            = 0x5,
    TCPInit         = 0x6,
    ExtensionFound  = 0x7,
    ToRespawn       = 0x8,
    TakeTank        = 0x9
}
/**
 * Packet headers for the [clientbound packets](https://github.com/ABCxFF/diepindepth/blob/main/protocol/clientbound.md).
 */
export const enum ClientBound {
    Update          = 0x0,
    OutdatedClient  = 0x1,
    Compressed      = 0x2,
    Notification    = 0x3,
    ServerInfo      = 0x4,
    Ping            = 0x5,
    PartyCode       = 0x6,
    Accept          = 0x7,
    Achievement     = 0x8,
    InvalidParty    = 0x9,
    PlayerCount     = 0xA,
    ProofOfWork     = 0xB
}

/**
 * Flags sent within the [input packet](https://github.com/ABCxFF/diepindepth/blob/main/protocol/serverbound.md#0x01-input-packet).
 */
export const enum InputFlags {
    leftclick   = 1 << 0,
    up          = 1 << 1,
    left        = 1 << 2,
    down        = 1 << 3,
    right       = 1 << 4,
    godmode     = 1 << 5,
    suicide     = 1 << 6,
    rightclick  = 1 << 7,
    levelup     = 1 << 8,
    gamepad     = 1 << 9,
    switchtank  = 1 << 10,
    adblock     = 1 << 11
}

/**
 * The flag names for the arena field group.
 */
export const enum ArenaFlags {
    noJoining        = 1 << 0,
    showsLeaderArrow = 1 << 1,
    hiddenScores     = 1 << 2,
    gameReadyStart   = 1 << 3,
    canUseCheats     = 1 << 4
}
/**
 * The flag names for the team field group.
 */
export const enum TeamFlags {
    hasMothership = 1 << 0
}
/**
 * The flag names for the camera field group.
 */
export const enum CameraFlags {
    usesCameraCoords      = 1 << 0,
    showingDeathStats     = 1 << 1,
    gameWaitingStart      = 1 << 2
}
/**
 * The flag names for the tsyle field group.
 */
export const enum StyleFlags {
    isVisible          = 1 << 0,
    hasBeenDamaged     = 1 << 1,
    isFlashing         = 1 << 2,
    renderFirst        = 1 << 3,
    isStar             = 1 << 4,
    isCachable         = 1 << 5,
    showsAboveParent   = 1 << 6,
    hasNoDmgIndicator  = 1 << 7
}
/**
 * The flag names for the position field group.
 */
export const enum PositionFlags {
    absoluteRotation    = 1 << 0,
    canMoveThroughWalls = 1 << 1
}
/**
 * The flag names for the physics field group.
 */
export const enum PhysicsFlags {
    isTrapezoid             = 1 << 0,
    showsOnMap              = 1 << 1,
    doChildrenCollision     = 1 << 2,
    noOwnTeamCollision      = 1 << 3,
    isSolidWall             = 1 << 4,
    onlySameOwnerCollision  = 1 << 5,
    isBase                  = 1 << 6,
    _unknown1               = 1 << 7,
    canEscapeArena          = 1 << 8
}
/**
 * The flag names for the barrel field group.
 */
export const enum BarrelFlags {
    hasShot = 1 << 0
}
/**
 * The flag names for the health field group.
 */
export const enum HealthFlags {
    hiddenHealthbar = 1 << 0
}
/**
 * The flag names for the name field group.
 */
export const enum NameFlags {
    hiddenName = 1 << 0,
    highlightedName = 1 << 1
}

/**
 * Credits to CX for discovering this.
 * This is not fully correct but it works up to the decimal (float rounding likely causes this).
 * 
 * `[index: level]->score at level`
 */
export const levelToScoreTable = Array(maxPlayerLevel).fill(0);

for (let i = 1; i < maxPlayerLevel; ++i) {
    levelToScoreTable[i] = levelToScoreTable[i - 1] + (40 / 9 * 1.06 ** (i - 1) * Math.min(31, i));
}

/**
 * Credits to CX for discovering this.
 * This is not fully correct but it works up to the decimal (float rounding likely causes this).
 * 
 * Used for level calculation across the codebase.
 * 
 * `(level)->score at level`
 */
export function levelToScore(level: number): number {
    if (level >= maxPlayerLevel) return levelToScoreTable[maxPlayerLevel - 1];
    if (level <= 0) return 0;

    return levelToScoreTable[level - 1];
}
`

~src/Const/TankDefinitions.json:
`
[
    {
        "id": 0,
        "name": "Tank",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [
            1,
            6,
            7,
            8,
            36
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 1,
        "name": "Twin",
        "upgradeMessage": "",
        "levelRequirement": 15,
        "upgrades": [
            3,
            4,
            13
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.75,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.9,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0.75,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.9,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 2,
        "name": "Triplet",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -26,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.7,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 26,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.7,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.7,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 3,
        "name": "Triple Shot",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            2,
            14,
            42
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -0.7853981633974483,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.7853981633974483,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 4,
        "name": "Quad Tank",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            5,
            40
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 5,
        "name": "Octo Tank",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -0.7853981633974483,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.7853981633974483,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -2.356194490192345,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.356194490192345,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.65,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 6,
        "name": "Sniper",
        "upgradeMessage": "",
        "levelRequirement": 15,
        "upgrades": [
            15,
            11,
            19,
            31
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1.5,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 7,
        "name": "Machine Gun",
        "upgradeMessage": "",
        "levelRequirement": 15,
        "upgrades": [
            10,
            20,
            29
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 0.5,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 8,
        "name": "Flank Guard",
        "upgradeMessage": "",
        "levelRequirement": 15,
        "upgrades": [
            9,
            4,
            13,
            41
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 9,
        "name": "Tri-Angle",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            23,
            24
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.665191429188092,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.6179938779914944,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 10,
        "name": "Destroyer",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            25,
            49,
            54,
            55
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 71.39999999999999,
                "delay": 0,
                "reload": 4,
                "recoil": 15,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 3,
                    "speed": 0.7,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 0.1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 11,
        "name": "Overseer",
        "upgradeMessage": "Use your left mouse button to control the drones",
        "levelRequirement": 30,
        "upgrades": [
            12,
            17,
            26,
            33,
            48,
            52
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 4,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 4,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 12,
        "name": "Overlord",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 13,
        "name": "Twin Flank",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            18,
            48
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 14,
        "name": "Penta Shot",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -0.7853981633974483,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.66,
                "reload": 1,
                "recoil": 0.7,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.55,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.7853981633974483,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.66,
                "reload": 1,
                "recoil": 0.7,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.55,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -0.39269908169872414,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.33,
                "reload": 1,
                "recoil": 0.7,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.55,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.39269908169872414,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0.33,
                "reload": 1,
                "recoil": 0.7,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.55,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.7,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.55,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 15,
        "name": "Assassin",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            22,
            21
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.8,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 120,
                "width": 42,
                "delay": 0,
                "reload": 2,
                "recoil": 3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1.5,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 16,
        "name": "Arena Closer",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 75,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 300,
                    "damage": 7,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 17,
        "name": "Necromancer",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": true,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 4,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 0,
                "canControlDrones": true,
                "bullet": {
                    "type": "necrodrone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.42,
                    "speed": 0.72,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 0,
                "canControlDrones": true,
                "bullet": {
                    "type": "necrodrone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.42,
                    "speed": 0.72,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Drone Count",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 18,
        "name": "Triple Twin",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.0943951023931953,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.0943951023931953,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -2.0943951023931953,
                "offset": -26,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -2.0943951023931953,
                "offset": 26,
                "size": 95,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 19,
        "name": "Hunter",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            28,
            43
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.85,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0,
                "reload": 2.5,
                "recoil": 0.3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1.4,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 56.7,
                "delay": 0.2,
                "reload": 2.5,
                "recoil": 0.3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1.4,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 20,
        "name": "Gunner",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            39,
            32,
            43
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -32,
                "size": 65,
                "width": 25.2,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 32,
                "size": 65,
                "width": 25.2,
                "delay": 0.75,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": -17,
                "size": 85,
                "width": 25.2,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 17,
                "size": 85,
                "width": 25.2,
                "delay": 0.25,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 21,
        "name": "Stalker",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": true,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.8,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 120,
                "width": 42,
                "delay": 0,
                "reload": 2,
                "recoil": 3,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1.5,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 22,
        "name": "Ranger",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.7,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "pronounced",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 120,
                "width": 42,
                "delay": 0,
                "reload": 2,
                "recoil": 3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1.5,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 23,
        "name": "Booster",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.9269908169872414,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0.66,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.356194490192345,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0.66,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.665191429188092,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.33,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.6179938779914944,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.33,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 24,
        "name": "Fighter",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.8,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -1.5707963267948966,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.8,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.665191429188092,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.6179938779914944,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 2.5,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 0.5,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 25,
        "name": "Hybrid",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 71.39999999999999,
                "delay": 0,
                "reload": 4,
                "recoil": 15,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 3,
                    "speed": 0.7,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 0.1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 1.4,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 26,
        "name": "Manager",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": true,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 3,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 8,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.8,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 27,
        "name": "Mothership",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 0.01,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 16,
        "borderWidth": 140,
        "barrels": [
            {
                "angle": 0.19634954084936207,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.5890486225480862,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.9817477042468103,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.3744467859455345,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.7671458676442586,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.159844949342983,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.552544031041707,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.9452431127404313,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.3379421944391554,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.7306412761378795,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.12333992150429,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.516039439535327,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.908738521234052,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 5.301437166600463,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 5.6941366846315,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": true,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 6.086835766330224,
                "offset": 0,
                "size": 60,
                "width": 10.5,
                "delay": 0,
                "reload": 6,
                "recoil": 0,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 2,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 0.7,
                    "speed": 0.48,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 28,
        "name": "Predator",
        "upgradeMessage": "Use your right mouse button to look further in the direction you're facing",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": true,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.85,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0,
                "reload": 3,
                "recoil": 0.3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1.4,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 56.7,
                "delay": 0.2,
                "reload": 3,
                "recoil": 0.3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1.4,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 71.39999999999999,
                "delay": 0.4,
                "reload": 3,
                "recoil": 0.3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.75,
                    "speed": 1.4,
                    "scatterRate": 0.3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 29,
        "name": "Sprayer",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 0.5,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 3,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    null,
    {
        "id": 31,
        "name": "Trapper",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            35,
            32,
            33,
            34,
            44
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 8,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 32,
        "name": "Gunner Trapper",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -16,
                "size": 75,
                "width": 21,
                "delay": 0.66,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 16,
                "size": 75,
                "width": 21,
                "delay": 0.33,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.5,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 60,
                "width": 54.6,
                "delay": 0,
                "reload": 3,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 8,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 33,
        "name": "Overtrapper",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 8,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.0943951023931953,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 1,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 1.4,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.1887902047863905,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 6,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 1,
                "canControlDrones": false,
                "bullet": {
                    "type": "drone",
                    "sizeRatio": 1,
                    "health": 1.4,
                    "damage": 0.7,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 34,
        "name": "Mega Trapper",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 54.6,
                "delay": 0,
                "reload": 3.3,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 1.28,
                    "health": 3.2,
                    "damage": 1.6,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 8,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 35,
        "name": "Tri-Trapper",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.0943951023931953,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.1887902047863905,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 36,
        "name": "Smasher",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            38,
            50,
            51
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "smasher",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 10
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 10
            },
            {
                "name": "Max Health",
                "max": 10
            },
            {
                "name": "Health Regen",
                "max": 10
            }
        ]
    },
    null,
    {
        "id": 38,
        "name": "Landmine",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": true,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0,
        "visibilityRateMoving": 0.16,
        "invisibilityRate": 0.003,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "landmine",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 10
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 10
            },
            {
                "name": "Max Health",
                "max": 10
            },
            {
                "name": "Health Regen",
                "max": 10
            }
        ]
    },
    {
        "id": 39,
        "name": "Auto Gunner",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "autoturret",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -32,
                "size": 65,
                "width": 25.2,
                "delay": 0.5,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 32,
                "size": 65,
                "width": 25.2,
                "delay": 0.75,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": -17,
                "size": 85,
                "width": 25.2,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 17,
                "size": 85,
                "width": 25.2,
                "delay": 0.25,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 0.45,
                    "damage": 0.5,
                    "speed": 1.1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 40,
        "name": "Auto 5",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "auto5",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 41,
        "name": "Auto 3",
        "upgradeMessage": "",
        "levelRequirement": 30,
        "upgrades": [
            40,
            39
        ],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "auto3",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 42,
        "name": "Spread Shot",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 1.3089969389957472,
                "offset": 0,
                "size": 65,
                "width": 29.4,
                "delay": 0.833325,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -1.3089969389957472,
                "offset": 0,
                "size": 65,
                "width": 29.4,
                "delay": 0.833325,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.0471975511965976,
                "offset": 0,
                "size": 71,
                "width": 29.4,
                "delay": 0.666675,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -1.0471975511965976,
                "offset": 0,
                "size": 71,
                "width": 29.4,
                "delay": 0.666675,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.7853981633974483,
                "offset": 0,
                "size": 77,
                "width": 29.4,
                "delay": 0.5,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -0.7853981633974483,
                "offset": 0,
                "size": 77,
                "width": 29.4,
                "delay": 0.5,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.5235987755982988,
                "offset": 0,
                "size": 83,
                "width": 29.4,
                "delay": 0.333325,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -0.5235987755982988,
                "offset": 0,
                "size": 83,
                "width": 29.4,
                "delay": 0.333325,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.2617993877991494,
                "offset": 0,
                "size": 89,
                "width": 29.4,
                "delay": 0.166675,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": -0.2617993877991494,
                "offset": 0,
                "size": 89,
                "width": 29.4,
                "delay": 0.166675,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 0.6,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 42,
                "delay": 0,
                "reload": 2,
                "recoil": 0.1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 1,
                    "damage": 1,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 43,
        "name": "Streamliner",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.85,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 110,
                "width": 42,
                "delay": 0,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1.1,
                    "scatterRate": 0.3,
                    "lifeLength": 0.8,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 100,
                "width": 42,
                "delay": 0.2,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1.1,
                    "scatterRate": 0.3,
                    "lifeLength": 0.8,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 90,
                "width": 42,
                "delay": 0.4,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1.1,
                    "scatterRate": 0.3,
                    "lifeLength": 0.8,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 42,
                "delay": 0.6,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1.1,
                    "scatterRate": 0.3,
                    "lifeLength": 0.8,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0.8,
                "reload": 1,
                "recoil": 0.2,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.2,
                    "speed": 1.1,
                    "scatterRate": 0.3,
                    "lifeLength": 0.8,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 44,
        "name": "Auto Trapper",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "autoturret",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 42,
                "delay": 0,
                "reload": 1.5,
                "recoil": 1,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 2,
                    "damage": 1,
                    "speed": 2,
                    "scatterRate": 1,
                    "lifeLength": 8,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 45,
        "name": "Dominator",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 6000,
        "preAddon": "dombase",
        "postAddon": "dompronounced",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 34.99965,
                "delay": 0.001,
                "reload": 3,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 100,
                    "damage": 10,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 0.1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 0
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 0
            },
            {
                "name": "Max Health",
                "max": 0
            },
            {
                "name": "Health Regen",
                "max": 0
            }
        ]
    },
    {
        "id": 46,
        "name": "Dominator",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 6000,
        "preAddon": "dombase",
        "postAddon": "dompronounced",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": -6,
                "size": 75,
                "width": 17.50035,
                "delay": 0.666,
                "reload": 0.3,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.6,
                    "health": 5,
                    "damage": 1,
                    "speed": 1.2,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 6,
                "size": 75,
                "width": 17.50035,
                "delay": 0.333,
                "reload": 0.3,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.6,
                    "health": 5,
                    "damage": 1,
                    "speed": 1.2,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 17.50035,
                "delay": 0.001,
                "reload": 0.3,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 0.6,
                    "health": 5,
                    "damage": 1,
                    "speed": 1.2,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 0
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 0
            },
            {
                "name": "Max Health",
                "max": 0
            },
            {
                "name": "Health Regen",
                "max": 0
            }
        ]
    },
    {
        "id": 47,
        "name": "Dominator",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 6000,
        "preAddon": "dombase",
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 0.7853981633974483,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 2.356194490192345,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.141592653589793,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 3.9269908169872414,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.71238898038469,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 5.497787143782138,
                "offset": 0,
                "size": 60,
                "width": 21,
                "delay": 0,
                "reload": 1.5,
                "recoil": 0,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": "trapLauncher",
                "forceFire": true,
                "bullet": {
                    "type": "trap",
                    "sizeRatio": 0.8,
                    "health": 20,
                    "damage": 3,
                    "speed": 4,
                    "scatterRate": 1,
                    "lifeLength": 3.2,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 0
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 0
            },
            {
                "name": "Max Health",
                "max": 0
            },
            {
                "name": "Health Regen",
                "max": 0
            }
        ]
    },
    {
        "id": 48,
        "name": "Battleship",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 1.5707963267948966,
                "offset": -20,
                "size": 75,
                "width": 29.4,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "droneCount": 4294967295,
                "canControlDrones": false,
                "bullet": {
                    "type": "swarm",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.15,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.71238898038469,
                "offset": -20,
                "size": 75,
                "width": 29.4,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "droneCount": 4294967295,
                "canControlDrones": false,
                "bullet": {
                    "type": "swarm",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.15,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 1.5707963267948966,
                "offset": 20,
                "size": 75,
                "width": 29.4,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "droneCount": 4294967295,
                "canControlDrones": true,
                "bullet": {
                    "type": "swarm",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.15,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            },
            {
                "angle": 4.71238898038469,
                "offset": 20,
                "size": 75,
                "width": 29.4,
                "delay": 0,
                "reload": 1,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "droneCount": 4294967295,
                "canControlDrones": true,
                "bullet": {
                    "type": "swarm",
                    "sizeRatio": 0.7,
                    "health": 1,
                    "damage": 0.15,
                    "speed": 1,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 49,
        "name": "Annihilator",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 95,
                "width": 96.6,
                "delay": 0,
                "reload": 4,
                "recoil": 17,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "bullet",
                    "sizeRatio": 1,
                    "health": 2,
                    "damage": 3,
                    "speed": 0.7,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 0.05
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 50,
        "name": "Auto Smasher",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "autosmasher",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 10
            },
            {
                "name": "Reload",
                "max": 10
            },
            {
                "name": "Bullet Damage",
                "max": 10
            },
            {
                "name": "Bullet Penetration",
                "max": 10
            },
            {
                "name": "Bullet Speed",
                "max": 10
            },
            {
                "name": "Body Damage",
                "max": 10
            },
            {
                "name": "Max Health",
                "max": 10
            },
            {
                "name": "Health Regen",
                "max": 10
            }
        ]
    },
    {
        "id": 51,
        "name": "Spike",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": "spike",
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 10
            },
            {
                "name": "Reload",
                "max": 0
            },
            {
                "name": "Bullet Damage",
                "max": 0
            },
            {
                "name": "Bullet Penetration",
                "max": 0
            },
            {
                "name": "Bullet Speed",
                "max": 0
            },
            {
                "name": "Body Damage",
                "max": 10
            },
            {
                "name": "Max Health",
                "max": 10
            },
            {
                "name": "Health Regen",
                "max": 10
            }
        ]
    },
    {
        "id": 52,
        "name": "Factory",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 4,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 70,
                "width": 42,
                "delay": 0,
                "reload": 3,
                "recoil": 1,
                "isTrapezoid": true,
                "trapezoidDirection": 0,
                "addon": null,
                "droneCount": 6,
                "canControlDrones": true,
                "bullet": {
                    "type": "minion",
                    "sizeRatio": 1,
                    "health": 4,
                    "damage": 0.7,
                    "speed": 0.56,
                    "scatterRate": 1,
                    "lifeLength": -1,
                    "absorbtionFactor": 1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Drone Damage",
                "max": 7
            },
            {
                "name": "Drone Health",
                "max": 7
            },
            {
                "name": "Drone Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 53,
        "name": "",
        "upgradeMessage": "",
        "levelRequirement": 0,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": true
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 1,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": null,
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 54,
        "name": "Skimmer",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": "launcher",
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 71.39999999999999,
                "delay": 0,
                "reload": 4,
                "recoil": 3,
                "isTrapezoid": false,
                "trapezoidDirection": 0,
                "addon": null,
                "bullet": {
                    "type": "skimmer",
                    "sizeRatio": 1,
                    "health": 3,
                    "damage": 1,
                    "speed": 0.5,
                    "scatterRate": 1,
                    "lifeLength": 1.3,
                    "absorbtionFactor": 0.1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    },
    {
        "id": 55,
        "name": "Rocketeer",
        "upgradeMessage": "",
        "levelRequirement": 45,
        "upgrades": [],
        "flags": {
            "invisibility": false,
            "zoomAbility": false,
            "canClaimSquares": false,
            "devOnly": false
        },
        "visibilityRateShooting": 0.23,
        "visibilityRateMoving": 0.08,
        "invisibilityRate": 0.03,
        "fieldFactor": 0.9,
        "absorbtionFactor": 1,
        "speed": 1,
        "maxHealth": 50,
        "preAddon": "launcher",
        "postAddon": null,
        "sides": 1,
        "borderWidth": 15,
        "barrels": [
            {
                "angle": 0,
                "offset": 0,
                "size": 80,
                "width": 52.5,
                "delay": 0,
                "reload": 4,
                "recoil": 3,
                "isTrapezoid": true,
                "trapezoidDirection": 3.141592653589793,
                "addon": null,
                "bullet": {
                    "type": "rocket",
                    "sizeRatio": 1,
                    "health": 5,
                    "damage": 1,
                    "speed": 0.3,
                    "scatterRate": 1,
                    "lifeLength": 1,
                    "absorbtionFactor": 0.1
                }
            }
        ],
        "stats": [
            {
                "name": "Movement Speed",
                "max": 7
            },
            {
                "name": "Reload",
                "max": 7
            },
            {
                "name": "Bullet Damage",
                "max": 7
            },
            {
                "name": "Bullet Penetration",
                "max": 7
            },
            {
                "name": "Bullet Speed",
                "max": 7
            },
            {
                "name": "Body Damage",
                "max": 7
            },
            {
                "name": "Max Health",
                "max": 7
            },
            {
                "name": "Health Regen",
                "max": 7
            }
        ]
    }
]
`

~src/Const/TankDefinitions.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import DevTankDefinitions, { DevTank } from "./DevTankDefinitions";
import { Tank, Color } from "./Enums";
import _TankDefinitions from "./TankDefinitions.json";

/** The types of post addons that exist in the game, by their id. */
export type postAddonId = "dompronounced" | "auto5" | "auto3" | "autosmasher" | "spike" | "pronounced" | "smasher" | "landmine" | "autoturret" | "weirdspike" | "auto2" | "auto7" | "autorocket" | "spiesk"
/** The types of post addons that exist in the game, by their id. */
export type preAddonId = "dombase" | "launcher"
/** A joint list of all post addon ids and pre addon ids. */
export type addonId = preAddonId | postAddonId;

/** The types of projectiles in the game */
export type projectileId = "bullet" | "drone" | "trap" | "necrodrone" | "minion" | "skimmer" | "rocket" | "swarm" | "flame" | "wall" | "croc";

/** The types of barrel addons that exist in the game */
export type barrelAddonId = "trapLauncher" | "purplebarrel";

/** Increase in opacity when taking damage. */
export const visibilityRateDamage = 0.2;

/**
 * Format that the game stores bullet definitions in its memory.
 */
export interface BulletDefinition {
    /** The type of the bullet that the barrel shoots. */
    type: projectileId;
    /** Size of the bullet shot out of the barrel in relation to the barrel's size. */
    sizeRatio: number;
    /** Used to calculate the health of the bullet that the barrel shoots. */
    health: number;
    /** Used to calculate the damage of the bullet that the barrel shoots. */
    damage: number;
    /** Used to calculate the speed of the bullet that the barrel shoots. */
    speed: number;
    /** Used to calculate the scattering rate / spread of the bullets. */
    scatterRate: number;
    /** Used to calculate the life length of a bullet that the barrel shoots. */
    lifeLength: number;
    /** Knockback factor field of the bullet */
    absorbtionFactor: number;
    /** Projectile color - by default this is set to parent's body color. */
    color?: Color;
    /** Overrides number of sides for projectile. */
    sides?: number;
}

/**
 * Format that the game stores barrel definitions in its memory.
 */
export interface BarrelDefinition {
    /** The angle that the barrel is rotated towards, from the tank's body.  */
    angle: number;
    /** The x offset of the barrel (think of Twin's barrels for example) at base radius (50).  */
    offset: number;
    /** The y offset of the barrel (distance from the tanks main body) at base radius (50). Will have no effect on clientside tankrendering.*/
    distance?: number;
    /** The size of the barrel. Think of Sniper, the longer side is the size.  */
    size: number;
    /** The width of the barrel. Think of Sniper, the shorter side is the width. Width is used to determine bullet size */
    width: number;
    /** Delay between firing cycle. Think of Octo: half of the barrels have 0 delay, and the other half have 0.5 delay.  */
    delay: number;
    /** Used to calculate the reload of a barrel. */
    reload: number;
    /** Used to calculate the recoil of a barrel's shot. */
    recoil: number;
    /** Example: Machine Gun's barrel is a trapezoid. */
    isTrapezoid: boolean;
    /** Direction that the trapezoid is facing. Machine Gun's `trapezoidalDirection` is `0`, and Stalker's is `π`. */
    trapezoidDirection: number;
    /** Barrel addon. As of now only the trapper addon exists. */
    addon: barrelAddonId | null;
    /** The maximum numbers of drones the barrel can spawn - only present if `bullet.type` === 'drone'. */
    droneCount?: number;
    /** Whether or not the drones are controllable - only present if `bullet.type` === 'drone'. */
    canControlDrones?: boolean;
    /** Whether or not the barrel should always shoot (Trapper Dominator, Defender). */
    forceFire?: boolean;
    /** Barrel color - by default this is set to the 'Barrel' color id. */
    color?: Color;
    /** The definition of the bullet that is shot from the barrel. */
    bullet: BulletDefinition;
}
/**
 * Format that the game stores stat definitions in its memory.
 */
export interface StatDefinition {
    /** The name of the stat. */
    name: string;
    /** The stat level limit. */
    max: number;
}
/**
 * Format that the game stores tank definitions in its memory.
 */
export interface TankDefinition {
    /** The id of the tank. */
    id: Tank | DevTank;
    /** The name of the tank. */
    name: string;
    /** If not empty, the client is sent a notification with this message when it upgrades to this tank. */
    upgradeMessage: string;
    /** The levels required to upgrade to this tank. */
    levelRequirement: number;
    /** The tanks this tank can upgrade to. */
    upgrades: (Tank | DevTank)[];
    /** Boolean flags about the tank. */
    flags: {
        /** If the tank can go invisible. */
        invisibility: boolean;
        /** If the tank has a Predator-like zoom ability. */
        zoomAbility: boolean;
        /** If the tank can claim squares by killing them (necro). */
        canClaimSquares?: boolean;
        /** If the tank requires devmode to access (unused). */
        devOnly: boolean;
        /** If the tank should be rendered as a star (eg. traps are stars with 3 sides). */
        displayAsStar?: boolean;
        /** If the tank should be rendered as a trapezoid (eg. drone barrels are trapezoids), sides needs to be set to 2 for this to take effect. */
        displayAsTrapezoid?: boolean;
    },
    /** How much the opacity increases per tick while shooting. */
    visibilityRateShooting: number;
    /** How much the opacity increases per tick while moving. */
    visibilityRateMoving: number;
    /** How much does the opacity decrease per tick. */
    invisibilityRate: number;
    /** Used to determine the FOV of a tank. */
    fieldFactor: number;
    /** The speed of the tank. */
    speed: number;
    /** The absorbtionFactor (field) of the tank. */
    absorbtionFactor: number;
    /** The base max health of the tank. */
    maxHealth: number;
    /** The addon, if not empty, which is built before the barrels. */
    preAddon: addonId | null;
    /** The addon, if not empty, which is built after the barrels. */
    postAddon: addonId | null;
    /** The sides of the tank's body. */
    sides: number;
    /** The ratio used for size to width calculation, only takes effect when sides is 2 (rectangle). */
    widthRatio?: number;
    /** The border width of the tank's body. */
    borderWidth: number;
    /** Can be used to override the tank body's base size. */
    baseSizeOverride?: number;
    /** The tank's barrels. */
    barrels: BarrelDefinition[];
    /** The tank's stat names and limits. */
    stats: StatDefinition[];
}

/**
 * List of all tank definitions.
 */
const TankDefinitions = _TankDefinitions as (TankDefinition | null)[] & Record<Tank, TankDefinition>;
export default TankDefinitions;

/**
 * The count of all existing tanks (some in tank definitions are null).
 * Used for tank xor generation.
 */
export const TankCount = TankDefinitions.reduce((a, b) => b ? a + 1 : a, 0);

/**
 * A function used to retrieve both tank and dev tank definitions from their id.
 * Negative IDs are used to index dev tanks, whereas positive are used to index normal tanks. 
 */
export const getTankById = function (id: number): TankDefinition | null {
    return (id < 0 ? DevTankDefinitions[~id] : TankDefinitions[id]) || null;
}

export const getTankByName = function (tankName: string): TankDefinition | null {
    return TankDefinitions.find(tank => tank && tank.name === tankName) || DevTankDefinitions.find(tank => tank && tank.name === tankName) || null;
}
`

~src/Entity/Boss/AbstractBoss.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";

import { ClientBound, Color, PositionFlags } from "../../Const/Enums";
import { VectorAbstract } from "../../Physics/Vector";
import { AI, AIState, Inputs } from "../AI";
import { NameGroup } from "../../Native/FieldGroups";
import LivingEntity from "../Live";
import TankBody from "../Tank/TankBody";
import { CameraEntity } from "../../Native/Camera";


/**
 * Possible targets for movement
 */
enum Target {
    None = -1,
    BottomRight = 0,
    TopRight = 1,
    TopLeft = 2,
    BottomLeft = 3
}

class BossMovementControl {
    /** Current target on the map. */
    public target: Target = Target.None;

    /** The boss thats movement is being controlled. */
    public boss: AbstractBoss;

    public constructor(boss: AbstractBoss) {
        this.boss = boss;
    }
    
    public moveBoss() {
        const { x, y } = this.boss.positionData.values;
        if (this.target === Target.None) {
            if (x >= 0 && y >= 0) {
                this.target = Target.BottomRight;
            } else if (x <= 0 && y >= 0) {
                this.target = Target.BottomLeft;
            } else if (x <= 0 && y <= 0) {
                this.target = Target.TopLeft;
            }else /*if (x >= 0 && y <= 0)*/ {
                this.target = Target.TopRight;
            }
        }

        const target: VectorAbstract = this.target === Target.BottomRight ?
            { x: 3 * this.boss.game.arena.arenaData.values.rightX / 4, y: 3 * this.boss.game.arena.arenaData.values.bottomY / 4} : this.target === Target.BottomLeft ?
            { x: 3 * this.boss.game.arena.arenaData.values.leftX / 4, y: 3 * this.boss.game.arena.arenaData.values.bottomY / 4} : this.target === Target.TopLeft ? 
            { x: 3 * this.boss.game.arena.arenaData.values.leftX / 4, y: 3 * this.boss.game.arena.arenaData.values.topY / 4} :
            { x: 3 * this.boss.game.arena.arenaData.values.rightX / 4, y: 3 * this.boss.game.arena.arenaData.values.topY  / 4};

        // Target becomes delta now
        target.x = (target.x - x);
        target.y = (target.y - y);
        const dist = target.x ** 2 + target.y ** 2;
        if (dist < 90_000 /* 300 ** 2*/) this.target = (this.target + 1) % 4;
        else {
            const angle = Math.atan2(target.y, target.x)
            this.boss.inputs.movement.x = Math.cos(angle);
            this.boss.inputs.movement.y = Math.sin(angle);
        }
    }
}

/**
 * Class which represents all the bosses.
 */
export default class AbstractBoss extends LivingEntity {
    /** Always existant name field group, present in all bosses. */
    public nameData: NameGroup = new NameGroup(this);
    /** Alternate name, eg Guardian and Guardian of the Pentagons to appear in notifications" */
    public altName: string | null = null;
    /** The reload time calculation property. Used for calculating reload of barrels. */
    public reloadTime = 15;

    /** The AI that controls how this boss moves. */
    public ai: AI;
    /** The AI's inputs (for fullfilling BarrelBase typedef). */
    public inputs: Inputs;

    /** The boss's "camera entity" */
    public cameraEntity: CameraEntity = this as unknown as CameraEntity;

    /** Whether or not the broadcast message was sent "The ___ has spawned!" */
    private hasBeenWelcomed = false;

    /** List of the boss barrels. */
    protected barrels: Barrel[] = [];
    /** The speed to maintain during movement. */
    public movementSpeed = 0.5;

    /** The thing that controls map wide movement. */
    protected movementControl = new BossMovementControl(this)

    public constructor(game: GameServer) {
        super(game);

        const {x, y} = this.game.arena.findSpawnLocation()
        this.positionData.values.x = x;
        this.positionData.values.y = y;
        
        this.relationsData.values.team = this.cameraEntity;

        this.physicsData.values.absorbtionFactor = 0.05;
        this.positionData.values.flags |= PositionFlags.absoluteRotation;
        this.scoreReward = 30000 * this.game.arena.shapeScoreRewardMultiplier;
        this.damagePerTick = 60;

        this.ai = new AI(this);
        this.ai.viewRange = 2000;
        this.ai['_findTargetInterval'] = 0;
        this.inputs = this.ai.inputs;

        // default ehColor
        this.styleData.values.color = Color.Fallen;

        this.physicsData.values.sides = 1;
        this.physicsData.values.size = 50 * Math.pow(1.01, 75 - 1);

        this.reloadTime = 15 * Math.pow(0.914, 7);

        this.healthData.values.health = this.healthData.values.maxHealth = 3000;
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    // For map wide movement
    protected moveAroundMap() {
        this.movementControl.moveBoss();
    }

    /** See LivingEntity.onDeath
     * This broadcasts when people kill it
     * Will set game.arena.boss to null, so that the next boss can spawn
     */
    public onDeath(killer: LivingEntity) {
        // Reset arena.boss
        if (this.game.arena.boss === this) this.game.arena.boss = null;

        const killerName = (killer instanceof TankBody && killer.nameData.values.name) || "an unnamed tank"
        this.game.broadcast()
            .u8(ClientBound.Notification)
            .stringNT(`The ${this.altName || this.nameData.values.name} has been defeated by ${killerName}!`)
            .u32(0x000000)
            .float(10000)
            .stringNT("").send();
    }

    public tick(tick: number) {
        // Force
        if (this.inputs !== this.ai.inputs) this.inputs = this.ai.inputs;

        this.ai.movementSpeed = this.movementSpeed;
        
        if (this.ai.state !== AIState.possessed) this.moveAroundMap();
        else {
            const x = this.positionData.values.x,
                  y = this.positionData.values.y;

            this.positionData.angle = Math.atan2(this.ai.inputs.mouse.y - y, this.ai.inputs.mouse.x - x);
        }
        this.accel.add({
            x: this.inputs.movement.x * this.movementSpeed,
            y: this.inputs.movement.y * this.movementSpeed,
        });
        this.inputs.movement.set({
            x: 0,
            y: 0
        });

        this.regenPerTick = this.healthData.values.maxHealth / 25000;

        // So the name can be set in an extended constructor
        if (!this.hasBeenWelcomed) {
            let message = "An unnamed boss has spawned!"
            if (this.nameData.values.name) {
                message = `The ${this.altName || this.nameData.values.name} has spawned!`;
            }
            this.game.broadcast().u8(ClientBound.Notification).stringNT(message).u32(0x000000).float(10000).stringNT("").send();
            this.hasBeenWelcomed = true;
        }

        super.tick(tick);
    }
}
`

~src/Entity/Boss/Defender.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";
import AutoTurret, { AutoTurretDefinition } from "../Tank/AutoTurret";
import AbstractBoss from "./AbstractBoss";

import { Color, Tank, PositionFlags } from "../../Const/Enums";
import { AIState } from "../AI";

import { BarrelDefinition } from "../../Const/TankDefinitions";
import { PI2 } from "../../util";

/**
 * Definitions (stats and data) of the mounted turret on Defender
 *
 * Defender's gun
 */
const MountedTurretDefinition: BarrelDefinition = {
    ...AutoTurretDefinition,
    bullet: {
        ...AutoTurretDefinition.bullet,
        speed: 2.3,
        damage: 0.75,
        health: 5.75,
        color: Color.Neutral
    }
};

/**
 * Definitions (stats and data) of the trap launcher on Defender
 */
const DefenderDefinition: BarrelDefinition = {
    angle: 0,
    offset: 0,
    size: 120,
    width: 71.4,
    delay: 0,
    reload: 4,
    recoil: 2,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: "trapLauncher",
    forceFire: true,
    bullet: {
        type: "trap",
        sizeRatio: 0.8,
        health: 12.5,
        damage: 4,
        speed: 5,
        scatterRate: 1,
        lifeLength: 8,
        absorbtionFactor: 1,
        color: Color.Neutral
    }
}

// The size of a Defender by default
const DEFENDER_SIZE = 150;

/**
 * Class which represents the boss "Defender"
 */
export default class Defender extends AbstractBoss {

    /** Defender's trap launchers */
    private trappers: Barrel[] = [];
    /** See AbstractBoss.movementSpeed */
    public movementSpeed = 0.35;

    public constructor(game: GameServer) {
        super(game);
        this.nameData.values.name = 'Defender';
        this.styleData.values.color = Color.EnemyTriangle;
        this.relationsData.values.team = this.game.arena;
        this.physicsData.values.size = DEFENDER_SIZE * Math.SQRT1_2;
        this.ai.viewRange = 0;

        this.physicsData.values.sides = 3;

        for (let i = 0; i < 3; ++i) {
            // Add trap launcher
            this.trappers.push(new Barrel(this, {
                ...DefenderDefinition,
                angle: PI2 * ((i / 3) - 1 / 6)
            }));

            // TODO:
            // Maybe make this into a class of itself - DefenderAutoTurret
            const base = new AutoTurret(this, MountedTurretDefinition);
            base.influencedByOwnerInputs = true;

            const angle = base.ai.inputs.mouse.angle = PI2 * (i / 3);

            base.positionData.values.y = this.physicsData.values.size * Math.sin(angle) * 0.6;
            base.positionData.values.x = this.physicsData.values.size * Math.cos(angle) * 0.6;

            base.physicsData.values.flags |= PositionFlags.absoluteRotation;

            const tickBase = base.tick;
            base.tick = (tick: number) => {
                base.positionData.y = this.physicsData.values.size * Math.sin(angle) * 0.6;
                base.positionData.x = this.physicsData.values.size * Math.cos(angle) * 0.6;

                tickBase.call(base, tick);
            }
        }
    }

    public get sizeFactor() {
        return (this.physicsData.values.size / Math.SQRT1_2) / DEFENDER_SIZE;
    }

    public tick(tick: number) {
       super.tick(tick);

        if (this.ai.state !== AIState.possessed) {
            this.positionData.angle += this.ai.passiveRotation * Math.PI * Math.SQRT1_2;
        }
    }
}
`

~src/Entity/Boss/FallenBooster.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";
import TankDefinitions from "../../Const/TankDefinitions";
import AbstractBoss from "./AbstractBoss";

import { Tank } from "../../Const/Enums";
import { AIState } from "../AI";

/**
 * Class which represents the boss "FallenBooster"
 */
export default class FallenBooster extends AbstractBoss {
    /** The speed to maintain during movement. */
    public movementSpeed = 1;

    public constructor(game: GameServer) {
        super(game);

        this.nameData.values.name = 'Fallen Booster';
        for (const barrelDefinition of TankDefinitions[Tank.Booster].barrels) {

            const def = Object.assign({}, barrelDefinition, {});
            def.bullet = Object.assign({}, def.bullet, { speed: 1.7, health: 6.25 });
            this.barrels.push(new Barrel(this, def));
        }
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    protected moveAroundMap() {
      const x = this.positionData.values.x,
      y = this.positionData.values.y
        if (this.ai.state === AIState.idle) {
            super.moveAroundMap();
            this.positionData.angle = Math.atan2(this.inputs.movement.y, this.inputs.movement.x)
        } else {
            this.positionData.angle = Math.atan2(this.ai.inputs.mouse.y - y, this.ai.inputs.mouse.x - x)
        }
    }

    public tick(tick: number) {
        super.tick(tick);
    }
}
`

~src/Entity/Boss/FallenOverlord.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";
import TankDefinitions from "../../Const/TankDefinitions";
import AbstractBoss from "./AbstractBoss";

import { Tank } from "../../Const/Enums";
import { AIState } from "../AI";

/**
 * Class which represents the boss "FallenOverlord"
 */
export default class FallenOverlord extends AbstractBoss {
    public constructor(game: GameServer) {
        super(game);

        this.nameData.values.name = 'Fallen Overlord';

        for (const barrelDefinition of TankDefinitions[Tank.Overlord].barrels) {

            const def = Object.assign({}, barrelDefinition, { droneCount: 7, reload: 0.25 });
            def.bullet = Object.assign({}, def.bullet, { sizeRatio: 0.5, speed: 1.7, damage: 0.5, health: 12.5 });
            this.barrels.push(new Barrel(this, def));
        }
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    public tick(tick: number) {
        super.tick(tick);

        if (this.ai.state !== AIState.possessed) {
            this.positionData.angle += this.ai.passiveRotation;
        }
    }
}
`

~src/Entity/Boss/Guardian.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";
import AbstractBoss from "./AbstractBoss";

import { Color, Tank } from "../../Const/Enums";
import { AIState } from "../AI";
import { BarrelDefinition } from "../../Const/TankDefinitions";

const GuardianSpawnerDefinition: BarrelDefinition = {
    angle: Math.PI,
    offset: 0,
    size: 100,
    width: 71.4,
    delay: 0,
    reload: 0.25,
    recoil: 1,
    isTrapezoid: true,
    trapezoidDirection: 0,
    addon: null,
    droneCount: 24,
    canControlDrones: true,
    bullet: {
        type: "drone",
        sizeRatio: 21 / (71.4 / 2),
        health: 12.5,
        damage: 0.5,
        speed: 1.7,
        scatterRate: 1,
        lifeLength: 1.5,
        absorbtionFactor: 1
    }
};

// The size of Guardian by default
const GUARDIAN_SIZE = 135;

/**
 * Class which represents the boss "Guardian"
 */
export default class Guardian extends AbstractBoss {
    public constructor(game: GameServer) {
        super(game);

        this.nameData.values.name = 'Guardian';
        this.altName = 'Guardian of the Pentagons';
        this.styleData.values.color = Color.EnemyCrasher;
        this.relationsData.values.team = this.game.arena;
        this.physicsData.values.size = GUARDIAN_SIZE * Math.SQRT1_2;
        this.physicsData.values.sides = 3;

        this.barrels.push(new Barrel(this, GuardianSpawnerDefinition));
    }

    public get sizeFactor() {
        return (this.physicsData.values.size / Math.SQRT1_2) / GUARDIAN_SIZE;
    }

    protected moveAroundMap() {
        super.moveAroundMap();
        this.positionData.angle = Math.atan2(this.inputs.movement.y, this.inputs.movement.x)
    }

    public tick(tick: number) {
        super.tick(tick);
    }
}
`

~src/Entity/Boss/Summoner.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import Barrel from "../Tank/Barrel";
import AbstractBoss from "./AbstractBoss";

import { Color, Tank } from "../../Const/Enums";
import { AIState } from "../AI";

import { BarrelDefinition } from "../../Const/TankDefinitions";
import { PI2 } from "../../util";

const SummonerSpawnerDefinition: BarrelDefinition = {
    angle: Math.PI,
    offset: 0,
    size: 135,
    width: 71.4,
    delay: 0,
    reload: 0.25,
    recoil: 1,
    isTrapezoid: true,
    trapezoidDirection: 0,
    addon: null,
    droneCount: 7,
    canControlDrones: true,
    bullet: {
        type: "drone",
        sizeRatio: 55 * Math.SQRT1_2 / (71.4 / 2),
        health: 12.5,
        damage: 0.5,
        speed: 1.7,
        scatterRate: 1,
        lifeLength: -1,
        absorbtionFactor: 1,
        color: Color.NecromancerSquare,
        sides: 4
    }
};

const SUMMONER_SIZE = 150;
/**
 * Class which represents the boss "Summoner"
 */
export default class Summoner extends AbstractBoss {

    /** Summoner spawners */
    private spawners: Barrel[] = [];

    public constructor(game: GameServer) {
        super(game);

        this.nameData.values.name = 'Summoner';
        this.styleData.values.color = Color.EnemySquare;
        this.relationsData.values.team = this.game.arena;
        this.physicsData.values.size = SUMMONER_SIZE * Math.SQRT1_2;
        this.physicsData.values.sides = 4;

        for (let i = 0; i < 4; ++i) {
            this.spawners.push(new Barrel(this, {
                ...SummonerSpawnerDefinition,
                angle: PI2 * ((i / 4) - 1 / 4)
            }));

        }
    }

    public get sizeFactor() {
        return (this.physicsData.values.size / Math.SQRT1_2) / SUMMONER_SIZE;
    }


    public tick(tick: number) {
        super.tick(tick);

        if (this.ai.state !== AIState.possessed) {
            this.positionData.angle += this.ai.passiveRotation;
        }
    }
}
`

~src/Entity/Misc/ArenaCloser.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import TankBody from "../Tank/TankBody";

import { Color, PhysicsFlags, PositionFlags, Stat, Tank } from "../../Const/Enums";
import { CameraEntity } from "../../Native/Camera";
import { AI, AIState, Inputs } from "../AI";

/**
 * Represents the Arena Closers that end the game.
 */
export default class ArenaCloser extends TankBody {
    /** Size of a level 0 Arena Closer. */
    public static BASE_SIZE = 175;
    
    /** The AI that controls how the AC moves. */
    public ai: AI;

    public constructor(game: GameServer) {
        const inputs = new Inputs();
        const camera = new CameraEntity(game);

        const setLevel = camera.setLevel;
        camera.setLevel = function(level) {
            setLevel.call(this, level);

            this.sizeFactor *= (ArenaCloser.BASE_SIZE / 50);
        }
        camera.sizeFactor = (ArenaCloser.BASE_SIZE / 50);

        super(game, camera, inputs);

        this.relationsData.values.team = game.arena;

        this.ai = new AI(this);
        this.ai.inputs = inputs;
        this.ai.viewRange = Infinity;

        this.setTank(Tank.ArenaCloser);

        const def = (this.definition = Object.assign({}, this.definition));
        def.maxHealth = 10000;
        // TODO(ABC):
        // Fix all the stats
        def.speed = 1;

        this.damagePerTick = 200;

        this.nameData.values.name = "Arena Closer";
        this.styleData.values.color = Color.Neutral;
        this.positionData.values.flags |= PositionFlags.canMoveThroughWalls;
        this.physicsData.values.flags |= PhysicsFlags.canEscapeArena;
        camera.cameraData.values.player = this;

        for (let i = Stat.MovementSpeed; i < Stat.BodyDamage; ++i) camera.cameraData.values.statLevels.values[i] = 7;

        this.ai.aimSpeed = this.barrels[0].bulletAccel * 1.6;
        this.setInvulnerability(true);
    }

    public tick(tick: number) {
        this.ai.movementSpeed = this.cameraEntity.cameraData.values.movementSpeed * 10;

        this.inputs = this.ai.inputs;

        if (this.ai.state === AIState.idle) {
            const angle = this.positionData.values.angle + this.ai.passiveRotation;
            const mag = Math.sqrt((this.inputs.mouse.x - this.positionData.values.x) ** 2 + (this.inputs.mouse.y - this.positionData.values.y) ** 2);
            this.inputs.mouse.set({
                x: this.positionData.values.x + Math.cos(angle) * mag,
                y: this.positionData.values.y + Math.sin(angle) * mag
            });
        }

        super.tick(tick);
    }
}
`

~src/Entity/Misc/BaseDrones.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

// Hi there

// TODO(ABC):
// Implement 8 drones owned by an invisible overlord
`

~src/Entity/Misc/Dominator.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { Color, NameFlags, StyleFlags, Tank } from "../../Const/Enums";
import ArenaEntity from "../../Native/Arena";
import { CameraEntity } from "../../Native/Camera";
import { AI, AIState, Inputs } from "../AI";
import LivingEntity from "../Live";
import Bullet from "../Tank/Projectile/Bullet";
import TankBody from "../Tank/TankBody";
import TeamBase from "./TeamBase";
import { TeamEntity } from "./TeamEntity";

/**
 * Dominator Tank
 */
export default class Dominator extends TankBody {
    /** Size of a dominator */
    public static SIZE = 160;

    /** The AI that controls how the Dominator aims. */
    public ai: AI;

    /** The base its located on (used to change base color). */
    public base: TeamBase;

    public constructor(arena: ArenaEntity, base: TeamBase, pTankId: Tank | null = null) {
        let tankId: Tank;
        if (pTankId === null) {
            const r = Math.random() * 3;

            if (r < 1) tankId = Tank.DominatorD;
            else if (r < 2) tankId = Tank.DominatorG;
            else tankId = Tank.DominatorT;
        } else tankId = pTankId;

        const inputs = new Inputs();
        const camera = new CameraEntity(arena.game);

        camera.setLevel(75);
        camera.sizeFactor = (Dominator.SIZE / 50);

        super(arena.game, camera, inputs);

        this.relationsData.values.team = arena;
        this.physicsData.values.size = Dominator.SIZE;
        // TODO(ABC):
        // Add setTeam method for this
        this.styleData.values.color = Color.Neutral;

        this.ai = new AI(this, true);
        this.ai.inputs = inputs;
        this.ai.movementSpeed = 0;
        this.ai.viewRange = 2000;
        this.ai.doAimPrediction = true;

        this.setTank(tankId);
        const def = (this.definition = Object.assign({}, this.definition));
        def.speed = camera.cameraData.values.movementSpeed = 0;
        this.nameData.values.name = "Dominator";
        this.nameData.values.flags |= NameFlags.hiddenName;
        this.physicsData.values.absorbtionFactor = 0;
        
        this.positionData.values.x = base.positionData.values.x;
        this.positionData.values.y = base.positionData.values.y;
        
        this.scoreReward = 0;
        camera.cameraData.values.player = this;

        this.base = base;

        if (this.styleData.values.flags & StyleFlags.isFlashing) { 
            this.styleData.values.flags ^= StyleFlags.isFlashing;
            this.damageReduction = 1.0;
        }
    }

    public onDeath(killer: LivingEntity) {
        if (this.relationsData.values.team === this.game.arena && killer.relationsData.values.team instanceof TeamEntity) {
            this.relationsData.team = killer.relationsData.values.team || this.game.arena;
            this.styleData.color = this.relationsData.team.teamData?.teamColor || killer.styleData.values.color;
        } else {
            this.relationsData.team = this.game.arena
            this.styleData.color = this.game.arena.teamData.teamColor;
        }

        this.base.styleData.color = this.styleData.values.color;
        this.base.relationsData.team = this.relationsData.values.team;;

        this.healthData.health = this.healthData.values.maxHealth;

        for (let i = 1; i <= this.game.entities.lastId; ++i) {
            const entity = this.game.entities.inner[i];
            if (entity instanceof Bullet && entity.relationsData.values.owner === this) entity.destroy();
        }

        if (this.ai.state === AIState.possessed) {
            this.ai.inputs.deleted = true;
            this.ai.inputs = this.inputs = new Inputs();
            this.ai.state = AIState.idle;
        }
    }

    public tick(tick: number) {
        if (!this.barrels.length) return super.tick(tick)
        this.ai.aimSpeed = this.barrels[0].bulletAccel;
        this.inputs = this.ai.inputs;

        if (this.ai.state === AIState.idle) {
            const angle = this.positionData.values.angle + this.ai.passiveRotation;
            const mag = Math.sqrt((this.inputs.mouse.x - this.positionData.values.x) ** 2 + (this.inputs.mouse.y - this.positionData.values.y) ** 2);
            this.inputs.mouse.set({
                x: this.positionData.values.x + Math.cos(angle) * mag,
                y: this.positionData.values.y + Math.sin(angle) * mag
            });
        }

        super.tick(tick);
    }
}
`

~src/Entity/Misc/MazeWall.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import ObjectEntity from "../Object";

import { PhysicsFlags, Color } from "../../Const/Enums";
/**
 * Only used for maze walls and nothing else.
 */
export default class MazeWall extends ObjectEntity {
    public constructor(game: GameServer, x: number, y: number, width: number, height: number) {
        super(game);

        this.positionData.values.x = x;
        this.positionData.values.y = y;

        this.physicsData.values.width = width;
        this.physicsData.values.size = height;
        this.physicsData.values.sides = 2;
        this.physicsData.values.flags |= PhysicsFlags.isSolidWall | PhysicsFlags.showsOnMap;
        this.physicsData.values.pushFactor = 2;
        this.physicsData.values.absorbtionFactor = 0;

        this.styleData.values.borderWidth = 10;
        this.styleData.values.color = Color.Box;
    }
}
`

~src/Entity/Misc/Mothership.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { ClientInputs } from "../../Client";
import { tps } from "../../config";
import { Color, Tank, Stat, ColorsHexCode, ClientBound, TeamFlags } from "../../Const/Enums";
import GameServer from "../../Game";
import ArenaEntity from "../../Native/Arena";
import { CameraEntity } from "../../Native/Camera";
import { AI, AIState, Inputs } from "../AI";
import Live from "../Live";
import TankBody from "../Tank/TankBody";
import { TeamEntity } from "./TeamEntity";

const POSSESSION_TIMER = tps * 60 * 5;

/**
 * Mothership Entity
 */
export default class Mothership extends TankBody {
    /** The AI that controls how the Mothership aims. */
    public ai: AI;

    /** If the mothership's AI ever gets possessed, this is the tick that the possession started. */
    public possessionStartTick: number = -1;

    public constructor(game: GameServer) {

        const inputs = new Inputs();
        const camera = new CameraEntity(game);

        camera.setLevel(140);

        super(game, camera, inputs);

        this.relationsData.values.team = game.arena;

        this.styleData.values.color = Color.Neutral;

        this.ai = new AI(this, true);
        this.ai.inputs = inputs;
        this.ai.viewRange = 2000;
        
        this.positionData.values.x = 0;
        this.positionData.values.y = 0;
        
        this.setTank(Tank.Mothership);
        
        this.nameData.values.name = "Mothership"
        
        this.scoreReward = 0;
        
        camera.cameraData.values.player = this;

        for (let i = Stat.MovementSpeed; i < Stat.HealthRegen; ++i) camera.cameraData.values.statLevels.values[i] = 7;
        camera.cameraData.values.statLevels.values[Stat.HealthRegen] = 1;

        const def = (this.definition = Object.assign({}, this.definition));
        // 418 is what the normal health increase for stat/level would be, so we just subtract it and force it 7k
        def.maxHealth = 7008 - 418;
    }

    public onDeath(killer: Live): void {
        const team = this.relationsData.values.team;
        const teamIsATeam = team instanceof TeamEntity;

        const killerTeam = killer.relationsData.values.team;
        const killerTeamIsATeam = killerTeam instanceof TeamEntity;

        // UNCOMMENT TO ALLOW SOLO KILLS
        // if (!killerTeamIsATeam) return;
        this.game.broadcast()
            .u8(ClientBound.Notification)
            // If mothership has a team name, use it, otherwise just say has destroyed a mothership
            .stringNT(`${killerTeamIsATeam ? killerTeam.teamName : (killer.nameData?.values.name || "an unnamed tank")} has destroyed ${teamIsATeam ? team.teamName + "'s" : "a"} Mothership!`)
            .u32(killerTeamIsATeam ? ColorsHexCode[killerTeam.teamData.values.teamColor] : 0x000000)
            .float(-1)
            .stringNT("").send();   
    }

    public delete(): void {
        // No more mothership arrow - seems like in old diep this wasn't the case - we should probably keep though
        if (this.relationsData.values.team?.teamData) this.relationsData.values.team.teamData.flags  &= ~TeamFlags.hasMothership;
        this.ai.inputs.deleted = true;
        super.delete();
    }


    public tick(tick: number) {
        if (!this.barrels.length) return super.tick(tick)

        this.inputs = this.ai.inputs;

        if (this.ai.state === AIState.idle) {
            const angle = this.positionData.values.angle + this.ai.passiveRotation;
            const mag = Math.sqrt((this.inputs.mouse.x - this.positionData.values.x) ** 2 + (this.inputs.mouse.y - this.positionData.values.y) ** 2);
            this.inputs.mouse.set({
                x: this.positionData.values.x + Math.cos(angle) * mag,
                y: this.positionData.values.y + Math.sin(angle) * mag
            });
        } else if (this.ai.state === AIState.possessed && this.possessionStartTick === -1) {
            this.possessionStartTick = tick;
        }
        if (this.possessionStartTick !== -1 && this.ai.state !== AIState.possessed) {
            this.possessionStartTick = -1;
        }

        // after 10 minutes, kick out the person possessing
        if (this.possessionStartTick !== -1) {
            if (this.possessionStartTick !== -1 && this.ai.state !== AIState.possessed) {
                this.possessionStartTick = -1;
            } else if (this.inputs instanceof ClientInputs) {
                if (tick - this.possessionStartTick >= POSSESSION_TIMER) {
                    this.inputs.deleted = true;
                } else if (tick - this.possessionStartTick === Math.floor(POSSESSION_TIMER - 10 * tps)) {
                    this.inputs.client.notify("You only have 10 seconds left in control of the Mothership", ColorsHexCode[this.styleData.values.color], 5_000, "");
                }
            }
        }

        super.tick(tick);
    }
}
`

~src/Entity/Misc/TeamBase.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";

import { HealthFlags, PhysicsFlags, StyleFlags } from "../../Const/Enums";
import { TeamGroupEntity } from "./TeamEntity";
import LivingEntity from "../Live";
/**
 * Represents Team Bases in game.
 */
export default class TeamBase extends LivingEntity {

    public constructor(game: GameServer, team: TeamGroupEntity, x: number, y: number, width: number, height: number, painful: boolean=true) {
        super(game);

        this.relationsData.values.team = team;

        this.positionData.values.x = x;
        this.positionData.values.y = y;

        this.physicsData.values.width = width;
        this.physicsData.values.size = height;
        this.physicsData.values.sides = 2;
        this.physicsData.values.flags |= PhysicsFlags.showsOnMap | PhysicsFlags.noOwnTeamCollision | PhysicsFlags.isBase;
        this.physicsData.values.pushFactor = 2;
        this.damagePerTick = 5;

        if (!painful) {
            this.physicsData.values.pushFactor = 0;
            this.damagePerTick = 0;
        }

        this.damageReduction = 0;
        this.physicsData.values.absorbtionFactor = 0;

        this.styleData.values.opacity = 0.1;
        this.styleData.values.borderWidth = 0;
        this.styleData.values.color = team.teamData.teamColor;
        this.styleData.values.flags |= StyleFlags.renderFirst | StyleFlags.hasNoDmgIndicator;

        this.healthData.flags |= HealthFlags.hiddenHealthbar
        this.healthData.health = this.healthData.values.maxHealth = 0xABCFF; // ;)
    }

    public tick(tick: number) {
        // No animation. No regen
        this.lastDamageTick = tick;
    }
}
`

~src/Entity/Misc/TeamEntity.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";

import { Color } from "../../Const/Enums";
import { Entity } from "../../Native/Entity";
import { TeamGroup } from "../../Native/FieldGroups";

export type TeamGroupEntity = Entity & { teamData: TeamGroup }

export const ColorsTeamName: { [K in Color]?: string } = {
    [Color.Border]: "BORDER",
    [Color.Barrel]: "BARREL",
    [Color.Tank]: "TANK",
    [Color.TeamBlue]: "BLUE",
    [Color.TeamRed]: "RED",
    [Color.TeamPurple]: "PURPLE",
    [Color.TeamGreen]: "GREEN",
    [Color.Shiny]: "SHINY",
    [Color.EnemySquare]: "SQUARE",
    [Color.EnemyTriangle]: "TRIANGLE",
    [Color.EnemyPentagon]: "PENTAGON",
    [Color.EnemyCrasher]: "CRASHER",
    [Color.Neutral]: "a mysterious group",
    [Color.ScoreboardBar]: "SCOREBOARD",
    [Color.Box]: "MAZE",
    [Color.EnemyTank]: "ENEMY",
    [Color.NecromancerSquare]: "SUNCHIP",
    [Color.Fallen]: "FALLEN"
}

export class TeamEntity extends Entity implements TeamGroupEntity {
    /** This group makes `this` a team entity in the first place. */
    public teamData: TeamGroup = new TeamGroup(this);

    /** Used for notifications in team based gamemodes */
    public teamName: string;

    public constructor(game: GameServer, color: Color, name: string = ColorsTeamName[color] || "UNKNOWN") {
        super(game);

        this.teamData.values.teamColor = color;
        this.teamName = name;
    }
}
`

~src/Entity/Shape/AbstractShape.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import LivingEntity from "../Live";

import { Color, PositionFlags, NameFlags } from "../../Const/Enums";
import { NameGroup } from "../../Native/FieldGroups";
import { AI } from "../AI";
import { normalizeAngle, PI2 } from "../../util";

/**
 * Ticks between turns
 */
const TURN_TIMEOUT = 300;
/**
 * Abstract class which all shapes are subclasses of.
 * 
 * Used to calculate tick speed from `BASE_ROTATION`, `BASE_ORBIT`, and `BASE_VELOCITY`,
 * all of which are defined staticly, and can be defined a subclass statically as well.
 */
export default class AbstractShape extends LivingEntity {
    /** Used to calculate the speed at which the shape turns. Radians Per Tick. */
    protected static BASE_ROTATION = AI.PASSIVE_ROTATION;
    /** Used to calculate the speed at which the shape orbits. Radians Per Tick. */
    protected static BASE_ORBIT = 0.005;
    /** The velocity of the shape's orbits. */
    protected static BASE_VELOCITY = 1;

    /** Always existant name field group, present in all shapes. */
    public nameData: NameGroup = new NameGroup(this);
    /** If the shape is shiny or not */
    public isShiny: boolean = false;

    /** Wether or not to do idle movements */
    protected doIdleRotate: boolean = true;
    /** The current direction of the shape's orbit. */
    protected orbitAngle: number;
    /** The decided orbit rate, based on the constructor's BASE_ORBIT. *//* @ts-ignore */
    protected orbitRate = (Math.random() < .5 ? -1 : 1) * this.constructor.BASE_ORBIT
    /** The decided rotation rate, based on the constructor's BASE_ROTATION. *//* @ts-ignore */
    protected rotationRate = (Math.random() < .5 ? -1 : 1) * this.constructor.BASE_ROTATION
    /** The decided velocity of the shape, based on the constructor's BASE_VELOCITY. *//* @ts-ignore */
    protected shapeVelocity = this.constructor.BASE_VELOCITY;

    /** Whether or not the tank is turning */
    protected isTurning: number = 0;
    /** The destination for the angle of the shape's orbit - it slowly becomes this */
    protected targetTurningAngle: number = 0;

    public constructor(game: GameServer) {
        super(game);

        this.relationsData.values.team = game.arena;

        // shape names are by default hidden
        this.nameData.values.flags = NameFlags.hiddenName;
        this.positionData.values.flags |= PositionFlags.absoluteRotation;
        this.orbitAngle = this.positionData.values.angle = (Math.random() * PI2);
    }

    protected turnTo(angle: number) {
        if (normalizeAngle(this.orbitAngle - angle) < 0.20) return;
        this.targetTurningAngle = angle;
        this.isTurning = TURN_TIMEOUT;
    }

    public tick(tick: number) {
        if (!this.doIdleRotate) {
            return super.tick(tick);
        }
        
        const y = this.positionData.values.y;
        const x = this.positionData.values.x;

        // goes down too much
        if (this.isTurning === 0) {
            if (x > this.game.arena.arenaData.values.rightX - 400
                || x < this.game.arena.arenaData.values.leftX + 400
                || y < this.game.arena.arenaData.values.topY + 400
                || y > this.game.arena.arenaData.values.bottomY - 400) {
                this.turnTo(Math.PI + Math.atan2(y, x));
            } else if (x > this.game.arena.arenaData.values.rightX - 500) {
                this.turnTo(Math.sign(this.orbitRate) * Math.PI / 2);
            } else if (x < this.game.arena.arenaData.values.leftX + 500) {
                this.turnTo(-1 * Math.sign(this.orbitRate) * Math.PI / 2);
            } else if (y < this.game.arena.arenaData.values.topY + 500) {
                this.turnTo(this.orbitRate > 0 ? 0 : Math.PI);
            } else if (y > this.game.arena.arenaData.values.bottomY - 500) {
                this.turnTo(this.orbitRate > 0 ? Math.PI : 0);
            }
        }
        this.positionData.angle += this.rotationRate;
        this.orbitAngle += this.orbitRate + (this.isTurning === TURN_TIMEOUT ? this.orbitRate * 10 : 0);
        if (this.isTurning === TURN_TIMEOUT && (((this.orbitAngle - this.targetTurningAngle) % (PI2)) + (PI2)) % (PI2) < 0.20) {
            this.isTurning -= 1;
        } else if (this.isTurning !== TURN_TIMEOUT && this.isTurning !== 0) this.isTurning -= 1;

        // convert from angle to orbit angle: angle / (10 / 3)
        // convert from orbit angle to angle: orbitAngle * (10 / 3)
        this.maintainVelocity(this.orbitAngle, this.shapeVelocity);

        super.tick(tick);
    }
}
`

~src/Entity/Shape/Crasher.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import LivingEntity from "../Live";
import AbstractShape from "./AbstractShape";

import { Color, PositionFlags } from "../../Const/Enums";
import { AI, AIState } from "../AI";
import { tps } from "../../config";

/**
 * Crasher entity class.
 */
export default class Crasher extends AbstractShape {
    /** Controls the artificial intelligence of the crasher */
    public ai: AI;

    /** Whether or not the crasher is large. */
    public isLarge: boolean;
    /** The max speed the crasher can move when targetting a player.s */
    private targettingSpeed: number;

    public constructor(game: GameServer, large=false) {
        super(game);

        this.nameData.values.name = "Crasher";

        this.positionData.values.flags |= PositionFlags.canMoveThroughWalls;
        this.healthData.values.health = this.healthData.values.maxHealth = large ? 30 : 10;
        this.physicsData.values.size = (large ? 55 : 35) * Math.SQRT1_2;
        this.physicsData.values.sides = 3;
        this.physicsData.values.absorbtionFactor = large ? 0.1 : 2;
        this.physicsData.values.pushFactor =  large ? 12 : 8;

        this.styleData.values.color = Color.EnemyCrasher;

        this.scoreReward = large ? 25 : 15;
        this.damagePerTick = 8;
        this.isLarge = large;
        this.targettingSpeed = large ? 2.64 : 2.602;

        this.ai = new AI(this);
        this.ai.viewRange = 2000;
        this.ai.aimSpeed = (this.ai.movementSpeed = this.targettingSpeed);
        this.ai['_findTargetInterval'] = tps;
    }

    tick(tick: number) {
        this.ai.aimSpeed = 0;
        this.ai.movementSpeed = this.targettingSpeed;
        
        if (this.ai.state === AIState.idle) {
            this.doIdleRotate = true;
        } else {
            this.doIdleRotate = false;
            this.positionData.angle = Math.atan2(this.ai.inputs.mouse.y - this.positionData.values.y, this.ai.inputs.mouse.x - this.positionData.values.x);
            this.accel.add({
                x: this.ai.inputs.movement.x * this.targettingSpeed,
                y: this.ai.inputs.movement.y * this.targettingSpeed
            });
        }

        this.ai.inputs.movement.set({
            x: 0,
            y: 0
        })

        super.tick(tick);
    }
}
`

~src/Entity/Shape/Manager.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import ArenaEntity from "../../Native/Arena";
import GameServer from "../../Game";

import Crasher from "./Crasher";
import Pentagon from "./Pentagon";
import Triangle from "./Triangle";
import Square from "./Square";
import AbstractShape from "./AbstractShape";
import { removeFast } from "../../util";

/**
 * Used to balance out shape count in the arena, as well
 * as determines where each type of shape spawns around the arena.
 */

export default class ShapeManager {
    /** Current game server */
    protected game: GameServer;
    /** Arena whose shapes are being managed */
    protected arena: ArenaEntity;
    /** Stores all shapes */
    protected shapes: AbstractShape[] = [];

    public constructor(arena: ArenaEntity) {
        this.arena = arena;
        this.game = arena.game;
    }

    /**
     * Spawns a shape in a random location on the map.
     * Determines shape type by the random position chosen.
     */
    protected spawnShape(): AbstractShape {
        let shape: AbstractShape;
        const {x, y} = this.arena.findSpawnLocation();
        const rightX = this.arena.arenaData.values.rightX;
        const leftX = this.arena.arenaData.values.leftX;
        if (Math.max(x, y) < rightX / 10 && Math.min(x, y) > leftX / 10) {
            // Pentagon Nest
            shape = new Pentagon(this.game, Math.random() <= 0.05);

            shape.positionData.values.x = x;
            shape.positionData.values.y = y;
            shape.relationsData.values.owner = shape.relationsData.values.team = this.arena;
        } else if (Math.max(x, y) < rightX / 5 && Math.min(x, y) > leftX / 5) {
            // Crasher Zone
            const isBig = Math.random() < .2;

            shape = new Crasher(this.game, isBig);
            
            shape.positionData.values.x = x;
            shape.positionData.values.y = y;
            shape.relationsData.values.owner = shape.relationsData.values.team = this.arena;
        } else {
            // Fields of Shapes
            const rand = Math.random();
            if (rand < .04) {
                shape = new Pentagon(this.game);

                shape.positionData.values.x = x;
                shape.positionData.values.y = y;
                shape.relationsData.values.owner = shape.relationsData.values.team = this.arena;
            } else if (rand < .20) { // < 16%
                shape = new Triangle(this.game);

                shape.positionData.values.x = x;
                shape.positionData.values.y = y;
                shape.relationsData.values.owner = shape.relationsData.values.team = this.arena;
            } else { // if rand < 80%
                shape = new Square(this.game);

                shape.positionData.values.x = x;
                shape.positionData.values.y = y;
                shape.relationsData.values.owner = shape.relationsData.values.team = this.arena;
            }
        }

        shape.scoreReward *= this.arena.shapeScoreRewardMultiplier;

        return shape;
    }

    /** Kills all shapes in the arena */
    public killAll() {
        for(let i = 0; i < this.shapes.length; ++i) {
            this.shapes[i]?.delete();
        }
    }

    protected get wantedShapes() {
        return 1000;
    }

    public tick() {
        for (let i = this.wantedShapes; i --> 0;) {
            const shape = this.shapes[i];
            // Alternatively, Entity.exists(shape), though this is probably faster
            if (!shape || shape.hash === 0) this.shapes[i] = this.spawnShape();
        }
    }
}
`

~src/Entity/Shape/Pentagon.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";

import { Color } from "../../Const/Enums";

/**
 * Pentagon entity class.
 */
export default class Pentagon extends AbstractShape {
    /** If the pentagon is an alpha pentagon or not */
    public isAlpha: boolean;

    protected static BASE_ROTATION = AbstractShape.BASE_ROTATION / 2;
    protected static BASE_ORBIT = AbstractShape.BASE_ORBIT / 2;
    protected static BASE_VELOCITY = AbstractShape.BASE_VELOCITY / 2;

    public constructor(game: GameServer, isAlpha=false, shiny=(Math.random() < 0.000001) && !isAlpha) {
        super(game);
        
        this.nameData.values.name = isAlpha ? "Alpha Pentagon" : "Pentagon";

        this.healthData.values.health = this.healthData.values.maxHealth = (isAlpha ? 3000 : 100);
        this.physicsData.values.size = (isAlpha ? 200 : 75) * Math.SQRT1_2;
        this.physicsData.values.sides = 5;
        this.styleData.values.color = shiny ? Color.Shiny : Color.EnemyPentagon;

        this.physicsData.values.absorbtionFactor = isAlpha ? 0.05 : 0.5;
        this.physicsData.values.pushFactor = 11;

        this.isAlpha = isAlpha;
        this.isShiny = shiny;

        this.damagePerTick = isAlpha ? 20 : 12;
        this.scoreReward = isAlpha ? 3000 : 130;
        
        if (shiny) {
            this.scoreReward *= 100;
            this.healthData.values.health = this.healthData.values.maxHealth *= 10;
        }
    }
}
`

~src/Entity/Shape/Square.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";

import { Color } from "../../Const/Enums";

export default class Square extends AbstractShape {
    public constructor(game: GameServer, shiny=Math.random() < 0.000001) {
        super(game);
        this.nameData.values.name = "Square";
        this.healthData.values.health = this.healthData.values.maxHealth = 10;
        this.physicsData.values.size = 55 * Math.SQRT1_2;
        this.physicsData.values.sides = 4;
        this.styleData.values.color = shiny ? Color.Shiny : Color.EnemySquare;

        this.damagePerTick = 8;
        this.scoreReward = 10;
        this.isShiny = shiny;

        if (shiny) {
            this.scoreReward *= 100;
            this.healthData.values.health = this.healthData.values.maxHealth *= 10;
        }
    }
}
`

~src/Entity/Shape/Triangle.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import AbstractShape from "./AbstractShape";

import { Color } from "../../Const/Enums";

export default class Triangle extends AbstractShape {
    public constructor(game: GameServer, shiny=Math.random() < 0.000001) {
        super(game);
        
        this.nameData.values.name = "Triangle";
        this.healthData.values.health = this.healthData.values.maxHealth = 30;
        this.physicsData.values.size = 55 * Math.SQRT1_2;
        this.physicsData.values.sides = 3;
        this.styleData.values.color = shiny ? Color.Shiny : Color.EnemyTriangle;

        this.damagePerTick = 8;
        this.scoreReward = 25;
        this.isShiny = shiny;

        if (shiny) {
            this.scoreReward *= 100;
            this.healthData.values.health = this.healthData.values.maxHealth *= 10;
        }
    }
}
`

~src/Entity/Tank/Projectile/Bullet.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import LivingEntity from "../../Live";
import Barrel from "../Barrel";

import { HealthFlags, PositionFlags, PhysicsFlags, Stat, StyleFlags } from "../../../Const/Enums";
import { TankDefinition } from "../../../Const/TankDefinitions";
import { BarrelBase } from "../TankBody";
import { EntityStateFlags } from "../../../Native/Entity";

/**
 * The bullet class represents the bullet entity in diep.
 */
export default class Bullet extends LivingEntity {
    /** The barrel that the bullet is being shot from. */
    protected barrelEntity: Barrel;
    /** The tick this entity was created in. */
    protected spawnTick = 0;
    /** Speed the bullet will accelerate at. */
    protected baseAccel = 0;
    /** Starting velocity of the bullet. */
    protected baseSpeed = 0;
    /** Percent of accel applied when dying. */
    protected deathAccelFactor = 0.5;
    /** Life length in ticks before the bullet dies. */
    protected lifeLength = 0;
    /** Angle the projectile is shot at. */
    protected movementAngle = 0;
    /** Definition of the tank (if existant) shooting the bullet. */
    protected tankDefinition: TankDefinition | null = null;
    /** Whether or not to use .shootAngle or .position.angle. */
    protected usePosAngle = false;
    /** The tank who shot the bullet. */
    protected tank: BarrelBase;

    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel.game);

        this.tank = tank;
        
        this.tankDefinition = tankDefinition;

        // if (barrel.definition.bullet.type === "drone") throw new TypeError("Invalid bullet type for this class");
        this.movementAngle = shootAngle;
        this.barrelEntity = barrel;
        this.spawnTick = barrel.game.tick;

        this.relationsData.values.owner = tank;

        tank.rootParent.styleData.zIndex = barrel.game.entities.zIndex++;

        const bulletDefinition = barrel.definition.bullet;
        const sizeFactor = tank.sizeFactor;
        const statLevels = tank.cameraEntity.cameraData?.values.statLevels.values;

        this.relationsData.values.team = barrel.relationsData.values.team;
        this.relationsData.values.owner = tank;

        this.physicsData.values.sides = bulletDefinition.sides ?? 1;
        this.physicsData.values.flags |= PhysicsFlags.noOwnTeamCollision | PhysicsFlags.canEscapeArena;
        if (tank.positionData.values.flags & PositionFlags.canMoveThroughWalls) this.positionData.values.flags |= PositionFlags.canMoveThroughWalls
        this.physicsData.values.size = (barrel.physicsData.values.width / 2) * bulletDefinition.sizeRatio;
        this.styleData.values.color = bulletDefinition.color || tank.rootParent.styleData.values.color;
        this.styleData.values.flags |= StyleFlags.hasNoDmgIndicator;
        this.healthData.values.flags = HealthFlags.hiddenHealthbar;

        const bulletDamage = statLevels ? statLevels[Stat.BulletDamage] : 0;
        const bulletPenetration = statLevels ? statLevels[Stat.BulletPenetration] : 0;

        this.physicsData.values.absorbtionFactor = bulletDefinition.absorbtionFactor;
        this.physicsData.values.pushFactor = ((7 / 3) + bulletDamage) * bulletDefinition.damage * bulletDefinition.absorbtionFactor;

        this.baseAccel = barrel.bulletAccel;
        this.baseSpeed = barrel.bulletAccel + 30 - Math.random() * bulletDefinition.scatterRate;

        this.healthData.values.health = this.healthData.values.maxHealth = (1.5 * bulletPenetration + 2) * bulletDefinition.health;
        this.damagePerTick = (7 + bulletDamage * 3) * bulletDefinition.damage;
        this.damageReduction = 0.25;

        this.lifeLength = bulletDefinition.lifeLength * 72;

        const {x, y} = tank.getWorldPosition();
        
        this.positionData.values.x = x + (Math.cos(shootAngle) * barrel.physicsData.values.size) - Math.sin(shootAngle) * barrel.definition.offset * sizeFactor + Math.cos(shootAngle) * (barrel.definition.distance || 0);
        this.positionData.values.y = y + (Math.sin(shootAngle) * barrel.physicsData.values.size) + Math.cos(shootAngle) * barrel.definition.offset * sizeFactor + Math.sin(shootAngle) * (barrel.definition.distance || 0);
        this.positionData.values.angle = shootAngle;
    }

    /** Extends LivingEntity.onKill - passes kill to the owner. */
    public onKill(killedEntity: LivingEntity) {
        // TODO(ABC):
        // Make this, work differently
        /** @ts-ignore */
        if (typeof this.tank.onKill === 'function') this.tank.onKill(killedEntity);
    }

    public tick(tick: number) {
        super.tick(tick);

        if (tick === this.spawnTick + 1) this.addAcceleration(this.movementAngle, this.baseSpeed);
        else this.maintainVelocity(this.usePosAngle ? this.positionData.values.angle : this.movementAngle, this.baseAccel);

        if (tick - this.spawnTick >= this.lifeLength) this.destroy(true);
        // TODO(ABC):
        // This code will be reimplemented in the update that allows for easy camera entity switches
        if ((this.relationsData.values.team?.entityState || 0) & EntityStateFlags.needsDelete) this.relationsData.values.team = null
    }
}
`

~src/Entity/Tank/Projectile/CrocSkimmer.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { InputFlags } from "../../../Const/Enums";
import { BarrelDefinition, TankDefinition } from "../../../Const/TankDefinitions";
import { Inputs } from "../../AI";
import { BarrelBase } from "../TankBody";
import { CameraEntity } from "../../../Native/Camera";

/**
 * Barrel definition for the skimmer skimmer's barrel.
 */
const CrocSkimmerBarrelDefinition: BarrelDefinition = {
    angle: Math.PI / 2,
    offset: 0,
    size: 70,
    width: 42,
    delay: 0,
    reload: 0.5,
    recoil: 0,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 0.3,
        damage: 3 / 5,
        speed: .2,
        scatterRate: 1,
        lifeLength: 0.25,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * Represents all skimmereer skimmers in game.
 */
export default class CrocSkimmer extends Bullet implements BarrelBase {
    /** The croc skimmer's barrels */
    private skimmerBarrels: Barrel[];

    /** The camera entity (used as team) of the croc skimmer. */
    public cameraEntity: CameraEntity;
    /** The reload time of the skimmer's barrel. */
    public reloadTime = 15;
    /** The inputs for when to shoot or not. (croc skimmer) */
    public inputs: Inputs;


    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        this.cameraEntity = tank.cameraEntity;

        const skimmerBarrels: Barrel[] = this.skimmerBarrels =[];

        const s1 = new Barrel(this, {...CrocSkimmerBarrelDefinition});
        const s2Definition = {...CrocSkimmerBarrelDefinition};
        s2Definition.angle += Math.PI
        const s2 = new Barrel(this, s2Definition);

        s1.styleData.values.color = this.styleData.values.color;
        s2.styleData.values.color = this.styleData.values.color;

        skimmerBarrels.push(s1, s2);

        this.inputs = new Inputs();
        this.inputs.flags |= InputFlags.leftclick;
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    public tick(tick: number) {
        this.reloadTime = this.tank.reloadTime;
        super.tick(tick);

        if (this.deletionAnimation) return;
        // Only accurate on current version, but we dont want that
        // if (!Entity.exists(this.barrelEntity.rootParent) && (this.inputs.flags & InputFlags.leftclick)) this.inputs.flags ^= InputFlags.leftclick;
    }
}
`

~src/Entity/Tank/Projectile/Drone.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { PhysicsFlags, StyleFlags } from "../../../Const/Enums";
import { TankDefinition } from "../../../Const/TankDefinitions";
import { Entity } from "../../../Native/Entity";
import { AI, AIState } from "../../AI";
import { BarrelBase } from "../TankBody";

/**
 * The drone class represents the drone (projectile) entity in diep.
 */
export default class Drone extends Bullet {
    /** The AI of the drone (for AI mode) */
    public ai: AI;

    /** The drone's radius of resting state */
    public static MAX_RESTING_RADIUS = 400 ** 2;

    /** Used let the drone go back to the player in time. */
    private restCycle = true;

    /** Cached prop of the definition. */
    protected canControlDrones: boolean;

    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        const bulletDefinition = barrel.definition.bullet;

        this.usePosAngle = true;
        
        this.ai = new AI(this);
        this.ai.viewRange = 850 * tank.sizeFactor;
        this.ai.targetFilter = (targetPos) => (targetPos.x - this.tank.positionData.values.x) ** 2 + (targetPos.y - this.tank.positionData.values.y) ** 2 <= this.ai.viewRange ** 2; // (1000 ** 2) 1000 radius
        this.canControlDrones = typeof this.barrelEntity.definition.canControlDrones === 'boolean' && this.barrelEntity.definition.canControlDrones;
        this.physicsData.values.sides = bulletDefinition.sides ?? 3;
        if (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) this.physicsData.values.flags ^= PhysicsFlags.noOwnTeamCollision;
        this.physicsData.values.flags |= PhysicsFlags.onlySameOwnerCollision;
        this.physicsData.values.flags ^= PhysicsFlags.canEscapeArena;
        this.styleData.values.flags &= ~StyleFlags.hasNoDmgIndicator;

        if (barrel.definition.bullet.lifeLength !== -1) {
            this.lifeLength = 88 * barrel.definition.bullet.lifeLength;
        } else {
            this.lifeLength = Infinity;
        }
        this.deathAccelFactor = 1;

        this.physicsData.values.pushFactor = 4;
        this.physicsData.values.absorbtionFactor = bulletDefinition.absorbtionFactor;

        this.baseSpeed /= 3;

        barrel.droneCount += 1;

        this.ai.movementSpeed = this.ai.aimSpeed = this.baseAccel;
    }

    /** Extends LivingEntity.destroy - so that the drone count decreases for the barrel. */
    public destroy(animate=true) {
        if (!animate) this.barrelEntity.droneCount -= 1;

        super.destroy(animate);
    }
    
    /** This allows for factory to hook in before the entity moves. */
    protected tickMixin(tick: number) {
        super.tick(tick);
    }

    public tick(tick: number) {
        const usingAI = !this.canControlDrones || this.tank.inputs.deleted || (!this.tank.inputs.attemptingShot() && !this.tank.inputs.attemptingRepel());
        const inputs = !usingAI ? this.tank.inputs : this.ai.inputs;

        if (usingAI && this.ai.state === AIState.idle) {
            const delta = {
                x: this.positionData.values.x - this.tank.positionData.values.x,
                y: this.positionData.values.y - this.tank.positionData.values.y
            }
            const base = this.baseAccel;

            // still a bit inaccurate, works though
            let unitDist = (delta.x ** 2 + delta.y ** 2) / Drone.MAX_RESTING_RADIUS;
            if (unitDist <= 1 && this.restCycle) {
                this.baseAccel /= 6;
                this.positionData.angle += 0.01 + 0.012 * unitDist;
            } else {
                const offset = Math.atan2(delta.y, delta.x) + Math.PI / 2
                delta.x = this.tank.positionData.values.x + Math.cos(offset) * this.tank.physicsData.values.size * 1.2 - this.positionData.values.x;
                delta.y = this.tank.positionData.values.y + Math.sin(offset) * this.tank.physicsData.values.size * 1.2 - this.positionData.values.y;
                this.positionData.angle = Math.atan2(delta.y, delta.x);
                if (unitDist < 0.5) this.baseAccel /= 3;
                this.restCycle = (delta.x ** 2 + delta.y ** 2) <= 4 * (this.tank.physicsData.values.size ** 2);
            }

            if (!Entity.exists(this.barrelEntity)) this.destroy();

            this.tickMixin(tick);

            this.baseAccel = base;

            return;
        } else {
            this.positionData.angle = Math.atan2(inputs.mouse.y - this.positionData.values.y, inputs.mouse.x - this.positionData.values.x);
            this.restCycle = false
        }


        
        if (this.canControlDrones && inputs.attemptingRepel()) {
            this.positionData.angle += Math.PI; 
        }

        // So that switch tank works, as well as on death
        if (!Entity.exists(this.barrelEntity)) this.destroy();

        this.tickMixin(tick);
    }
}
`

~src/Entity/Tank/Projectile/Flame.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { TankDefinition } from "../../../Const/TankDefinitions";
import { BarrelBase } from "../TankBody";

export default class Flame extends Bullet {
    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        this.baseSpeed *= 2;
        this.baseAccel = 0;
        this.damageReduction = 1;
        
        this.physicsData.values.sides = 4;
        this.physicsData.values.absorbtionFactor = this.physicsData.values.pushFactor = 0;
        this.lifeLength = 25 * barrel.definition.bullet.lifeLength;
    }

    public destroy(animate?: boolean): void {
        super.destroy(false);
    }

    public tick(tick: number) {
        super.tick(tick);

        this.damageReduction += 1 / 25;
        this.styleData.opacity -= 1 / 25;
    }
}
`

~src/Entity/Tank/Projectile/Minion.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Drone from "./Drone";

import { InputFlags, PhysicsFlags } from "../../../Const/Enums";
import { BarrelDefinition, TankDefinition } from "../../../Const/TankDefinitions";
import { AIState, Inputs } from "../../AI";
import { BarrelBase } from "../TankBody";
import { CameraEntity } from "../../../Native/Camera";

/**
 * Barrel definition for the factory minion's barrel.
 */
 const MinionBarrelDefinition: BarrelDefinition = {
    angle: 0,
    offset: 0,
    size: 80,
    width: 50.4,
    delay: 0,
    reload: 1,
    recoil: 1.35,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 0.4,
        damage: 0.4,
        speed: 0.8,
        scatterRate: 1,
        lifeLength: 1,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * The drone class represents the minion (projectile) entity in diep.
 */
export default class Minion extends Drone implements BarrelBase {
    /** Size of the focus the minions orbit. */
    public static FOCUS_RADIUS = 850 ** 2;

    /** The minion's barrel */
    private minionBarrel: Barrel;

    /** The camera entity (used as team) of the minion. */
    public cameraEntity: CameraEntity;
    /** The reload time of the minion's barrel. */
    public reloadTime = 1;
    /** The inputs for when to shoot or not. */
    public inputs = new Inputs();

    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        const bulletDefinition = barrel.definition.bullet;

        this.inputs = this.ai.inputs;
        this.ai.viewRange = 900;
        this.usePosAngle = false;

        this.physicsData.values.sides = bulletDefinition.sides ?? 1;
        this.physicsData.values.size *= 1.2;
        
        if (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) this.physicsData.values.flags ^= PhysicsFlags.noOwnTeamCollision;
        if (this.physicsData.values.flags & PhysicsFlags.canEscapeArena) this.physicsData.values.flags ^= PhysicsFlags.canEscapeArena;

        this.physicsData.values.flags |= PhysicsFlags.onlySameOwnerCollision;

        this.cameraEntity = tank.cameraEntity;

        this.minionBarrel = new Barrel(this, MinionBarrelDefinition);
        this.ai.movementSpeed = this.ai.aimSpeed = this.baseAccel;
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    /** This allows for factory to hook in before the entity moves. */
    protected tickMixin(tick: number) {
        this.reloadTime = this.tank.reloadTime;

        const usingAI = !this.canControlDrones || !this.tank.inputs.attemptingShot() && !this.tank.inputs.attemptingRepel();
        const inputs = !usingAI ? this.tank.inputs : this.ai.inputs;

        if (usingAI && this.ai.state === AIState.idle) {
            this.movementAngle = this.positionData.values.angle;
        } else {
            this.inputs.flags |= InputFlags.leftclick;

            const dist = inputs.mouse.distanceToSQ(this.positionData.values);

            if (dist < Minion.FOCUS_RADIUS / 4) { // Half
                this.movementAngle = this.positionData.values.angle + Math.PI;
            } else if (dist < Minion.FOCUS_RADIUS) {
                this.movementAngle = this.positionData.values.angle + Math.PI / 2;
            } else this.movementAngle = this.positionData.values.angle;
        }

        super.tickMixin(tick);
    }
}
`

~src/Entity/Tank/Projectile/NecromancerSquare.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Drone from "./Drone";

import { Color, PhysicsFlags, Tank } from "../../../Const/Enums";
import { TankDefinition } from "../../../Const/TankDefinitions";
import { AI } from "../../AI";
import { BarrelBase } from "../TankBody";
import AbstractShape from "../../Shape/AbstractShape";
import LivingEntity from "../../Live";

/**
 * The drone class represents the drone (projectile) entity in diep.
 */
export default class NecromancerSquare extends Drone {
    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        const bulletDefinition = barrel.definition.bullet;
        
        this.ai = new AI(this);
        this.ai.viewRange = 900;

        this.physicsData.values.sides = 4;
        // this.physics.values.size = 55 * Math.SQRT1_2 * bulletDefinition.sizeRatio;

        // if (shape.isShiny) this.health.values.maxHealth = this.health.values.health *= 10
        this.styleData.values.color = tank.relationsData.values.team?.teamData?.values.teamColor || Color.NecromancerSquare;
        if (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) this.physicsData.values.flags ^= PhysicsFlags.noOwnTeamCollision;
        this.physicsData.values.flags |= PhysicsFlags.onlySameOwnerCollision;

        // TODO(ABC):
        // No hardcoded - unless it is hardcoded in diep (all signs show that it might be so far)
        if (tankDefinition && tankDefinition.id === Tank.Battleship) {
            this.lifeLength = 88;
        } else {
            this.lifeLength = Infinity;
            if (this.physicsData.values.flags & PhysicsFlags.canEscapeArena) this.physicsData.values.flags ^= PhysicsFlags.canEscapeArena;
        }
        this.deathAccelFactor = 1;

        this.physicsData.values.pushFactor = 4;
        this.physicsData.values.absorbtionFactor = bulletDefinition.absorbtionFactor;

        this.baseSpeed = 0;
    }

    /** Given a shape, it will create a necromancer square using stats from the shape */
    public static fromShape(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shape: LivingEntity): NecromancerSquare {
        const sunchip = new NecromancerSquare(barrel, tank, tankDefinition, shape.positionData.values.angle);
        sunchip.physicsData.values.sides = shape.physicsData.values.sides;
        sunchip.physicsData.values.size = shape.physicsData.values.size;
        sunchip.positionData.values.x = shape.positionData.values.x;
        sunchip.positionData.values.y = shape.positionData.values.y;
        sunchip.positionData.values.angle = shape.positionData.values.angle;
        
        const shapeDamagePerTick: number = shape['damagePerTick'];

        sunchip.damagePerTick *= shapeDamagePerTick / 8;
        sunchip.healthData.values.maxHealth = (sunchip.healthData.values.health *= (shapeDamagePerTick / 8));
        return sunchip;
    }
}
`

~src/Entity/Tank/Projectile/Rocket.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { InputFlags } from "../../../Const/Enums";
import { BarrelDefinition, TankDefinition } from "../../../Const/TankDefinitions";
import { Inputs } from "../../AI";
import { BarrelBase } from "../TankBody";
import { CameraEntity } from "../../../Native/Camera";

/**
 * Barrel definition for the rocketeer rocket's barrel.
 */
const RocketBarrelDefinition: BarrelDefinition = {
    angle: Math.PI,
    offset: 0,
    size: 70,
    width: 72,
    delay: 0,
    reload: 0.15,
    recoil: 3.3,
    isTrapezoid: true,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 0.3,
        damage: 3 / 5,
        speed: 1.5,
        scatterRate: 5,
        lifeLength: 0.1,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * Represents all rocketeer rockets in game.
 */
export default class Rocket extends Bullet implements BarrelBase {
    /** The rocket's barrel */
    private rocketBarrel: Barrel;

    /** The camera entity (used as team) of the rocket. */
    public cameraEntity: CameraEntity;
    /** The reload time of the rocket's barrel. */
    public reloadTime = 1;
    /** The inputs for when to shoot or not. (Rocket) */
    public inputs = new Inputs();


    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);
        
        this.cameraEntity = tank.cameraEntity;

        const rocketBarrel = this.rocketBarrel = new Barrel(this, {...RocketBarrelDefinition});
        rocketBarrel.styleData.values.color = this.styleData.values.color;
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    public tick(tick: number) {
        this.reloadTime = this.tank.reloadTime;
        if (!this.deletionAnimation && this.rocketBarrel) this.rocketBarrel.definition.width = ((this.barrelEntity.definition.width / 2) * RocketBarrelDefinition.width) / this.physicsData.values.size;

        super.tick(tick);

        if (this.deletionAnimation) return;
        // not fully accurate
        if (tick - this.spawnTick >= this.tank.reloadTime) this.inputs.flags |= InputFlags.leftclick;
        // Only accurate on current version, but we dont want that
        // if (!Entity.exists(this.barrelEntity.rootParent) && (this.inputs.flags & InputFlags.leftclick)) this.inputs.flags ^= InputFlags.leftclick; 
    }
}
`

~src/Entity/Tank/Projectile/Skimmer.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { InputFlags } from "../../../Const/Enums";
import { BarrelDefinition, TankDefinition } from "../../../Const/TankDefinitions";
import { Inputs } from "../../AI";
import { BarrelBase } from "../TankBody";
import { CameraEntity } from "../../../Native/Camera";

/**
 * Barrel definition for the skimmer skimmer's barrel.
 */
const SkimmerBarrelDefinition: BarrelDefinition = {
    angle: Math.PI / 2,
    offset: 0,
    size: 70,
    width: 42,
    delay: 0,
    reload: 0.35,
    recoil: 0,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 0.3,
        damage: 3 / 5,
        speed: 1.1,
        scatterRate: 1,
        lifeLength: 0.25,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * Represents all skimmereer skimmers in game.
 */
export default class Skimmer extends Bullet implements BarrelBase {
    /** Default speed the skimmer spins */
    public static BASE_ROTATION = 0.1;

    /** The skimmer's barrels */
    private skimmerBarrels: Barrel[];

    /** The camera entity (used as team) of the skimmer. */
    public cameraEntity: CameraEntity;
    /** The reload time of the skimmer's barrel. */
    public reloadTime = 15;
    /** The inputs for when to shoot or not. (skimmer) */
    public inputs: Inputs;

    /** The direction the bullet will rotating in. */
    private rotationPerTick = Skimmer.BASE_ROTATION;


    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number, direction: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        this.rotationPerTick = direction;

        this.cameraEntity = tank.cameraEntity;

        const skimmerBarrels: Barrel[] = this.skimmerBarrels =[];

        const s1 = new class extends Barrel {
            // Keep the width constant
            protected resize() {
                super.resize();
                this.physicsData.values.width = this.definition.width
                // this.physicsData.state.width = 0;
            }
        }(this, {...SkimmerBarrelDefinition});
        const s2Definition = {...SkimmerBarrelDefinition};
        s2Definition.angle += Math.PI
        const s2 = new class extends Barrel {
            // Keep the width constant
            protected resize() {
                super.resize();
                this.physicsData.width = this.definition.width
            }
        }(this, s2Definition);

        s1.styleData.values.color = this.styleData.values.color;
        s2.styleData.values.color = this.styleData.values.color;

        skimmerBarrels.push(s1, s2);

        this.inputs = new Inputs();
        this.inputs.flags |= InputFlags.leftclick;
    }

    public get sizeFactor() {
        return this.physicsData.values.size / 50;
    }

    public tick(tick: number) {
        this.reloadTime = this.tank.reloadTime;
        this.positionData.angle += this.rotationPerTick;
        super.tick(tick);

        if (this.deletionAnimation) return;
        // Only accurate on current version, but we dont want that
        // if (!Entity.exists(this.barrelEntity.rootParent) && (this.inputs.flags & InputFlags.leftclick)) this.inputs.flags ^= InputFlags.leftclick;
    }
}
`

~src/Entity/Tank/Projectile/Swarm.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { PhysicsFlags } from "../../../Const/Enums";
import { TankDefinition } from "../../../Const/TankDefinitions";
import Barrel from "../Barrel";
import { BarrelBase } from "../TankBody";
import Drone from "./Drone";

/**
 * The Swarm class represents the swarm (projectile) entity in diep - think BattleShip
 */
export class Swarm extends Drone {
    public constructor(barrel: Barrel,  tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);
        this.ai.viewRange = 850 * tank.sizeFactor * 2;
        this.physicsData.values.flags |= PhysicsFlags.canEscapeArena;
    }

    // TODO:
    // Add the custom resting state AI (after fixing real drone's)
}
`

~src/Entity/Tank/Projectile/Trap.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Barrel from "../Barrel";
import Bullet from "./Bullet";

import { PhysicsFlags, StyleFlags } from "../../../Const/Enums";
import { TankDefinition } from "../../../Const/TankDefinitions";
import { BarrelBase } from "../TankBody";
import { DevTank } from "../../../Const/DevTankDefinitions";
import { PI2 } from "../../../util";

/**
 * The trap class represents the trap (projectile) entity in diep.
 */
export default class Trap extends Bullet {
    /** Number of ticks before the trap cant collide with its own team. */
    protected collisionEnd = 0;

    public constructor(barrel: Barrel, tank: BarrelBase, tankDefinition: TankDefinition | null, shootAngle: number) {
        super(barrel, tank, tankDefinition, shootAngle);

        const bulletDefinition = barrel.definition.bullet;

        this.baseSpeed = (barrel.bulletAccel / 2) + 30 - Math.random() * barrel.definition.bullet.scatterRate;
        this.baseAccel = 0;
        this.physicsData.values.sides = bulletDefinition.sides ?? 3;
        if (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) this.physicsData.values.flags ^= PhysicsFlags.noOwnTeamCollision;
        this.physicsData.values.flags |= PhysicsFlags.onlySameOwnerCollision;
        this.styleData.values.flags |= StyleFlags.isStar;
        this.styleData.values.flags &= ~StyleFlags.hasNoDmgIndicator;

        this.collisionEnd = this.lifeLength >> 3;
        this.lifeLength = (600 * barrel.definition.bullet.lifeLength) >> 3;
        if (tankDefinition && tankDefinition.id === DevTank.Bouncy) this.collisionEnd = this.lifeLength - 1;
        
        // Check this?
        this.positionData.values.angle = Math.random() * PI2;
    }

    public tick(tick: number) {
        super.tick(tick);

        if (tick - this.spawnTick === this.collisionEnd) {
            if (this.physicsData.values.flags & PhysicsFlags.onlySameOwnerCollision) this.physicsData.flags ^= PhysicsFlags.onlySameOwnerCollision;
            this.physicsData.values.flags |= PhysicsFlags.noOwnTeamCollision;
        }
    }
}
`

~src/Entity/Tank/Addons.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../../Game";
import ObjectEntity from "../Object";
import AutoTurret from "./AutoTurret";

import { Color, PositionFlags, PhysicsFlags, StyleFlags } from "../../Const/Enums";
import { BarrelBase } from "./TankBody";
import { addonId, BarrelDefinition } from "../../Const/TankDefinitions";
import { AI, AIState, Inputs } from "../AI";
import LivingEntity from "../Live";
import { normalizeAngle, PI2 } from "../../util";
import { CameraEntity } from "../../Native/Camera";

/**
 * Abstract class to represent an addon in game.
 * 
 * Addons are entities added on to a tank during its creation. There are two types:
 * pre addons, and post addons. Pre addons are built before the barrels are built - for example
 * a dominator's base is a pre addon. A post addon is an addon built after the barrels are
 * built - for example the pronounciation of Ranger's barrel is a post addon.
 * 
 * Read [addons.md on diepindepth](https://github.com/ABCxFF/diepindepth/blob/main/extras/addons.md) 
 * for more details and examples.
 */
export class Addon {
    /** The current game server */
    protected game: GameServer;
    /** Helps the class determine size ratio as well as who is the owner */
    protected owner: BarrelBase;

    public constructor(owner: BarrelBase) {
        this.owner = owner;
        this.game = owner.game;
    }

    /**
     * `createGuard` method creates a smasher-like guard shape. 
     * Read (addons.md on diepindepth)[https://github.com/ABCxFF/diepindepth/blob/main/extras/addons.md]
     * for more details and examples.
     */
    protected createGuard(sides: number, sizeRatio: number, offsetAngle: number, radiansPerTick: number): GuardObject {
        return new GuardObject(this.game, this.owner, sides, sizeRatio, offsetAngle, radiansPerTick);
    }

    /**
     * `createAutoTurrets` method builds `count` auto turrets around the current
     * tank's body. 
     */
    protected createAutoTurrets(count: number) {
        const rotPerTick = AI.PASSIVE_ROTATION;
        const MAX_ANGLE_RANGE = PI2 / 4; // keep within 90º each side

        const rotator = this.createGuard(1, .1, 0, rotPerTick) as GuardObject & { turrets: AutoTurret[] };
        rotator.turrets = [];

        const ROT_OFFSET = 0.8;

        if (rotator.styleData.values.flags & StyleFlags.isVisible) rotator.styleData.values.flags ^= StyleFlags.isVisible;

        for (let i = 0; i < count; ++i) {
            const base = new AutoTurret(rotator, AutoTurretMiniDefinition);
            base.influencedByOwnerInputs = true;

            const angle = base.ai.inputs.mouse.angle = PI2 * (i / count);
            base.ai.passiveRotation = rotPerTick;
            base.ai.targetFilter = (targetPos) => {
                const pos = base.getWorldPosition();
                const angleToTarget = Math.atan2(targetPos.y - pos.y, targetPos.x - pos.x);
                
                const deltaAngle = normalizeAngle(angleToTarget - ((angle + rotator.positionData.values.angle)));

                return deltaAngle < MAX_ANGLE_RANGE || deltaAngle > (PI2 - MAX_ANGLE_RANGE);
            }

            base.positionData.values.y = this.owner.physicsData.values.size * Math.sin(angle) * ROT_OFFSET;
            base.positionData.values.x = this.owner.physicsData.values.size * Math.cos(angle) * ROT_OFFSET;

            if (base.styleData.values.flags & StyleFlags.showsAboveParent) base.styleData.values.flags ^= StyleFlags.showsAboveParent;
            base.physicsData.values.flags |= PositionFlags.absoluteRotation;

            const tickBase = base.tick;
            base.tick = (tick: number) => {
                base.positionData.y = this.owner.physicsData.values.size * Math.sin(angle) * ROT_OFFSET;
                base.positionData.x = this.owner.physicsData.values.size * Math.cos(angle) * ROT_OFFSET;

                tickBase.call(base, tick);

                if (base.ai.state === AIState.idle) base.positionData.angle = angle + rotator.positionData.values.angle;
            }

            rotator.turrets.push(base);
        }

        return rotator;
    }
}


const AutoTurretMiniDefinition: BarrelDefinition = {
    angle: 0,
    offset: 0,
    size: 55,
    width: 42 * 0.7,
    delay: 0.01,
    reload: 1,
    recoil: 0.3,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 1,
        damage: 0.4,
        speed: 1.2,
        scatterRate: 1,
        lifeLength: 1,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * A smasher-like guard object.
 * Read (addons.md on diepindepth)[https://github.com/ABCxFF/diepindepth/blob/main/extras/addons.md]
 * for more details and examples.
 */
export class GuardObject extends ObjectEntity implements BarrelBase {
    /***** From BarrelBase *****/
    public inputs: Inputs;
    public cameraEntity: CameraEntity;
    public reloadTime: number;

    /** Helps the class determine size ratio as well as who is the owner */
    protected owner: BarrelBase;
    /** To store the size ratio (in compared to the owner) */
    public sizeRatio: number;
    /** Radians per tick, how many radians the guard will rotate in a tick */
    public radiansPerTick: number;

    public constructor(game: GameServer, owner: BarrelBase, sides: number, sizeRatio: number, offsetAngle: number, radiansPerTick: number) {
        super(game);

        this.owner = owner;
        this.inputs = owner.inputs;
        this.cameraEntity = owner.cameraEntity;
        // It's weird, but it's how it works
        sizeRatio *= Math.SQRT1_2
        this.sizeRatio = sizeRatio;
        this.radiansPerTick = radiansPerTick;

        this.setParent(owner);
        this.relationsData.values.owner = owner;
        this.relationsData.values.team = owner.relationsData.values.team;

        this.styleData.values.color = Color.Border;
        this.positionData.values.flags |= PositionFlags.absoluteRotation;
        this.positionData.values.angle = offsetAngle;
        this.physicsData.values.sides = sides;
        this.reloadTime = owner.reloadTime;
        this.physicsData.values.size = owner.physicsData.values.size * sizeRatio;
    }

    /**
     * Size factor, used for calculation of the turret and base size.
     */
    get sizeFactor() {
        return this.owner.sizeFactor;
    }

    /**
     * Called (if ever) similarly to LivingEntity.onKill
     * Spreads onKill to owner
     */
    public onKill(killedEntity: LivingEntity) {
        if (!(this.owner instanceof LivingEntity)) return;
        this.owner.onKill(killedEntity);
    }

    public tick(tick: number): void {
        this.reloadTime = this.owner.reloadTime;
        this.physicsData.size = this.sizeRatio * this.owner.physicsData.values.size;
        this.positionData.angle += this.radiansPerTick;
        // It won't ever do any collisions, so no need to tick the object
        // super.tick(tick);
    }
}

/** Spikes addon. */
class SpikeAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(3, 1.3, 0, 0.17);
        this.createGuard(3, 1.3, Math.PI / 3, 0.17);
        this.createGuard(3, 1.3, Math.PI / 6, 0.17);
        this.createGuard(3, 1.3, Math.PI / 2, 0.17);
    }
}
/** Dominator's Base addon. */
class DomBaseAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(6, 1.24, 0, 0);
    }
}
/** Smasher addon. */
class SmasherAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(6, 1.15, 0, .1);
    }
}
/** Landmine addon. */
class LandmineAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(6, 1.15, 0, .1);
        this.createGuard(6, 1.15, 0, .05);
    }
}
/** The thing underneath Rocketeer and Twister addon. */
class LauncherAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        const launcher = new ObjectEntity(this.game);
        const sizeRatio = 65.5 * Math.SQRT2 / 50;
        const widthRatio = 33.6 / 50;
        const size = this.owner.physicsData.values.size;

        launcher.setParent(this.owner);
        launcher.relationsData.values.owner = this.owner;
        launcher.relationsData.values.team = this.owner.relationsData.values.team;

        launcher.physicsData.values.size = sizeRatio * size;
        launcher.physicsData.values.width = widthRatio * size;
        launcher.positionData.values.x = launcher.physicsData.values.size / 2;

        launcher.styleData.values.color = Color.Barrel;
        launcher.physicsData.values.flags |= PhysicsFlags.isTrapezoid;
        launcher.physicsData.values.sides = 2;

        launcher.tick = () => {
            const size = this.owner.physicsData.values.size;

            launcher.physicsData.size = sizeRatio * size;
            launcher.physicsData.width = widthRatio * size;
            launcher.positionData.x = launcher.physicsData.values.size / 2;
        }
    }
}
/** Centered Auto Turret addon. */
class AutoTurretAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        new AutoTurret(owner);
    }
}

/** Smasher + Centered Auto Turret addon. */
class AutoSmasherAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(6, 1.15, 0, .1);
        new AutoTurret(owner);
    }
}
/** 5 Auto Turrets */
class Auto5Addon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createAutoTurrets(5);
    }
}
/** 3 Auto Turrets */
class Auto3Addon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createAutoTurrets(3);
    }
}
/** The thing above ranger's barrel. */
class PronouncedAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        const pronounce = new ObjectEntity(this.game);
        const sizeRatio = 50 / 50;
        const widthRatio = 42 / 50;
        const offsetRatio = 40 / 50;
        const size = this.owner.physicsData.values.size;

        pronounce.setParent(this.owner);
        pronounce.relationsData.values.owner = this.owner;
        pronounce.relationsData.values.team = this.owner.relationsData.values.team

        pronounce.physicsData.values.size = sizeRatio * size;
        pronounce.physicsData.values.width = widthRatio * size;
        pronounce.positionData.values.x = offsetRatio * size;
        pronounce.positionData.values.angle = Math.PI;

        pronounce.styleData.values.color = Color.Barrel;
        pronounce.physicsData.values.flags |= PhysicsFlags.isTrapezoid;
        pronounce.physicsData.values.sides = 2;

        pronounce.tick = () => {
            const size = this.owner.physicsData.values.size;

            pronounce.physicsData.size = sizeRatio * size;
            pronounce.physicsData.width = widthRatio * size;
            pronounce.positionData.x = offsetRatio * size;
        }
    }
}
/** The thing above Gunner + Destroyer Dominator's barrel. */
class PronouncedDomAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        const pronounce = new ObjectEntity(this.game);
        const sizeRatio = 22 / 50;
        const widthRatio = 35 / 50;
        const offsetRatio = 50 / 50;
        const size = this.owner.physicsData.values.size;

        pronounce.setParent(this.owner);
        pronounce.relationsData.values.owner = this.owner;
        pronounce.relationsData.values.team = this.owner.relationsData.values.team

        pronounce.physicsData.values.size = sizeRatio * size;
        pronounce.physicsData.values.width = widthRatio * size;
        pronounce.positionData.values.x = offsetRatio * size;
        pronounce.positionData.values.angle = Math.PI;
        
        pronounce.styleData.values.color = Color.Barrel;
        pronounce.physicsData.values.flags |= PhysicsFlags.isTrapezoid;
        pronounce.physicsData.values.sides = 2;

        pronounce.tick = () => {
            const size = this.owner.physicsData.values.size;

            pronounce.physicsData.size = sizeRatio * size;
            pronounce.physicsData.width = widthRatio * size;
            pronounce.positionData.x = offsetRatio * size;
        }
    }
}
/** Weird spike addon. Based on the arrasio Original. */
class WeirdSpikeAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(3, 1.5, 0, 0.17);
        this.createGuard(3, 1.5, 0, -0.16);
    }
}
/** 2 Auto Turrets */
class Auto2Addon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createAutoTurrets(2);
    }
}
/** 7 Auto Turrets */
class Auto7Addon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createAutoTurrets(7);
    }
}

/** Centered Auto Rocket addon. */
class AutoRocketAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        const base = new AutoTurret(owner, {
            angle: 0,
            offset: 0,
            size: 40,
            width: 26.25,
            delay: 0,
            reload: 2,
            recoil: 0.75,
            isTrapezoid: true,
            trapezoidDirection: 3.141592653589793,
            addon: null,
            bullet: {
                type: "rocket",
                sizeRatio: 1,
                health: 2.5,
                damage: 0.5,
                speed: 0.3,
                scatterRate: 1,
                lifeLength: 0.75,
                absorbtionFactor: 0.1
            }
        });

        new LauncherAddon(base);

        base.turret.styleData.zIndex += 2;
    }
}
/** SPIESK addon. */
class SpieskAddon extends Addon {
    public constructor(owner: BarrelBase) {
        super(owner);

        this.createGuard(4, 1.3, 0, 0.17);
        this.createGuard(4, 1.3, Math.PI / 6, 0.17);
        this.createGuard(4, 1.3, 2 * Math.PI / 6, 0.17);
    }
}

/**
 * All addons in the game by their ID.
 */
export const AddonById: Record<addonId, typeof Addon | null> = {
    spike: SpikeAddon,
    dombase: DomBaseAddon,
    launcher: LauncherAddon,
    dompronounced: PronouncedDomAddon,
    auto5: Auto5Addon,
    auto3: Auto3Addon,
    autosmasher: AutoSmasherAddon,
    pronounced: PronouncedAddon,
    smasher: SmasherAddon,
    landmine: LandmineAddon,
    autoturret: AutoTurretAddon,
    // not part of diep
    weirdspike: WeirdSpikeAddon,
    auto7: Auto7Addon,
    auto2: Auto2Addon,
    autorocket: AutoRocketAddon,
    spiesk: SpieskAddon,
}
`

~src/Entity/Tank/AutoTurret.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import ObjectEntity from "../Object";
import Barrel from "./Barrel";

import { BarrelBase } from "./TankBody";
import { Color, InputFlags, PositionFlags, NameFlags, PhysicsFlags, Stat, StyleFlags } from "../../Const/Enums";
import { BarrelDefinition } from "../../Const/TankDefinitions";
import { AI, AIState, Inputs } from "../AI";
import { Entity } from "../../Native/Entity";
import { NameGroup } from "../../Native/FieldGroups";
import LivingEntity from "../Live";
import { CameraEntity } from "../../Native/Camera";

export const AutoTurretDefinition: BarrelDefinition = {
    angle: 0,
    offset: 0,
    size: 55,
    width: 42 * 0.7,
    delay: 0.01,
    reload: 1,
    recoil: 0.3,
    isTrapezoid: false,
    trapezoidDirection: 0,
    addon: null,
    bullet: {
        type: "bullet",
        health: 1,
        damage: 0.3,
        speed: 1.2,
        scatterRate: 1,
        lifeLength: 1,
        sizeRatio: 1,
        absorbtionFactor: 1
    }
};

/**
 * Auto Turret Barrel + Barrel Base
 */
export default class AutoTurret extends ObjectEntity {
    // TODO(ABC):
    // Maybe just remove this
    /** For mounted turret name to show up on Auto Turrets. */
    public nameData: NameGroup = new NameGroup(this);

    /** Barrel's owner (Tank-like object). */
    private owner: BarrelBase;
    /** Actual turret / barrel. */
    public turret: Barrel;
    /** The AI controlling the turret. */
    public ai: AI;
    /** The AI's inputs, for determining whether to shoot or not. */
    public inputs: Inputs;
    /** Camera entity / team of the turret. */
    public cameraEntity: CameraEntity;

    /** If set to true, (auto 5 auto 3), player can influence auto turret's */
    public influencedByOwnerInputs: boolean = false;

    /** The reload time of the turret. */
    public reloadTime = 15;
    /** The size of the auto turret base */
    public baseSize: number;

    public constructor(owner: BarrelBase, turretDefinition: BarrelDefinition = AutoTurretDefinition, baseSize: number = 25) {
        super(owner.game);

        this.cameraEntity = owner.cameraEntity;
        this.ai = new AI(this);
        this.ai.doAimPrediction = true;
        this.inputs = this.ai.inputs;

        this.owner = owner;
        
        this.setParent(owner);
        this.relationsData.values.owner = owner;

        this.relationsData.values.team = owner.relationsData.values.team;

        this.physicsData.values.sides = 1;
        this.baseSize = baseSize;
        this.physicsData.values.size = this.baseSize * this.sizeFactor;

        this.styleData.values.color = Color.Barrel;
        this.styleData.values.flags |= StyleFlags.showsAboveParent;

        this.positionData.values.flags |= PositionFlags.absoluteRotation;

        this.nameData.values.name = "Mounted Turret";
        this.nameData.values.flags |= NameFlags.hiddenName;

        this.turret = new Barrel(this, turretDefinition);
        this.turret.physicsData.values.flags |= PhysicsFlags.doChildrenCollision;
    }
    
    /**
     * Size factor, used for calculation of the turret and base size.
     */
    public get sizeFactor() {
        return this.owner.sizeFactor;
    }

    /**
     * Called similarly to LivingEntity.onKill
     * Spreads onKill to owner
     */
    public onKill(killedEntity: LivingEntity) {
        if (!(this.owner instanceof LivingEntity)) return;
        this.owner.onKill(killedEntity);
    }

    public tick(tick: number) {
        if (this.inputs !== this.ai.inputs) this.inputs = this.ai.inputs;

        this.relationsData.values.team = this.owner.relationsData.values.team;

        if (this.ai.state === AIState.hasTarget) this.ai.passiveRotation = Math.random() < .5 ? AI.PASSIVE_ROTATION : -AI.PASSIVE_ROTATION;

        this.physicsData.size = this.baseSize * this.sizeFactor;

        this.ai.aimSpeed = this.turret.bulletAccel;
        // Top Speed
        this.ai.movementSpeed = 0;

        this.reloadTime = this.owner.reloadTime;

        let useAI = !(this.influencedByOwnerInputs && (this.owner.inputs.attemptingRepel() || this.owner.inputs.attemptingShot()));
        if (!useAI) {
            const {x, y} = this.getWorldPosition();
            let flip = this.owner.inputs.attemptingRepel() ? -1 : 1;
            const deltaPos = {x: (this.owner.inputs.mouse.x - x) * flip, y: (this.owner.inputs.mouse.y - y) * flip}

            if (this.ai.targetFilter({x: x + deltaPos.x, y: y + deltaPos.y}) === false) useAI = true;
            else {
                // if (this.owner.inputs.attemptingRepel()) this.inputs.flags |= InputFlags.rightclick;
                this.inputs.flags |= InputFlags.leftclick;
                this.positionData.angle = Math.atan2(deltaPos.y, deltaPos.x);
                this.ai.state = AIState.hasTarget;
            }
        }
        if (useAI) {
            if (this.ai.state === AIState.idle) {
                this.positionData.angle += this.ai.passiveRotation;
                this.turret.attemptingShot = false;
            } else {
                // Uh. Yeah
                const {x, y} = this.getWorldPosition();
                this.positionData.angle = Math.atan2(this.ai.inputs.mouse.y - y, this.ai.inputs.mouse.x - x);
            }
        }
    }
}
`

~src/Entity/Tank/Barrel.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as util from "../../util";

import Bullet from "./Projectile/Bullet";
import Trap from "./Projectile/Trap";
import Drone from "./Projectile/Drone";
import Rocket from "./Projectile/Rocket";
import Skimmer from "./Projectile/Skimmer";
import Minion from "./Projectile/Minion";
import ObjectEntity from "../Object";
import TankBody, { BarrelBase } from "./TankBody";

import { Color, PositionFlags, PhysicsFlags, BarrelFlags, Stat, Tank } from "../../Const/Enums";
import { BarrelGroup } from "../../Native/FieldGroups";
import { BarrelDefinition, TankDefinition } from "../../Const/TankDefinitions";
import { DevTank } from "../../Const/DevTankDefinitions";
import Flame from "./Projectile/Flame";
import MazeWall from "../Misc/MazeWall";
import CrocSkimmer from "./Projectile/CrocSkimmer";
import { BarrelAddon, BarrelAddonById } from "./BarrelAddons";
import { Swarm } from "./Projectile/Swarm";
import NecromancerSquare from "./Projectile/NecromancerSquare";
/**
 * Class that determines when barrels can shoot, and when they can't.
 */
export class ShootCycle {
    /** The barrel this cycle is keeping track of. */
    private barrelEntity: Barrel;
    /** The current position in the cycle. */
    private pos: number;
    /** The last known reload time of the barrel. */
    private reloadTime: number;

    public constructor(barrel: Barrel) {
        this.barrelEntity = barrel;
        this.barrelEntity.barrelData.reloadTime = this.barrelEntity.tank.reloadTime * this.barrelEntity.definition.reload;
        this.reloadTime = this.pos = barrel.barrelData.values.reloadTime;
    }

    public tick() {
        const reloadTime = this.barrelEntity.tank.reloadTime * this.barrelEntity.definition.reload;
        if (reloadTime !== this.reloadTime) {
            this.pos *= reloadTime / this.reloadTime;
            this.reloadTime = reloadTime;
        }

        const alwaysShoot = (this.barrelEntity.definition.forceFire) || (this.barrelEntity.definition.bullet.type === 'drone') || (this.barrelEntity.definition.bullet.type === 'minion');

        if (this.pos >= reloadTime) {
            // When its not shooting dont shoot, unless its a drone
            if (!this.barrelEntity.attemptingShot && !alwaysShoot) {
                this.pos = reloadTime;
                return;
            }
            // When it runs out of drones, dont shoot
            if (typeof this.barrelEntity.definition.droneCount === 'number' && this.barrelEntity.droneCount >= this.barrelEntity.definition.droneCount) {
                this.pos = reloadTime;
                return;
            }
        }

        if (this.pos >= reloadTime * (1 + this.barrelEntity.definition.delay)) {
            this.barrelEntity.barrelData.reloadTime = reloadTime;
            this.barrelEntity.shoot();
            this.pos = reloadTime * this.barrelEntity.definition.delay;
        }

        this.pos += 1;
    }
}

/**
 * The barrel class containing all barrel related data.
 * - Converts barrel definitions to diep objects
 * - Will contain shooting logic (or interact with it)
 */
export default class Barrel extends ObjectEntity {
    /** The raw data defining the barrel. */
    public definition: BarrelDefinition;
    /** The owner / tank / parent of the barrel.  */
    public tank: BarrelBase;
    /** The cycle at which the barrel can shoot. */
    public shootCycle: ShootCycle;
    /** Whether or not the barrel is cycling the shoot cycle. */
    public attemptingShot = false;
    /** Bullet base accel. Used for AI and bullet speed determination. */
    public bulletAccel = 20;
    /** Number of drones that this barrel shot that are still alive. */
    public droneCount = 0;

    /** The barrel's addons */
    public addons: BarrelAddon[] = [];

    /** Always existant barrel field group, present on all barrels. */
    public barrelData: BarrelGroup = new BarrelGroup(this);

    public constructor(owner: BarrelBase, barrelDefinition: BarrelDefinition) {
        super(owner.game);

        this.tank = owner;
        this.definition = barrelDefinition;

        // Begin Loading Definition
        this.styleData.values.color = this.definition.color ?? Color.Barrel;
        this.physicsData.values.sides = 2;
        if (barrelDefinition.isTrapezoid) this.physicsData.values.flags |= PhysicsFlags.isTrapezoid;

        this.setParent(owner);
        this.relationsData.values.owner = owner;
        this.relationsData.values.team = owner.relationsData.values.team;

        const sizeFactor = this.tank.sizeFactor;
        const size = this.physicsData.values.size = this.definition.size * sizeFactor;

        this.physicsData.values.width = this.definition.width * sizeFactor;
        this.positionData.values.angle = this.definition.angle + (this.definition.trapezoidDirection);
        this.positionData.values.x = Math.cos(this.definition.angle) * (size / 2 + (this.definition.distance || 0)) - Math.sin(this.definition.angle) * this.definition.offset * sizeFactor;
        this.positionData.values.y = Math.sin(this.definition.angle) * (size / 2 + (this.definition.distance || 0)) + Math.cos(this.definition.angle) * this.definition.offset * sizeFactor;

        // addons are below barrel, use StyleFlags.aboveParent to go above parent
        if (barrelDefinition.addon) {
            const AddonConstructor = BarrelAddonById[barrelDefinition.addon];
            if (AddonConstructor) this.addons.push(new AddonConstructor(this));
        }

        this.barrelData.values.trapezoidDirection = barrelDefinition.trapezoidDirection;
        this.shootCycle = new ShootCycle(this);

        this.bulletAccel = (20 + (owner.cameraEntity.cameraData?.values.statLevels.values[Stat.BulletSpeed] || 0) * 3) * barrelDefinition.bullet.speed;
    }

    /** Shoots a bullet from the barrel. */
    public shoot() {
        this.barrelData.flags ^= BarrelFlags.hasShot;

        // No this is not correct
        const scatterAngle = (Math.PI / 180) * this.definition.bullet.scatterRate * (Math.random() - .5) * 10;
        let angle = this.definition.angle + scatterAngle + this.tank.positionData.values.angle;

        // Map angles unto
        // let e: Entity | null | undefined = this;
        // while (!((e?.position?.flags || 0) & MotionFlags.absoluteRotation) && (e = e.relations?.values.parent) instanceof ObjectEntity) angle += e.position.values.angle;

        this.rootParent.addAcceleration(angle + Math.PI, this.definition.recoil * 2);

        let tankDefinition: TankDefinition | null = null;

        if (this.rootParent instanceof TankBody) tankDefinition = this.rootParent.definition;


        switch (this.definition.bullet.type) {
            case "skimmer":
                new Skimmer(this, this.tank, tankDefinition, angle, this.tank.inputs.attemptingRepel() ? -Skimmer.BASE_ROTATION : Skimmer.BASE_ROTATION);
                break;
            case "rocket":
                new Rocket(this, this.tank, tankDefinition, angle);
                break;
            case 'bullet': {
                const bullet = new Bullet(this, this.tank, tankDefinition, angle);

                if (tankDefinition && (tankDefinition.id === Tank.ArenaCloser || tankDefinition.id === DevTank.Squirrel)) bullet.positionData.flags |= PositionFlags.canMoveThroughWalls;
                break;
            }
            case 'trap':
                new Trap(this, this.tank, tankDefinition, angle);
                break;
            case 'drone':
                new Drone(this, this.tank, tankDefinition, angle);
                break;
            case 'necrodrone':
                new NecromancerSquare(this, this.tank, tankDefinition, angle);
                break;
            case 'swarm':
                new Swarm(this, this.tank, tankDefinition, angle);
                break;
            case 'minion':
                new Minion(this, this.tank, tankDefinition, angle);
                break;
            case 'flame':
                new Flame(this, this.tank, tankDefinition, angle);
                break;
            case 'wall': {
                let w = new MazeWall(this.game, Math.round(this.tank.inputs.mouse.x / 50) * 50, Math.round(this.tank.inputs.mouse.y / 50) * 50, 250, 250);
                setTimeout(() => {
                    w.destroy();
                }, 60 * 1000);
                break;
            }
            case "croc": 
                new CrocSkimmer(this, this.tank, tankDefinition, angle);
                break;
            default:
                util.log('Ignoring attempt to spawn projectile of type ' + this.definition.bullet.type);
                break;
        }

    }


    /** Resizes the barrel; when the tank gets bigger, the barrel must as well. */
    protected resize() {
        const sizeFactor = this.tank.sizeFactor;
        const size = this.physicsData.size = this.definition.size * sizeFactor;

        this.physicsData.width = this.definition.width * sizeFactor;
        this.positionData.angle = this.definition.angle + (this.definition.trapezoidDirection);
        this.positionData.x = Math.cos(this.definition.angle) * (size / 2 + (this.definition.distance || 0)) - Math.sin(this.definition.angle) * this.definition.offset * sizeFactor;
        this.positionData.y = Math.sin(this.definition.angle) * (size / 2 + (this.definition.distance || 0)) + Math.cos(this.definition.angle) * this.definition.offset * sizeFactor;

        // Updates bullet accel too
        this.bulletAccel = (20 + (this.tank.cameraEntity.cameraData?.values.statLevels.values[Stat.BulletSpeed] || 0) * 3) * this.definition.bullet.speed;
    }

    public tick(tick: number) {
        this.resize();

        this.relationsData.values.team = this.tank.relationsData.values.team;

        if (!this.tank.rootParent.deletionAnimation){
            this.attemptingShot = this.tank.inputs.attemptingShot();
            this.shootCycle.tick();
        }

        super.tick(tick);
    }
}
`

~src/Entity/Tank/BarrelAddons.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import { Color, PhysicsFlags } from "../../Const/Enums";
import { barrelAddonId } from "../../Const/TankDefinitions";
import GameServer from "../../Game";
import ObjectEntity from "../Object";
import Barrel from "./Barrel";

/**
 * Abstract class to represent a barrel's addon in game.
 * 
 * For more information on an addon, see Addons.ts - BarrelAddons are the same thing except they are applied on the barrel after it is made.
 * 
 * Read [addons.md on diepindepth](https://github.com/ABCxFF/diepindepth/blob/main/extras/addons.md) 
 * for more details and examples.
 */

export class BarrelAddon {
    /** The current game server */
    protected game: GameServer; 
    /** Helps the class determine size  */
    protected owner: Barrel;

    public constructor(owner: Barrel) {
        this.owner = owner;
        this.game = owner.game;
    }
}

/**
 * Entity attached to the edge of a trapper barrel
 */
export class TrapLauncher extends ObjectEntity {
    /** The barrel that this trap launcher is placed on. */
    public barrelEntity: Barrel;

    /** Resizes the trap launcher; when its barrel owner gets bigger, the trap launcher must as well. */
    public constructor(barrel: Barrel) {
        super(barrel.game);

        this.barrelEntity = barrel;
        this.setParent(barrel);
        this.relationsData.values.team = barrel;
        this.physicsData.values.flags = PhysicsFlags.isTrapezoid | PhysicsFlags.doChildrenCollision;
        this.styleData.values.color = Color.Barrel;

        this.physicsData.values.sides = 2;
        this.physicsData.values.width = barrel.physicsData.values.width;
        this.physicsData.values.size = barrel.physicsData.values.width * (20 / 42);
        this.positionData.values.x = (barrel.physicsData.values.size + this.physicsData.values.size) / 2;
    }

    public resize() {
        this.physicsData.sides = 2;
        this.physicsData.width = this.barrelEntity.physicsData.values.width;
        this.physicsData.size = this.barrelEntity.physicsData.values.width * (20 / 42);
        this.positionData.x = (this.barrelEntity.physicsData.values.size + this.physicsData.values.size) / 2;
    }


    public tick(tick: number) {
        super.tick(tick);

        this.resize();
    }
}

/** Trap launcher - added onto traps */
export class TrapLauncherAddon extends BarrelAddon {
    /** The actual trap launcher entity */
    public launcherEntity: TrapLauncher;

    public constructor(owner: Barrel) {
        super(owner);

        this.launcherEntity = new TrapLauncher(owner);
    }
}

export class PurpleBarrelAddon extends BarrelAddon {
    public constructor(owner: Barrel) {
        super(owner);
        owner.styleData.color = Color.TeamPurple;
    }
}

/**
 * All barrel addons in the game by their ID.
 */
 export const BarrelAddonById: Record<barrelAddonId, typeof BarrelAddon | null> = {
    trapLauncher: TrapLauncherAddon,
    purplebarrel: PurpleBarrelAddon
}
`

~src/Entity/Tank/TankBody.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as util from "../../util";

import Square from "../Shape/Square";
import NecromancerSquare from "./Projectile/NecromancerSquare";
import GameServer from "../../Game";
import ClientCamera, { CameraEntity } from "../../Native/Camera";
import LivingEntity from "../Live";
import ObjectEntity from "../Object";
import Barrel from "./Barrel";

import { Color, StyleFlags, StatCount, Tank, CameraFlags, Stat, InputFlags, PhysicsFlags, PositionFlags } from "../../Const/Enums";
import { Entity } from "../../Native/Entity";
import { NameGroup, ScoreGroup } from "../../Native/FieldGroups";
import { Addon, AddonById } from "./Addons";
import { getTankById, TankDefinition } from "../../Const/TankDefinitions";
import { DevTank } from "../../Const/DevTankDefinitions";
import { Inputs } from "../AI";
import AbstractBoss from "../Boss/AbstractBoss";
import { ArenaState } from "../../Native/Arena";
import { AccessLevel, maxPlayerLevel } from "../../config";

/**
 * Abstract type of entity which barrels can connect to.
 * - `sizeFactor` is required and must be a `number`
 * - `cameraEntity` is required and must be a `Camera`
 */
export type BarrelBase = ObjectEntity & { sizeFactor: number, cameraEntity: CameraEntity, reloadTime: number, inputs: Inputs };

/**
 * The Tank Body, which could also be called the Player class, converts defined
 * tank data into diep entities. Controls speeds, barrels, addons, and names.
 * Created for each spawn.
 */
export default class TankBody extends LivingEntity implements BarrelBase {
    /** Always existant name field group, present on all tanks. */
    public nameData: NameGroup = new NameGroup(this);
    /** Always existant score field group, present on all tanks. */
    public scoreData: ScoreGroup = new ScoreGroup(this);

    /** The camera entity which stores stats and level data about the tank. */
    public cameraEntity: CameraEntity;
    /** The inputs of the client, lets the barrels know when to shoot etc. */
    public inputs: Inputs;

    /** The tank's barrels, if any. */
    public barrels: Barrel[] = [];
    /** The tank's addons, if any. */
    private addons: Addon[] = [];

    /** Size of the tank at level 1. Defined by tank loader.  */
    public baseSize = 50;
    /** The definition of the currentTank */
    public definition: TankDefinition = getTankById(Tank.Basic) as TankDefinition;
    /** Reload time base, used for barrel's reloads. */
    public reloadTime = 15;
    /** The current tank definition / tank id. */
    private _currentTank: Tank | DevTank = Tank.Basic;
    /** Sets tanks to be invulnerable - example, godmode, or AC */
    public isInvulnerable: boolean = false;

    public constructor(game: GameServer, camera: CameraEntity, inputs: Inputs) {
        super(game);
        this.cameraEntity = camera;
        this.inputs = inputs;

        this.physicsData.values.size = 50;
        this.physicsData.values.sides = 1;
        this.styleData.values.color = Color.Tank;

        this.relationsData.values.team = camera;
        this.relationsData.values.owner = camera;

        this.cameraEntity.cameraData.spawnTick = game.tick;
        this.cameraEntity.cameraData.flags |= CameraFlags.showingDeathStats;
        this.cameraEntity.cameraData.killedBy = "";

        // spawn protection
        this.styleData.values.flags |= StyleFlags.isFlashing;
        this.damageReduction = 0;
        if (this.game.playersOnMap) this.physicsData.values.flags |= PhysicsFlags.showsOnMap;

        this.damagePerTick = 20;
        this.setTank(Tank.Basic);
    }

    /** The active change in size from the base size to the current. Contributes to barrel and addon sizes. */
    public get sizeFactor() {
        return this.physicsData.values.size / this.baseSize;
    }

    /** The current tank type / tank id. */
    public get currentTank() {
        return this._currentTank;
    }

    /** This method allows for changing the current tank. */
    public setTank(id: Tank | DevTank) {
        // Delete old barrels and addons
        for (let i = 0; i < this.children.length; ++i) {
            this.children[i].isChild = false;
            this.children[i].delete();
        }
        this.children = [];
        this.barrels = [];
        this.addons = [];

        // Get the new tank data
        const tank = getTankById(id);
        const camera = this.cameraEntity;

        if (!tank) throw new TypeError("Invalid tank ID");
        this.definition = tank;
        if (!Entity.exists(camera)) throw new Error("No camera");

        this.physicsData.sides = tank.sides;
        this.styleData.opacity = 1;

        for (let i: Stat = 0; i < StatCount; ++i) {
            const {name, max} = tank.stats[i];

            camera.cameraData.statLimits[i] = max;
            camera.cameraData.statNames[i] = name;
            if (camera.cameraData.statLevels[i] > max) {
                camera.cameraData.statsAvailable += (camera.cameraData.statLevels[i] - (camera.cameraData.statLevels[i] = max));
            }
        }

        // Size ratios
        this.baseSize = tank.baseSizeOverride ?? tank.sides === 4 ? Math.SQRT2 * 32.5 : tank.sides === 16 ? Math.SQRT2 * 25 : 50;
        this.physicsData.absorbtionFactor = this.isInvulnerable ? 0 : tank.absorbtionFactor;
        if (tank.absorbtionFactor === 0) this.positionData.flags |= PositionFlags.canMoveThroughWalls;
        else if (this.positionData.flags & PositionFlags.canMoveThroughWalls) this.positionData.flags ^= PositionFlags.canMoveThroughWalls;

        camera.cameraData.tank = this._currentTank = id;
        if (tank.upgradeMessage && camera instanceof ClientCamera) camera.client.notify(tank.upgradeMessage);

        // Build addons, then tanks, then addons.
        const preAddon = tank.preAddon;
        if (preAddon) {
            const AddonConstructor = AddonById[preAddon];
            if (AddonConstructor) this.addons.push(new AddonConstructor(this));
        }

        for (const barrel of tank.barrels) {
            this.barrels.push(new Barrel(this, barrel));
        }

        const postAddon = tank.postAddon;
        if (postAddon) {
            const AddonConstructor = AddonById[postAddon];
            if (AddonConstructor) this.addons.push(new AddonConstructor(this));
        }

        // Yeah, yeah why not
        this.cameraEntity.cameraData.tankOverride = tank.name;
        camera.setFieldFactor(tank.fieldFactor);
    }
    /** See LivingEntity.onKill */
    public onKill(entity: LivingEntity) {
        if (Entity.exists(this.cameraEntity.cameraData.values.player)) this.scoreData.score = this.cameraEntity.cameraData.score += entity.scoreReward;

        if (entity instanceof TankBody && entity.scoreReward && Math.max(this.cameraEntity.cameraData.values.level, maxPlayerLevel) - entity.cameraEntity.cameraData.values.level <= 20 || entity instanceof AbstractBoss) {
            if (this.cameraEntity instanceof ClientCamera) this.cameraEntity.client.notify("You've killed " + (entity.nameData.values.name || "an unnamed tank"));
        }

        // TODO(ABC):
        // This is actually not how necromancers claim squares.
        if (entity instanceof Square && this.definition.flags.canClaimSquares && this.barrels.length) {
            // If can claim, pick a random barrel that has drones it can still shoot, then shoot
            const MAX_DRONES_PER_BARREL = 11 + this.cameraEntity.cameraData.values.statLevels.values[Stat.Reload];
            const barrelsToShoot = this.barrels.filter((e) => e.definition.bullet.type === "necrodrone" && e.droneCount < MAX_DRONES_PER_BARREL);

            if (barrelsToShoot.length) {
                const barrelToShoot = barrelsToShoot[~~(Math.random()*barrelsToShoot.length)];

                // No destroy it on the next tick to make it look more like the way diep does it.
                entity.destroy(true);
                if (entity.deletionAnimation) {
                    entity.deletionAnimation.frame = 0;
                    entity.styleData.opacity = 1;
                }

                const sunchip = NecromancerSquare.fromShape(barrelToShoot, this, this.definition, entity);
            }
        }
    }

    /** See TankBody.isInvulnerable */
    public setInvulnerability(invulnerable: boolean) {
        if (this.styleData.flags & StyleFlags.isFlashing) this.styleData.flags ^= StyleFlags.isFlashing;

        if (this.isInvulnerable === invulnerable) return;
      
        if (invulnerable) {
            this.damageReduction = 0.0;
            this.physicsData.absorbtionFactor = 0.0;
        } else {
            this.damageReduction = 1.0;
            this.physicsData.absorbtionFactor = this.definition.absorbtionFactor;
        }
      
        this.isInvulnerable = invulnerable;
    }

    /** See LivingEntity.onDeath */
   public onDeath(killer: LivingEntity) {
        if (!(this.cameraEntity instanceof ClientCamera)) return this.cameraEntity.delete();
        if (!(this.cameraEntity.cameraData.player === this)) return;
        this.cameraEntity.spectatee = killer;
        this.cameraEntity.cameraData.killedBy = (killer.nameData && killer.nameData.values.name) || "";
    }

    /** Destroys the tank body. Extends LivivingEntity.destroy(animate); */
    public destroy(animate=true) {
        // Stats etc
        if (!animate && Entity.exists(this.cameraEntity)) {
            if (this.cameraEntity.cameraData.player === this) {
                this.cameraEntity.cameraData.deathTick = this.game.tick;
                this.cameraEntity.cameraData.respawnLevel = Math.min(Math.max(this.cameraEntity.cameraData.values.level - 1, 1), Math.floor(Math.sqrt(this.cameraEntity.cameraData.values.level) * 3.2796));
            }

            // Wipe this nonsense
            this.barrels = [];
            this.addons = [];
        }
        super.destroy(animate);
    }

    public delete() {
        if (this.cameraEntity.cameraData.values.player === this) this.cameraEntity.cameraData.FOV = 0.4;
        super.delete();
    }

    public tick(tick: number) {
        this.positionData.angle = Math.atan2(this.inputs.mouse.y - this.positionData.values.y, this.inputs.mouse.x - this.positionData.values.x);

        if (this.isInvulnerable) {
            if (this.game.clients.size !== 1 || this.game.arena.state !== ArenaState.OPEN) {
                // not for ACs
                if (this.cameraEntity instanceof ClientCamera && this.cameraEntity.client.accessLevel < AccessLevel.FullAccess) this.setInvulnerability(false);
            }
        }
        if (!this.deletionAnimation && !this.inputs.deleted) this.physicsData.size = this.baseSize * this.cameraEntity.sizeFactor;
        else this.regenPerTick = 0;

        super.tick(tick);

        // If we're currently in a deletion animation
        if (this.deletionAnimation) return;

        if (this.inputs.deleted) {
            if (this.cameraEntity.cameraData.values.level <= 5) return this.destroy();
            this.lastDamageTick = tick;
            this.healthData.health -= 2 + this.healthData.values.maxHealth / 500;

            if (this.isInvulnerable) this.setInvulnerability(false);
            if (this.styleData.values.flags & StyleFlags.isFlashing) {
                this.styleData.flags ^= StyleFlags.isFlashing;
                this.damageReduction = 1.0;
            }
            return;
            // return this.destroy();
        }

        if (this.definition.flags.zoomAbility && (this.inputs.flags & InputFlags.rightclick)) {
            if (!(this.cameraEntity.cameraData.values.flags & CameraFlags.usesCameraCoords)) {
                const angle = Math.atan2(this.inputs.mouse.y - this.positionData.values.y, this.inputs.mouse.x - this.positionData.values.x)
                this.cameraEntity.cameraData.cameraX = Math.cos(angle) * 1500 + this.positionData.values.x;
                this.cameraEntity.cameraData.cameraY = Math.sin(angle) * 1500 + this.positionData.values.y;
                this.cameraEntity.cameraData.flags |= CameraFlags.usesCameraCoords;
            }
        } else if (this.cameraEntity.cameraData.values.flags & CameraFlags.usesCameraCoords) this.cameraEntity.cameraData.flags ^= CameraFlags.usesCameraCoords;

        if (this.definition.flags.invisibility) {

            if (this.inputs.flags & InputFlags.leftclick) this.styleData.opacity += this.definition.visibilityRateShooting;
            if (this.inputs.flags & (InputFlags.up | InputFlags.down | InputFlags.left | InputFlags.right) || this.inputs.movement.x || this.inputs.movement.y) this.styleData.opacity += this.definition.visibilityRateMoving;
           
            this.styleData.opacity -= this.definition.invisibilityRate;

            this.styleData.opacity = util.constrain(this.styleData.values.opacity, 0, 1);
        }


        // Update stat related
        updateStats: {
            // Damage
            this.damagePerTick = this.cameraEntity.cameraData.statLevels[Stat.BodyDamage] * 6 + 20;
            if (this._currentTank === Tank.Spike) this.damagePerTick *= 1.5;

            // Max Health
            const maxHealthCache = this.healthData.values.maxHealth;

            this.healthData.maxHealth = this.definition.maxHealth + 2 * (this.cameraEntity.cameraData.values.level - 1) + this.cameraEntity.cameraData.values.statLevels.values[Stat.MaxHealth] * 20;
            if (this.healthData.values.health === maxHealthCache) this.healthData.health = this.healthData.maxHealth; // just in case
            else if (this.healthData.values.maxHealth !== maxHealthCache) {
                this.healthData.health *= this.healthData.values.maxHealth / maxHealthCache
            }

            // Regen
            this.regenPerTick = (this.healthData.values.maxHealth * 4 * this.cameraEntity.cameraData.values.statLevels.values[Stat.HealthRegen] + this.healthData.values.maxHealth) / 25000;

            // Reload
            this.reloadTime = 15 * Math.pow(0.914, this.cameraEntity.cameraData.values.statLevels.values[Stat.Reload]);
        }

        this.scoreData.score = this.cameraEntity.cameraData.values.score;

        if ((this.styleData.values.flags & StyleFlags.isFlashing) && (this.game.tick >= this.cameraEntity.cameraData.values.spawnTick + 374 || this.inputs.attemptingShot() || this.inputs.movement.magnitude > 0)) {
            this.styleData.flags ^= StyleFlags.isFlashing;
            // Dont worry about invulnerability here - not gonna be invulnerable while flashing ever (see setInvulnerability)
            this.damageReduction = 1.0;
        }

        if (this.definition.sides === 2) {
            this.physicsData.width = this.physicsData.size * (this.definition.widthRatio ?? 1);
            if (this.definition.flags.displayAsTrapezoid === true) this.physicsData.flags |= PhysicsFlags.isTrapezoid;
        } else if (this.definition.flags.displayAsStar === true) this.styleData.flags |= StyleFlags.isStar;

        this.accel.add({
            x: this.inputs.movement.x * this.cameraEntity.cameraData.values.movementSpeed,
            y: this.inputs.movement.y * this.cameraEntity.cameraData.values.movementSpeed
        });
        this.inputs.movement.set({
            x: 0,
            y: 0
        });
    }
}
`

~src/Entity/AI.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import Vector, { VectorAbstract } from "../Physics/Vector";
import LivingEntity from "./Live";
import ObjectEntity from "./Object";
import TankBody from "./Tank/TankBody";

import { InputFlags, PhysicsFlags } from "../Const/Enums";
import { Entity } from "../Native/Entity";
import { tps } from "../config";

// Beware
// The logic in this file is somewhat messed up

/**
 * Used for simplifying the current state of the AI.
 * - `idle`: When the AI is idle
 * - `target`: When the AI has found a target
 */
export const enum AIState {
    idle = 0,
    hasTarget = 1,
    possessed = 3
}

/**
 * Inputs are the shared thing between AIs and Clients. Both use inputs
 * and both can replace eachother.
 */
export class Inputs {
    /**
     * InputFlags.
     */
    public flags = 0;
    /** Mouse position */
    public mouse: Vector = new Vector();
    /** Movement direction */
    public movement: Vector = new Vector();
    /** Whether the inputs are deleted or not. */
    public deleted = false;

    public constructor() { }

    public attemptingShot(): boolean {
        return !!(this.flags & InputFlags.leftclick);
    }
    public attemptingRepel(): boolean {
        return !!(this.flags & InputFlags.rightclick);
    }
}

/**
 * The Intelligence behind Auto Turrets.
 */
export class AI {
    /** Default static rotation that Auto Turrets rotate when in passive mode. */
    public static PASSIVE_ROTATION = 0.01;
    /** Whether a player < FullAccess can claim */
    public isClaimable: boolean = false;

    /** Specific rotation of the AI in passive mode. */
    public passiveRotation = Math.random() < .5 ? AI.PASSIVE_ROTATION : -AI.PASSIVE_ROTATION;
    /** View range in diep units. */
    public viewRange = 1700;
    /** The state of the AI. */
    public state = AIState.idle;

    /** The inputs, which are more like outputs for the AI. */
    public inputs: Inputs = new Inputs();
    /** The entity's whose AI is `this`. */
    public owner: ObjectEntity;
    /** The current game. */
    public game: GameServer;
    /** The AI's target. */
    public target: ObjectEntity | null = null;
    /** The speed at which the ai's owner can move. */
    public movementSpeed = 1;
    /** The speed at which the ai can reach the target. */
    public aimSpeed = 1;
    /** If the AI should predict enemy's movements, and aim accordingly. */
    public doAimPrediction: boolean = false;

    /** Target filter letting owner classes filter what can't be a target by position - false = not valid target */
    public targetFilter: (possibleTargetPos: VectorAbstract) => boolean;

    /** Stores the creation of the AI, used to optimize ticking */
    private _creationTick: number;

    private _findTargetInterval: number = 2;

    public constructor(owner: ObjectEntity, claimable?: boolean) {
        this.owner = owner;
        this.game = owner.game;
        this._creationTick = this.game.tick;

        this.inputs.mouse.set({
            x: 20,
            y: 0
        });

        this.targetFilter = () => true;
        if (claimable) this.isClaimable = true;

        this.game.entities.AIs.push(this);
    }

    /* Finds the closest entity in a different team */
    public findTarget(tick: number) {
        // If there's a target interval, wait a cycle till looking for new target
        if (this._findTargetInterval !== 0 && ((tick + this._creationTick) % this._findTargetInterval) !== 1)  {
            return Entity.exists(this.target) ? this.target : (this.target=null);
        }

        const rootPos = this.owner.rootParent.positionData.values;
        const team = this.owner.relationsData.values.team;

        // TODO(speed): find a way to speed up
        if (Entity.exists(this.target)) {

            // If the AI already has a valid target within view distance, it's not necessary to find a new one

            // Make sure the target hasn't changed teams, and is existant (sides != 0)
            if (team !== this.target.relationsData.values.team && this.target.physicsData.values.sides !== 0) {
                // confirm its within range
                const targetDistSq = (this.target.positionData.values.x - rootPos.x) ** 2 + (this.target.positionData.values.y - rootPos.y) ** 2;
                if (this.targetFilter(this.target.positionData.values) && targetDistSq < (this.viewRange ** 2) * 2) return this.target; // this range is inaccurate i think

            }
        }

        // const entities = this.game.entities.inner.slice(0, this.game.entities.lastId);
        const root = this.owner.rootParent === this.owner && this.owner.relationsData.values.owner instanceof ObjectEntity ? this.owner.relationsData.values.owner : this.owner.rootParent;
        const entities = this.viewRange === Infinity ? this.game.entities.inner.slice(0, this.game.entities.lastId) : this.game.entities.collisionManager.retrieve(root.positionData.values.x, root.positionData.values.y, this.viewRange, this.viewRange);

        let closestEntity = null;
        let closestDistSq = this.viewRange ** 2;

        for (let i = 0; i < entities.length; ++i) {

            const entity = entities[i];

            if (!(entity instanceof LivingEntity)) continue; // Check if the target is living

            if (entity.physicsData.values.flags & PhysicsFlags.isBase) continue; // Check if the target is a base

            if (!(entity.relationsData.values.owner === null || !(entity.relationsData.values.owner instanceof ObjectEntity))) continue; // Don't target entities who have an object owner

            if (entity.relationsData.values.team === team || entity.physicsData.values.sides === 0) continue;

            if (!this.targetFilter(entity.positionData.values)) continue; // Custom check

            const distSq = (entity.positionData.values.x - rootPos.x) ** 2 + (entity.positionData.values.y - rootPos.y) ** 2;

            if (distSq < closestDistSq) {
                closestEntity = entity;
                closestDistSq = distSq;
            }
        }

        return this.target = closestEntity;
    }

    /** Aims and predicts at the target. */
    public aimAt(target: ObjectEntity) {

        const movementSpeed = this.aimSpeed * 1.6;
        const ownerPos = this.owner.getWorldPosition();

        const pos = {
            x: target.positionData.values.x,
            y: target.positionData.values.y,
        }

        if (movementSpeed <= 0.001) { // Pls no weirdness

            this.inputs.movement.set({
                x: pos.x - ownerPos.x,
                y: pos.y - ownerPos.y
            });

            this.inputs.mouse.set(pos);

            // this.inputs.movement.angle = Math.atan2(delta.y, delta.x);
            this.inputs.movement.magnitude = 1;
            return;
        }
        if (this.doAimPrediction) {
            const delta = {
                x: pos.x - ownerPos.x,
                y: pos.y - ownerPos.y
            }

            let dist = Math.sqrt(delta.x ** 2 + delta.y ** 2);
            if (dist === 0) dist = 1;

            const unitDistancePerp = {
                x: delta.y / dist,
                y: -delta.x / dist
            }

            let entPerpComponent = unitDistancePerp.x * target.velocity.x + unitDistancePerp.y * target.velocity.y;

            if (entPerpComponent > movementSpeed * 0.9) entPerpComponent = movementSpeed * 0.9;

            if (entPerpComponent < movementSpeed * -0.9) entPerpComponent = movementSpeed * -0.9;

            const directComponent = Math.sqrt(movementSpeed ** 2 - entPerpComponent ** 2);
            const offset = (entPerpComponent / directComponent * dist) / 2;

            this.inputs.mouse.set({
                x: pos.x + offset * unitDistancePerp.x,
                y: pos.y + offset * unitDistancePerp.y
            });

        } else {
            this.inputs.mouse.set({
                x: pos.x,
                y: pos.y
            });
        }

        this.inputs.movement.magnitude = 1;
        this.inputs.movement.angle = Math.atan2(this.inputs.mouse.y - ownerPos.y, this.inputs.mouse.x - ownerPos.x);
    }

    public tick(tick: number) {
        // If its being posessed, but its possessor is deleted... then just restart;
        if (this.state === AIState.possessed) {
            if (!this.inputs.deleted) return;

            this.inputs = new Inputs();
        }

        const target = this.findTarget(tick);
        
        if (!target) {
            this.inputs.flags = 0;
            this.state = AIState.idle;
            const angle = this.inputs.mouse.angle + this.passiveRotation;

            this.inputs.mouse.set({
                x: Math.cos(angle) * 100,
                y: Math.sin(angle) * 100
            });
        } else {
            this.state = AIState.hasTarget;
            this.inputs.flags |= InputFlags.leftclick;
            this.aimAt(target);
        }
    }
}
`

~src/Entity/Live.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as util from "../util";

import ObjectEntity from "./Object";
import TankBody from "./Tank/TankBody";

import { visibilityRateDamage } from "../Const/TankDefinitions";
import { StyleFlags } from "../Const/Enums";
import { HealthGroup } from "../Native/FieldGroups";

/**
 * An Abstract class for all entities with health.
 */
export default class LivingEntity extends ObjectEntity {
    /** Always existant health field group, present on all entities with a healthbar. */
    public healthData: HealthGroup = new HealthGroup(this);

    /** The points a player is awarded when it kills this entity. */
    public scoreReward = 0;
    /** Amount of health gained per tick. */
    protected regenPerTick = 0;
    /** The damage this entity can emit onto another per tick. */
    protected damagePerTick = 8;
    /** Entities who have went through damage cycles with this entity in the past tick. No repeats. */
    protected damagedEntities: LivingEntity[] = [];
    /** Last tick that damage was received. */
    protected lastDamageTick = -1;
    /** Last tick that damage style flag was changed. */
    protected lastDamageAnimationTick = -1;
    /** Damage reduction (mathematical health increase). */
    public damageReduction = 1;

    /** Extends ObjectEntity.destroy() - diminishes health as well. */
    public destroy(animate=true) {
        if (this.hash === 0) return; // already deleted;

        if (animate) this.healthData.health = 0;

        super.destroy(animate);
    }

    /** Applies damage to two entity after colliding with eachother. */
    protected static applyDamage(entity1: LivingEntity, entity2: LivingEntity) {
        if (entity1.healthData.values.health <= 0 || entity2.healthData.values.health <= 0) return;
        if (entity1.damagedEntities.includes(entity2) || entity2.damagedEntities.includes(entity1)) return;
        if (entity1.damageReduction === 0 && entity2.damageReduction === 0) return;
        if (entity1.damagePerTick === 0 && entity1.physicsData.values.pushFactor === 0 || entity2.damagePerTick === 0 && entity2.physicsData.values.pushFactor === 0) return;

        const game = entity1.game;

        // entity2.lastDamageTick = entity1.lastDamageTick = entity1.game.tick;

        let dF1 = entity1.damagePerTick * entity2.damageReduction;
        let dF2 = entity2.damagePerTick * entity1.damageReduction;

        if (entity1 instanceof TankBody && entity2 instanceof TankBody) {
            dF1 *= 1.5;
            dF2 *= 1.5;
        }

        // Damage can't be more than enough to kill health
        const ratio = Math.max(1 - entity1.healthData.values.health / dF2, 1 - entity2.healthData.values.health / dF1)
        if (ratio > 0) { // Or >=, but minor optimizations
            dF1 *= 1 - ratio;
            dF2 *= 1 - ratio;
        }


        // Plays the animation damage for entity 2
        if (entity2.lastDamageAnimationTick !== game.tick && !(entity2.styleData.values.flags & StyleFlags.hasNoDmgIndicator)) {
            entity2.styleData.flags ^= StyleFlags.hasBeenDamaged;
            entity2.lastDamageAnimationTick = game.tick;
        }
        
        if (dF1 !== 0) {
            if (entity2.lastDamageTick !== game.tick && entity2 instanceof TankBody && entity2.definition.flags.invisibility && entity2.styleData.values.opacity < visibilityRateDamage) entity2.styleData.opacity += visibilityRateDamage;
            entity2.lastDamageTick = game.tick;
            entity2.healthData.health -= dF1;
        }
        
        // Plays the animation damage for entity 1
        if (entity1.lastDamageAnimationTick !== game.tick && !(entity1.styleData.values.flags & StyleFlags.hasNoDmgIndicator)) {
            entity1.styleData.flags ^= StyleFlags.hasBeenDamaged;
            entity1.lastDamageAnimationTick = game.tick;
        }
        
        if (dF2 !== 0) {
            if (entity1.lastDamageTick !== game.tick && entity1 instanceof TankBody && entity1.definition.flags.invisibility && entity1.styleData.values.opacity < visibilityRateDamage) entity1.styleData.opacity += visibilityRateDamage;
            entity1.lastDamageTick = game.tick;
            entity1.healthData.health -= dF2;
        }
        entity1.damagedEntities.push(entity2)
        entity2.damagedEntities.push(entity1)

        if (entity1.healthData.values.health < -0.0001) {
            util.warn("Health is below 0. Something in damage messed up]: ", entity1.healthData.health, entity2.healthData.health, ratio, dF1, dF2);
        }
        if (entity2.healthData.values.health < -0.0001) {
            util.warn("Health is below 0. Something in damage messed up]: ", entity1.healthData.health, entity2.healthData.health, ratio, dF1, dF2);
        }

        if (entity1.healthData.values.health < 0.0001) entity1.healthData.health = 0;
        if (entity2.healthData.values.health < 0.0001) entity2.healthData.health = 0;

        if (entity1.healthData.values.health === 0) {
            let killer: ObjectEntity = entity2;
            while (killer.relationsData.values.owner instanceof ObjectEntity && killer.relationsData.values.owner.hash !== 0) killer = killer.relationsData.values.owner;
            if (killer instanceof LivingEntity) entity1.onDeath(killer);
            entity2.onKill(entity1);
        }
        if (entity2.healthData.values.health === 0) {
            let killer: ObjectEntity = entity1;
            while (killer.relationsData.values.owner instanceof ObjectEntity && killer.relationsData.values.owner.hash !== 0) killer = killer.relationsData.values.owner;

            if (killer instanceof LivingEntity) entity2.onDeath(killer);
            entity1.onKill(entity2);
        }
    }

    /** Called when the entity kills another via collision. */
    public onKill(entity: LivingEntity) {}

    /** Called when the entity is killed via collision */
    public onDeath(killer: LivingEntity) {}

    /** Runs at the end of each tick. Will apply the damage then. */
    public applyPhysics() {
        super.applyPhysics();

        if (this.healthData.values.health <= 0) {
            this.destroy(true);

            this.damagedEntities = [];
            return;
        }

        // Regeneration
        if (this.healthData.values.health < this.healthData.values.maxHealth) {
            this.healthData.health += this.regenPerTick;

            // Regen boost after 30s
            if (this.game.tick - this.lastDamageTick >= 750) {
                this.healthData.health += this.healthData.values.maxHealth / 250;
            }
        }

        if (this.healthData.values.health > this.healthData.values.maxHealth) {
            this.healthData.health = this.healthData.values.maxHealth;
        }

        this.damagedEntities = [];
    }

    public tick(tick: number) {
        super.tick(tick);

        // It's cached
        const collidedEntities = this.findCollisions();

        for (let i = 0; i < collidedEntities.length; ++i) {
            if (!(collidedEntities[i] instanceof LivingEntity)) continue;

            if (collidedEntities[i].relationsData.values.team !== this.relationsData.values.team) {
                LivingEntity.applyDamage(collidedEntities[i] as LivingEntity, this);
            }
        }
    }
}
`

~src/Entity/Object.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as util from "../util";
import GameServer from "../Game";
import Velocity from "../Physics/Velocity";
import Vector from "../Physics/Vector";

import { PhysicsGroup, PositionGroup, RelationsGroup, StyleGroup } from "../Native/FieldGroups";
import { Entity } from "../Native/Entity";
import { PositionFlags, PhysicsFlags } from "../Const/Enums";

/**
 * The animator for how entities delete (the opacity and size fade out).
 */
class DeletionAnimation {
    /** The entity being animated. */
    private entity: ObjectEntity;
    /** The current frame of the deletion animation. */
    public frame = 5;

    public constructor(entity: ObjectEntity) {
        this.entity = entity;
    }

    /** Animates the death animation. Called by the owner's internal tick. */
    public tick() {
        if (this.frame === -1) throw new Error("Animation failed. Entity should be gone by now");

        switch (this.frame) {
            case 0: {
                this.entity.destroy(false);
                this.frame = -1;
                return;
            }
            case 5:
                this.entity.styleData.opacity = 1 - (1 / 6);
            default:
                this.entity.physicsData.size *= 1.1;
                this.entity.styleData.opacity -= 1 / 6;
                if (this.entity.styleData.values.opacity < 0) this.entity.styleData.opacity = 0;
                break;
        }

        this.frame -= 1;
    }
}

/**
 * Object Entity is used for all entities with relations,
 * physics, position, and style field groups. All physics
 * are applied through this class. **This class represents
 * everything you can see in game.**
 */
export default class ObjectEntity extends Entity {
    /** Always existant relations field group. Present in all objects. */
    public relationsData: RelationsGroup = new RelationsGroup(this);
    /** Always existant physics field group. Present in all objects. */
    public physicsData: PhysicsGroup = new PhysicsGroup(this);
    /** Always existant position field group. Present in all objects. */
    public positionData: PositionGroup = new PositionGroup(this);
    /** Always existant style field group. Present in all objects. */
    public styleData: StyleGroup = new StyleGroup(this);

    /** Animator used for deletion animation */
    public deletionAnimation: DeletionAnimation | null = null;

    /** When set to true (the default), physics are applied to the entity. */
    public isPhysical: boolean = true;

    /** Set to true of the entity has a parent. */
    public isChild: boolean = false;

    /** All children of the object entity. */
    public children: ObjectEntity[] = [];

    /** Used to determine the parent of all parents. */
    public rootParent: ObjectEntity = this;

    /** Whether or not the entity is near a camera. */
    public isViewed: boolean = false;

    /** Velocity used for physics. */
    public velocity = new Velocity();
    /** Acceleration used for physics. */
    public accel = new Vector();

    /** For internal spatial hash grid */
    private _queryId: number = -1;

    /** Cache of all ObjectEntitys who are colliding with `this` one at the current tick */
    private cachedCollisions: ObjectEntity[] = [];
    /** Tick that the cache was taken. */
    private cachedTick = 0;

    public constructor(game: GameServer) {
        super(game);

        this.styleData.zIndex = game.entities.zIndex++;
    }

    /** Calls the deletion animation, unless animate is set to false, in that case it instantly deletes. */
    public destroy(animate = true) {
        if (!animate) {
            if (this.deletionAnimation) this.deletionAnimation = null;

            this.delete();
        } else if (!this.deletionAnimation) { // if we aren't already deleting
            this.deletionAnimation = new DeletionAnimation(this);
        }
    }

    /** Extends Entity.delete, but removes child from parent. */
    public delete() {
        if (this.isChild) {
            util.removeFast(this.rootParent.children, this.rootParent.children.indexOf(this))
        } else {
            for (const child of this.children) {
                child.isChild = false;
                child.delete();
            }

            this.children = [];
        }

        super.delete();
    }

    /** Applies acceleration to the object. */
    public addAcceleration(angle: number, acceleration: number, negateFriction = false) {
        if (negateFriction) {
            const frictionComponent = this.velocity.angleComponent(angle) * .1;

            acceleration += frictionComponent;
        }

        this.accel.add(Vector.fromPolar(angle, acceleration));
    }

    /** Sets the velocity of the object. */
    public setVelocity(angle: number, magnitude: number) {
        // this.a.set(Vector.fromPolar(angle, acceleration));
        this.velocity.setPosition(this.positionData.values);
        this.velocity.set(Vector.fromPolar(angle, magnitude));
    }

    /** Updates the acceleration. */
    public maintainVelocity(angle: number, maxSpeed: number) {
        // acceleration * 10 = max speed. this relationship is caused by friction
        this.accel.add(Vector.fromPolar(angle, maxSpeed * 0.1));
    }

    /** Internal physics method used for calculating the current position of the object. */
    public applyPhysics() {
        if (!this.isViewed) {
            this.accel.set(new Vector(0, 0));
            return;
        }

        // apply friction opposite of current velocity
        this.addAcceleration(this.velocity.angle, this.velocity.magnitude * -0.1);

        // delta velocity
        this.velocity.add(this.accel);

        if (this.velocity.magnitude < 0.01) this.velocity.magnitude = 0;
        // when being deleted, entities slow down half speed
        else if (this.deletionAnimation) this.velocity.magnitude /= 2;
        this.positionData.x += this.velocity.x;
        this.positionData.y += this.velocity.y;

        // don't accumulate acceleration across ticks
        this.accel.set(new Vector(0, 0));

        // Keep things in the arena
        if (!(this.physicsData.values.flags & PhysicsFlags.canEscapeArena)) {
            const arena = this.game.arena;
            xPos: {
                if (this.positionData.values.x < arena.arenaData.values.leftX - arena.ARENA_PADDING) this.positionData.x = arena.arenaData.values.leftX - arena.ARENA_PADDING;
                else if (this.positionData.values.x > arena.arenaData.values.rightX + arena.ARENA_PADDING) this.positionData.x = arena.arenaData.values.rightX + arena.ARENA_PADDING;
                else break xPos;

                this.velocity.position.x = this.positionData.values.x;
            }
            yPos: {
                if (this.positionData.values.y < arena.arenaData.values.topY - arena.ARENA_PADDING) this.positionData.y = arena.arenaData.values.topY - arena.ARENA_PADDING;
                else if (this.positionData.values.y > arena.arenaData.values.bottomY + arena.ARENA_PADDING) this.positionData.y = arena.arenaData.values.bottomY + arena.ARENA_PADDING;
                else break yPos;

                this.velocity.position.y = this.positionData.values.y;
            }
        }
    }

    /** Applies knockback after hitting `entity` */
    protected receiveKnockback(entity: ObjectEntity) {
        let kbMagnitude = this.physicsData.values.absorbtionFactor * entity.physicsData.values.pushFactor;
        let kbAngle: number;
        let diffY = this.positionData.values.y - entity.positionData.values.y;
        let diffX = this.positionData.values.x - entity.positionData.values.x;
        // Prevents drone stacking etc
        if (diffX === 0 && diffY === 0) kbAngle = Math.random() * util.PI2;
        else kbAngle = Math.atan2(diffY, diffX);

        if ((entity.physicsData.values.flags & PhysicsFlags.isSolidWall || entity.physicsData.values.flags & PhysicsFlags.isBase) && !(this.positionData.values.flags & PositionFlags.canMoveThroughWalls))  {
            if (entity.physicsData.values.flags & PhysicsFlags.isSolidWall) {
                this.accel.magnitude *= 0.3;
                this.velocity.magnitude *= 0.3;
            }
            kbMagnitude /= 0.3;
        }
        if (entity.physicsData.values.sides === 2) {
            if (this.positionData.values.flags & PositionFlags.canMoveThroughWalls) {
                kbMagnitude = 0;
            } else if ((!(entity.physicsData.values.flags & PhysicsFlags.isBase) || entity.physicsData.values.pushFactor !== 0) && this.relationsData.values.owner instanceof ObjectEntity && !(Entity.exists(this.relationsData.values.team) && this.relationsData.values.team === entity.relationsData.values.team)) {
                // this is a bit off still. k
                this.velocity.setPosition(this.positionData.values);
                this.setVelocity(0, 0);
                this.destroy(true) // Kills off bullets etc
                return;
            } else {
                const relA = Math.cos(kbAngle + entity.positionData.values.angle) / entity.physicsData.values.size;
                const relB = Math.sin(kbAngle + entity.positionData.values.angle) / entity.physicsData.values.width;
                if (Math.abs(relA) <= Math.abs(relB)) {
                    if (relB < 0) {
                        this.addAcceleration(Math.PI * 3 / 2, kbMagnitude);
                    } else {
                        this.addAcceleration(Math.PI * 1 / 2, kbMagnitude);
                    }
                } else {
                    if (relA < 0) {
                        this.addAcceleration(Math.PI, kbMagnitude);
                    } else {
                        this.addAcceleration(0, kbMagnitude);
                    }
                }
            }
        } else {
            this.addAcceleration(kbAngle, kbMagnitude);
        }
    }

    /** Detects collisions. */
    protected findCollisions() {
        if (this.cachedTick === this.game.tick) return this.cachedCollisions;
        
        this.cachedTick = this.game.tick;
        this.cachedCollisions = [];

        // Lets just let the game deal with this next tick.
        if (this.hash === 0) return [];
        if (this.physicsData.values.sides === 0) return [];

        // TODO(speed):
        // and the for loop
        const entities = this.game.entities.collisionManager.retrieveEntitiesByEntity(this);
        for (let i = 0; i < entities.length; ++i) {
            const entity = entities[i];
            if (entity === this) continue;
            if (entity.deletionAnimation) continue;
            if (entity.relationsData.values.team === this.relationsData.values.team) {
                if ((entity.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision) ||
                    (this.physicsData.values.flags & PhysicsFlags.noOwnTeamCollision)) continue;

                if (entity.relationsData.values.owner !== this.relationsData.values.owner) {
                    if ((entity.physicsData.values.flags & PhysicsFlags.onlySameOwnerCollision) ||
                        (this.physicsData.values.flags & PhysicsFlags.onlySameOwnerCollision)) continue;
                }
            }
            
            if (this.relationsData.values.team === this.game.arena && (entity.physicsData.values.flags & PhysicsFlags.isBase)) continue;

            if (entity.physicsData.values.sides === 0) continue;
            if (entity.physicsData.values.sides === 2 && this.physicsData.values.sides === 2) {
                // in Diep.io source code, rectangles do not support collisions with other rectangles
                // uncomment the following code to enable rect on rect collisions
                /*
                const rect1Left = this.positionData.values.x - this.physicsData.values.width / 2;
                const rect1Right = this.positionData.values.x + this.physicsData.values.width / 2;
                const rect1Top = this.positionData.values.y - this.physicsData.values.size / 2;
                const rect1Bottom = this.positionData.values.y + this.physicsData.values.size / 2;
                const rect2Left = entity.positionData.values.x - entity.physicsData.values.width / 2;
                const rect2Right = entity.positionData.values.x + entity.physicsData.values.width / 2;
                const rect2Top = entity.positionData.values.y - entity.physicsData.values.size / 2;
                const rect2Bottom = entity.positionData.values.y + entity.physicsData.values.size / 2;
                if (rect1Left <= rect2Right &&
                        rect1Right >= rect2Left &&
                        rect1Top <= rect2Bottom &&
                        rect1Bottom >= rect2Top) this.cachedCollisions.push(entity);
                */
            } else if (this.physicsData.values.sides !== 2 && entity.physicsData.values.sides === 2) {
                const dX = util.constrain(this.positionData.values.x, entity.positionData.values.x - entity.physicsData.values.size / 2, entity.positionData.values.x + entity.physicsData.values.size / 2) - this.positionData.values.x;
                const dY = util.constrain(this.positionData.values.y, entity.positionData.values.y - entity.physicsData.values.width / 2, entity.positionData.values.y + entity.physicsData.values.width / 2) - this.positionData.values.y;

                if (dX ** 2 + dY ** 2 <= this.physicsData.size ** 2) this.cachedCollisions.push(entity);
            } else if (this.physicsData.values.sides === 2 && entity.physicsData.values.sides !== 2) {
                const dX = util.constrain(entity.positionData.values.x, this.positionData.values.x - this.physicsData.values.size / 2, this.positionData.values.x + this.physicsData.values.size / 2) - entity.positionData.values.x;
                const dY = util.constrain(entity.positionData.values.y, this.positionData.values.y - this.physicsData.values.width / 2, this.positionData.values.y + this.physicsData.values.width / 2) - entity.positionData.values.y;

                if (dX ** 2 + dY ** 2 <= entity.physicsData.size ** 2) this.cachedCollisions.push(entity);
            } else {
                if ((entity.positionData.values.x - this.positionData.values.x) ** 2 + (entity.positionData.values.y - this.positionData.values.y) ** 2 <= (entity.physicsData.values.size + this.physicsData.values.size) ** 2) {
                    this.cachedCollisions.push(entity);
                }
            }
        }

        return this.cachedCollisions;
    }

    /** Sets the parent in align with everything else. */
    public setParent(parent: ObjectEntity) {
        this.relationsData.parent = parent;
        this.rootParent = parent.rootParent;
        this.rootParent.children.push(this);

        this.isChild = true;
        this.isPhysical = false;
    }

    /** Returns the true world position (even for objects who have parents). */
    public getWorldPosition(): Vector {
        let pos = new Vector(this.positionData.values.x, this.positionData.values.y);

        let entity: ObjectEntity = this;
        while (entity.relationsData.values.parent instanceof ObjectEntity) {
            
            if (!(entity.relationsData.values.parent.positionData.values.flags & PositionFlags.absoluteRotation)) pos.angle += entity.positionData.values.angle;
            entity = entity.relationsData.values.parent;
            pos.x += entity.positionData.values.x;
            pos.y += entity.positionData.values.y;
        }

        return pos;
    }

    public tick(tick: number) {
        this.velocity.setPosition(this.positionData.values);
        this.deletionAnimation?.tick();

        if (this.isPhysical && !(this.deletionAnimation)) {
            const collidedEntities = this.findCollisions();

            for (let i = 0; i < collidedEntities.length; ++i) {
                this.receiveKnockback(collidedEntities[i]);
            }
        }

        if (this.isViewed) for (let i = 0; i < this.children.length; ++i) this.children[i].tick(tick);
    }
}
`

~src/Gamemodes/Domination.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Client from "../Client";
import { Color, ArenaFlags } from "../Const/Enums";
import Dominator from "../Entity/Misc/Dominator";
import TeamBase from "../Entity/Misc/TeamBase";
import { TeamEntity } from "../Entity/Misc/TeamEntity";
import TankBody from "../Entity/Tank/TankBody";
import GameServer from "../Game";
import ArenaEntity from "../Native/Arena";


const arenaSize = 11150;
const baseSize = 3345;
const domBaseSize = baseSize / 2;
/**
 * Domination Gamemode Arena
 */
export default class DominationArena extends ArenaEntity {
    /** Blue TeamBASEentity */
    public blueTeamBase: TeamBase;
    /** Red TeamBASE entity */
    public redTeamBase: TeamBase;

    /** Maps clients to their teams */
    public playerTeamMap: Map<Client, TeamBase> = new Map();

    public constructor(game: GameServer) {
        super(game);
        this.shapeScoreRewardMultiplier = 2.0;

        this.updateBounds(arenaSize * 2, arenaSize * 2)

        this.arenaData.values.flags |= ArenaFlags.hiddenScores;

        this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseSize / 2,  Math.random() > .5 ? (arenaSize - baseSize / 2) : -arenaSize + baseSize / 2, baseSize, baseSize);
        this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseSize / 2, Math.random() > .5 ? (arenaSize - baseSize / 2) : -arenaSize + baseSize / 2, baseSize, baseSize);
        
        new Dominator(this, new TeamBase(game, this, arenaSize / 2.5, arenaSize / 2.5, domBaseSize, domBaseSize, false));
        new Dominator(this, new TeamBase(game, this, arenaSize / -2.5, arenaSize / 2.5, domBaseSize, domBaseSize, false));
        new Dominator(this, new TeamBase(game, this, arenaSize / -2.5, arenaSize / -2.5, domBaseSize, domBaseSize, false));
        new Dominator(this, new TeamBase(game, this, arenaSize / 2.5, arenaSize / -2.5, domBaseSize, domBaseSize, false));
    }

    public spawnPlayer(tank: TankBody, client: Client) {
        tank.positionData.values.y = arenaSize * Math.random() - arenaSize;

        const xOffset = (Math.random() - 0.5) * baseSize,
              yOffset = (Math.random() - 0.5) * baseSize;

        const base = this.playerTeamMap.get(client) || [this.blueTeamBase, this.redTeamBase][0|Math.random()*2];
        tank.relationsData.values.team = base.relationsData.values.team;
        tank.styleData.values.color = base.styleData.values.color;
        tank.positionData.values.x = base.positionData.values.x + xOffset;
        tank.positionData.values.y = base.positionData.values.y + yOffset;
        this.playerTeamMap.set(client, base);

        if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
    }
}
`

~src/Gamemodes/FFA.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import ArenaEntity from "../Native/Arena";

/**
 * FFA Gamemode Arena
 */
export default class FFAArena extends ArenaEntity {}
`

~src/Gamemodes/Maze.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/
import ArenaEntity, { ArenaState } from "../Native/Arena";
import MazeWall from "../Entity/Misc/MazeWall";
import { VectorAbstract } from "../Physics/Vector";

// constss.
const CELL_SIZE = 635;
const GRID_SIZE = 40;
const ARENA_SIZE = CELL_SIZE * GRID_SIZE;
const SEED_AMOUNT = Math.floor(Math.random() * 30) + 30;
const TURN_CHANCE = 0.15;
const BRANCH_CHANCE = 0.1;
const TERMINATION_CHANCE = 0.15;

/**
 * Maze Gamemode Arena
 * 
 * Implementation details:
 * Maze map generator by damocles <github.com/SpanksMcYeet>
 *  - Added into codebase on December 3rd 2022
 */
export default class MazeArena extends ArenaEntity {
    /** Stores all the "seed"s */
    private SEEDS: VectorAbstract[] = [];
    /** Stores all the "wall"s, contains cell based coords */
    private WALLS: (VectorAbstract & {width: number, height: number})[] = [];
    /** Rolled out matrix of the grid */
    private MAZE: Uint8Array = new Uint8Array(GRID_SIZE * GRID_SIZE);

    public constructor(a: any) {
        super(a);
        this.updateBounds(ARENA_SIZE, ARENA_SIZE);
        this.allowBoss = false;
        this._buildMaze();
    }
    /** Creates a maze wall from cell coords */
    private _buildWallFromGridCoord(gridX: number, gridY: number, gridW: number, gridH: number) {
        const scaledW = gridW * CELL_SIZE;
        const scaledH = gridH * CELL_SIZE;
        const scaledX = gridX * CELL_SIZE - ARENA_SIZE / 2 + (scaledW / 2);
        const scaledY = gridY * CELL_SIZE - ARENA_SIZE / 2 + (scaledH / 2);
        new MazeWall(this.game, scaledX, scaledY, scaledH, scaledW);
    }
    /** Allows for easier (x, y) based getting of maze cells */
    private _get(x: number, y: number): number {
        return this.MAZE[y * GRID_SIZE + x];
    }
    /** Allows for easier (x, y) based setting of maze cells */
    private _set(x: number, y: number, value: number): number {
        return this.MAZE[y * GRID_SIZE + x] = value;
    }
    /** Converts MAZE grid into an array of set and unset bits for ease of use */
    private _mapValues(): [x: number, y: number, value: number][] {
        const values: [x: number, y: number, value: number][] = Array(this.MAZE.length);
        for (let i = 0; i < this.MAZE.length; ++i) values[i] = [i % GRID_SIZE, Math.floor(i / GRID_SIZE), this.MAZE[i]];
        return values;
    }
    /** Builds the maze */
    private _buildMaze() {
        // Plant some seeds
        for (let i = 0; i < 10000; i++) {
            // Stop if we exceed our maximum seed amount
            if (this.SEEDS.length >= SEED_AMOUNT) break;
            // Attempt a seed planting
            let seed: VectorAbstract = {
                x: Math.floor((Math.random() * GRID_SIZE) - 1),
                y: Math.floor((Math.random() * GRID_SIZE) - 1),
            };
            // Check if our seed is valid (is 3 GU away from another seed, and is not on the border)
            if (this.SEEDS.some(a => (Math.abs(seed.x - a.x) <= 3 && Math.abs(seed.y - a.y) <= 3))) continue;
            if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) continue;
            // Push it to the pending seeds and set its grid to a wall cell
            this.SEEDS.push(seed);
            this._set(seed.x, seed.y, 1);
        }
        const direction: number[][] = [
            [-1, 0], [1, 0], // left and right
            [0, -1], [0, 1], // up and down
        ];
        // Let it grow!
        for (let seed of this.SEEDS) {
            // Select a direction we want to head in
            let dir: number[] = direction[Math.floor(Math.random() * 4)];
            let termination = 1;
            // Now we can start to grow
            while (termination >= TERMINATION_CHANCE) {
                // Choose the next termination chance
                termination = Math.random();
                // Get the direction we're going in
                let [x, y] = dir;
                // Move forward in that direction, and set that grid to a wall cell
                seed.x += x;
                seed.y += y;
                if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) break;
                this._set(seed.x, seed.y, 1);
                // Now lets see if we want to branch or turn
                if (Math.random() <= BRANCH_CHANCE) {
                    // If the seeds exceeds 75, then we're going to stop creating branches in order to avoid making a massive maze tumor(s)
                    if (this.SEEDS.length > 75) continue;
                    // Get which side we want the branch to be on (left or right if moving up or down, and up and down if moving left or right)
                    let [ xx, yy ] = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
                    // Create the seed
                    let newSeed = {
                        x: seed.x + xx,
                        y: seed.y + yy,
                    };
                    // Push the seed and set its grid to a maze zone
                    this.SEEDS.push(newSeed);
                    this._set(seed.x, seed.y, 1);
                } else if (Math.random() <= TURN_CHANCE) {
                    // Get which side we want to turn to (left or right if moving up or down, and up and down if moving left or right)
                    dir = direction.filter(a => a.every((b, c) => b !== dir[c]))[Math.floor(Math.random() * 2)];
                }
            }
        }
        // Now lets attempt to add some singular walls around the arena
        for (let i = 0; i < 10; i++) {
            // Attempt to place it 
            let seed = {
                x: Math.floor((Math.random() * GRID_SIZE) - 1),
                y: Math.floor((Math.random() * GRID_SIZE) - 1),
            };
            // Check if our sprinkle is valid (is 3 GU away from another wall, and is not on the border)
            if (this._mapValues().some(([x, y, r]) => r === 1 && (Math.abs(seed.x - x) <= 3 && Math.abs(seed.y - y) <= 3))) continue;
            if (seed.x <= 0 || seed.y <= 0 || seed.x >= GRID_SIZE - 1 || seed.y >= GRID_SIZE - 1) continue;
            // Set its grid to a wall cell
            this._set(seed.x, seed.y, 1);
        }
        // Now it's time to fill in the inaccessible pockets
        // Start at the top left
        let queue: number[][] = [[0, 0]];
        this._set(0, 0, 2);
        let checkedIndices = new Set([0]);
        // Now lets cycle through the whole map
        for (let i = 0; i < 3000 && queue.length > 0; i++) {
            let next = queue.shift();
            if (next == null) break;
            let [x, y] = next;
            // Get what the coordinates of what lies to the side of our cell
            for (let [nx, ny] of [
                [x - 1, y], // left
                [x + 1, y], // right
                [x, y - 1], // top
                [x, y + 1], // bottom
            ]) {
                // If its a wall ignore it
                if (this._get(nx, ny) !== 0) continue;
                let i = ny * GRID_SIZE + nx;
                // Check if we've already checked this cell
                if (checkedIndices.has(i)) continue;
                // Add it to the checked cells if we haven't already
                checkedIndices.add(i);
                // Add it to the next cycle to check
                queue.push([nx, ny]);
                // Set its grid to an accessible cell
                this._set(nx, ny, 2);
            }
        }
        // Cycle through all areas of the map
        for (let x = 0; x < GRID_SIZE; x++) {
            for (let y = 0; y < GRID_SIZE; y++) {
                // If we're not a wall, ignore the cell and move on
                if (this._get(x, y) === 2) continue;
                // Define our properties
                let chunk = { x, y, width: 0, height: 1 };
                // Loop through adjacent cells and see how long we should be
                while (this._get(x + chunk.width, y) !== 2) {
                    this._set(x + chunk.width, y, 2);
                    chunk.width++;
                }
                // Now lets see if we need to be t h i c c
                outer: while (true) {
                    // Check the row below to see if we can still make a box
                    for (let i = 0; i < chunk.width; i++)
                        // Stop if we can't
                        if (this._get(x + i, y + chunk.height) === 2) break outer;
                    // If we can, remove the line of cells from the map and increase the height of the block
                    for (let i = 0; i < chunk.width; i++)
                        this._set(x + i, y + chunk.height, 2);
                    chunk.height++;
                }
                this.WALLS.push(chunk);
            }
        }
        // Create the walls!
        for (let {x, y, width, height} of this.WALLS)
            this._buildWallFromGridCoord(x, y, width, height);
    }
}
`

~src/Gamemodes/Mothership.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Client from "../Client";
import { Color, ArenaFlags, TeamFlags, ValidScoreboardIndex } from "../Const/Enums";
import Mothership from "../Entity/Misc/Mothership";
import { TeamEntity } from "../Entity/Misc/TeamEntity";
import TankBody from "../Entity/Tank/TankBody";
import GameServer from "../Game";
import ArenaEntity, { ArenaState } from "../Native/Arena";
import { Entity } from "../Native/Entity";
import { PI2 } from "../util";

const arenaSize = 11150;
const TEAM_COLORS = [Color.TeamBlue, Color.TeamRed];

/**
 * Mothership Gamemode Arena
 */
export default class MothershipArena extends ArenaEntity {
    /** All team entities in game */
    public teams: TeamEntity[] = [];
    /** Motherships in game */
    public motherships: Mothership[] = [];

    /** Maps clients to their mothership */
    public playerTeamMotMap: Map<Client, Mothership> = new Map();

    public constructor(game: GameServer) {
        super(game);
        this.shapeScoreRewardMultiplier = 3.0;

        this.arenaData.values.flags |= ArenaFlags.hiddenScores;

        // little fun thing to support multiple teams - spread colors around map
        let randAngle = Math.random() * PI2;
        for (const teamColor of TEAM_COLORS) {
            const team = new TeamEntity(this.game, teamColor);
            this.teams.push(team);
            const mot = new Mothership(this.game);
            this.motherships.push(mot);
    
            mot.relationsData.values.team = team;
            mot.styleData.values.color = team.teamData.values.teamColor;
            mot.positionData.values.x = Math.cos(randAngle) * arenaSize * 0.75;
            mot.positionData.values.y = Math.sin(randAngle) * arenaSize * 0.75;

            randAngle += PI2 / TEAM_COLORS.length;
        }

        this.updateBounds(arenaSize * 2, arenaSize * 2);
    }

    public spawnPlayer(tank: TankBody, client: Client) {
        if (!this.motherships.length && !this.playerTeamMotMap.has(client)) {
            const team = this.teams[~~(Math.random()*this.teams.length)];
            const { x, y } = this.findSpawnLocation();

            tank.positionData.values.x = x;
            tank.positionData.values.y = y;
            tank.relationsData.values.team = team;
            tank.styleData.values.color = team.teamData.teamColor;
            return;
        }

        const mothership = this.playerTeamMotMap.get(client) || this.motherships[~~(Math.random() * this.motherships.length)];
        this.playerTeamMotMap.set(client, mothership);

        tank.relationsData.values.team = mothership.relationsData.values.team;
        tank.styleData.values.color = mothership.styleData.values.color;

        // TODO: Possess mothership if its unpossessed
        if (Entity.exists(mothership)) {
            tank.positionData.values.x = mothership.positionData.values.x;
            tank.positionData.values.y = mothership.positionData.values.y;
        } else {
            const { x, y } = this.findSpawnLocation();

            tank.positionData.values.x = x;
            tank.positionData.values.y = y;
        }

        if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
    }
    public updateScoreboard(scoreboardPlayers: TankBody[]) {
        this.motherships.sort((m1, m2) => m2.healthData.values.health - m1.healthData.values.health);

        const length = Math.min(10, this.motherships.length);
        for (let i = 0; i < length; ++i) {
            const mothership = this.motherships[i];
            const team = mothership.relationsData.values.team;
            const isTeamATeam = team instanceof TeamEntity;
            if (mothership.styleData.values.color === Color.Tank) this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = Color.ScoreboardBar;
            else this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = mothership.styleData.values.color;
            this.arenaData.values.scoreboardNames[i as ValidScoreboardIndex] = isTeamATeam ? team.teamName : `Mothership ${i+1}`;
            // TODO: Change id
            this.arenaData.values.scoreboardTanks[i as ValidScoreboardIndex] = -1;
            this.arenaData.values.scoreboardScores[i as ValidScoreboardIndex] = mothership.healthData.values.health;
            this.arenaData.values.scoreboardSuffixes[i as ValidScoreboardIndex] = " HP";
        }
       
        this.arenaData.scoreboardAmount = length;
    }
    public tick(tick: number) {
        const length = Math.min(10, this.motherships.length);
        for (let i = 0; i < length; ++i) {
            const mothership = this.motherships[i];
            const team = mothership.relationsData.values.team;
            const isTeamATeam = team instanceof TeamEntity;
            if (isTeamATeam) {
                team.teamData.mothershipX = mothership.positionData.values.x;
                team.teamData.mothershipY = mothership.positionData.values.y;
                team.teamData.flags |= TeamFlags.hasMothership;
            }
        }

        // backwards to preserve
        for (let i = this.motherships.length; i --> 0;) {
            const mot = this.motherships[i];
            if (!Entity.exists(mot)) {
                const pop = this.motherships.pop();
                if (pop && i < this.motherships.length) this.motherships[i] = pop;
            }
        }

        if (this.motherships.length <= 1) {
            if (this.state === ArenaState.OPEN) {
                this.state = ArenaState.OVER;
                setTimeout(() => {
                    this.close();
                }, 5000);
            }
        }

        super.tick(tick);
    }
}
`

~src/Gamemodes/Sandbox.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import ArenaEntity from "../Native/Arena";

import ShapeManager from "../Entity/Shape/Manager";
import { ArenaFlags } from "../Const/Enums";

/**
 * Manage shape count
 */
export class SandboxShapeManager extends ShapeManager {
    protected get wantedShapes() {
        let i = 0;
        for (const client of this.game.clients) {
            if (client.camera) i += 1;
        }
        return Math.floor(i * 12.5);
    }
}

/**
 * Sandbox Gamemode Arena
 */
export default class SandboxArena extends ArenaEntity {
    /** Limits shape count to floor(12.5 * player count) */
	protected shapes: ShapeManager = new SandboxShapeManager(this);

    public constructor(game: GameServer) {
        super(game);

		this.updateBounds(2500, 2500);
        this.arenaData.values.flags |= ArenaFlags.canUseCheats;
		// const w1 = new MazeWall(this.game, 0, 0, 500, 500);
    }

    public tick(tick: number) {
		const arenaSize = Math.floor(25 * Math.sqrt(Math.max(this.game.clients.size, 1))) * 100;
		if (this.width !== arenaSize || this.height !== arenaSize) this.updateBounds(arenaSize, arenaSize);

        super.tick(tick);
    }
}
`

~src/Gamemodes/Team2.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import ArenaEntity from "../Native/Arena";
import Client from "../Client";

import TeamBase from "../Entity/Misc/TeamBase";
import TankBody from "../Entity/Tank/TankBody";

import { TeamEntity } from "../Entity/Misc/TeamEntity";
import { Color } from "../Const/Enums";

 const arenaSize = 11150;
 const baseWidth = 2007;
// const arenaSize = 2000;
// const baseWidth = 407;

/**
 * Teams2 Gamemode Arena
 */
export default class Teams2Arena extends ArenaEntity {
    /** Blue Team entity */
    public blueTeamBase: TeamBase;
    /** Red Team entity */
    public redTeamBase: TeamBase;
    // /** Limits shape count 100 */
    //     protected shapes: ShapeManager = new class extends ShapeManager {
    //     protected get wantedShapes() {
    //         return 64;
    //     }
    // }(this);

    /** Maps clients to their teams */
    public playerTeamMap: Map<Client, TeamBase> = new Map();
    
    public constructor(game: GameServer) {
        super(game);
        this.updateBounds(arenaSize * 2, arenaSize * 2);
        this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseWidth / 2, 0, arenaSize * 2, baseWidth);
        this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseWidth / 2, 0, arenaSize * 2, baseWidth);
    }

    public spawnPlayer(tank: TankBody, client: Client) {
        tank.positionData.values.y = 2 * arenaSize * Math.random() - arenaSize;

        const xOffset = (Math.random() - 0.5) * baseWidth;
        
        const base = this.playerTeamMap.get(client) || [this.blueTeamBase, this.redTeamBase][0|Math.random()*2];
        tank.relationsData.values.team = base.relationsData.values.team;
        tank.styleData.values.color = base.styleData.values.color;
        tank.positionData.values.x = base.positionData.values.x + xOffset;
        this.playerTeamMap.set(client, base);

        if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
    }
}
`

~src/Gamemodes/Team4.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import ArenaEntity from "../Native/Arena";
import Client from "../Client";

import TeamBase from "../Entity/Misc/TeamBase";
import TankBody from "../Entity/Tank/TankBody";

import { TeamEntity } from "../Entity/Misc/TeamEntity";
import { Color } from "../Const/Enums";

const arenaSize = 11150;
const baseSize = 3345;

/**
 * Teams4 Gamemode Arena
 */
export default class Teams4Arena extends ArenaEntity {
    /** Blue TeamBASEentity */
    public blueTeamBase: TeamBase;
    /** Red TeamBASE entity */
    public redTeamBase: TeamBase;
    /** Green TeamBASE entity */
    public greenTeamBase: TeamBase;
    /** Purple TeamBASE entity */
    public purpleTeamBase: TeamBase;

    /** Maps clients to their teams */
    public playerTeamMap: Map<Client, TeamBase> = new Map();

    public constructor(game: GameServer) {
        super(game);
        this.updateBounds(arenaSize * 2, arenaSize * 2);
        this.blueTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamBlue), -arenaSize + baseSize / 2,  -arenaSize + baseSize / 2, baseSize, baseSize);
        this.redTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamRed), arenaSize - baseSize / 2, arenaSize - baseSize / 2, baseSize, baseSize);
        this.greenTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamGreen), -arenaSize + baseSize / 2,  arenaSize - baseSize / 2, baseSize, baseSize);
        this.purpleTeamBase = new TeamBase(game, new TeamEntity(this.game, Color.TeamPurple), arenaSize - baseSize / 2, -arenaSize + baseSize / 2, baseSize, baseSize);
    }

    public spawnPlayer(tank: TankBody, client: Client) {
        tank.positionData.values.y = arenaSize * Math.random() - arenaSize;

        const xOffset = (Math.random() - 0.5) * baseSize,
              yOffset = (Math.random() - 0.5) * baseSize;
        
        const base = this.playerTeamMap.get(client) || [this.blueTeamBase, this.redTeamBase, this.greenTeamBase, this.purpleTeamBase][0|Math.random()*4];
        tank.relationsData.values.team = base.relationsData.values.team;
        tank.styleData.values.color = base.styleData.values.color;
        tank.positionData.values.x = base.positionData.values.x + xOffset;
        tank.positionData.values.y = base.positionData.values.y + yOffset;
        this.playerTeamMap.set(client, base);

        if (client.camera) client.camera.relationsData.team = tank.relationsData.values.team;
    }
}
`

~src/Native/Arena.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import ShapeManager from "../Entity/Shape/Manager";
import TankBody from "../Entity/Tank/TankBody";
import ArenaCloser from "../Entity/Misc/ArenaCloser";
import ClientCamera from "./Camera";

import { VectorAbstract } from "../Physics/Vector";
import { ArenaGroup, TeamGroup } from "./FieldGroups";
import { Entity } from "./Entity";
import { Color, ArenaFlags, ValidScoreboardIndex } from "../Const/Enums";
import { PI2, saveToLog } from "../util";
import { TeamGroupEntity } from "../Entity/Misc/TeamEntity";
import Client from "../Client";
import AbstractBoss from "../Entity/Boss/AbstractBoss";
import Guardian from "../Entity/Boss/Guardian";
import Summoner from "../Entity/Boss/Summoner";
import FallenOverlord from "../Entity/Boss/FallenOverlord";
import FallenBooster from "../Entity/Boss/FallenBooster";
import Defender from "../Entity/Boss/Defender";
import { bossSpawningInterval, scoreboardUpdateInterval } from "../config";

export const enum ArenaState {
	/** Alive, open */
	OPEN = 0,
	/** Game ended - someone won */
	OVER = 1,
	/** Lobby starts to close */
	CLOSING = 2,
	/** Lobby closed */
	CLOSED = 3,
}

/**
 * The Arena Entity, sent to the client and also used for internal calculations.
 */
export default class ArenaEntity extends Entity implements TeamGroupEntity {
	/** Always existant arena field group. Present in all arenas. */
	public arenaData: ArenaGroup = new ArenaGroup(this);
	/** Always existant team field group. Present in all (or maybe just ffa) arenas. */
	public teamData: TeamGroup = new TeamGroup(this);
	/** Cached width of the arena. Not sent to the client directly. */
	public width: number;
	/** Cached height of the arena. Not sent to the client directly. */
	public height: number;
	/** Whether or not the arena allows new players to spawn. */
	public state: ArenaState = ArenaState.OPEN;

	public shapeScoreRewardMultiplier: number = 1;

	/** Enable or disable natural boss spawning */
	public allowBoss: boolean = true;

	/** The current boss spawned into the game */
	public boss: AbstractBoss | null = null;

	/** Scoreboard leader */
	public leader: TankBody | null = null;

	/** Controller of all shapes in the arena. */
	protected shapes = new ShapeManager(this);

	/** Padding between arena size and maximum movement border. */
	public ARENA_PADDING = 200;

	public constructor(game: GameServer) {
		super(game);

		this.updateBounds(this.width = 22300, this.height = 22300);

		this.arenaData.values.topY = -this.height / 2;
		this.arenaData.values.bottomY = this.height / 2;
		this.arenaData.values.leftX = -this.width / 2;
		this.arenaData.values.rightX = this.width / 2;

		this.arenaData.values.flags = ArenaFlags.gameReadyStart;
		this.teamData.values.teamColor = Color.Neutral;
	}

	/**
	 * Finds a spawnable location on the map.
	 */
	 public findSpawnLocation(): VectorAbstract {
		const pos = {
			x: ~~(Math.random() * this.width - this.width / 2),
			y: ~~(Math.random() * this.height - this.height / 2),
		}

		findSpawn: for (let i = 0; i < 20; ++i) {
			const entities = this.game.entities.collisionManager.retrieve(pos.x, pos.y, 1000, 1000);

			// Only spawn < 1000 units away from player, unless we can't find a place to spawn
			for (let len = entities.length; --len >= 0;) {
				if (entities[len] instanceof TankBody && (entities[len].positionData.values.x - pos.x) ** 2 + (entities[len].positionData.values.y - pos.y) ** 2 < 1_000_000) { // 1000^2
					pos.x = ~~(Math.random() * this.width - this.width / 2);
					pos.y = ~~(Math.random() * this.height - this.height / 2);

					continue findSpawn;
				}
			}

			break;
		}

		return pos;
	}

	/**
	 * Updates the scoreboard / leaderboard arena fields.
	 */
	protected updateScoreboard(scoreboardPlayers: TankBody[]) {

		const scoreboardCount = this.arenaData.scoreboardAmount = (this.arenaData.values.flags & ArenaFlags.hiddenScores) ? 0 : Math.min(scoreboardPlayers.length, 10);

		if (scoreboardCount) {
			this.arenaData.flags |= ArenaFlags.showsLeaderArrow;
			let i;
			for (i = 0; i < scoreboardCount; ++i) {
				const player = scoreboardPlayers[i];
				
				if (player.styleData.values.color === Color.Tank) this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = Color.ScoreboardBar;
				else this.arenaData.values.scoreboardColors[i as ValidScoreboardIndex] = player.styleData.values.color;
				this.arenaData.values.scoreboardNames[i as ValidScoreboardIndex] = player.nameData.values.name;
				this.arenaData.values.scoreboardScores[i as ValidScoreboardIndex] = player.scoreData.values.score;
				// _currentTank only since ts ignore
				this.arenaData.values.scoreboardTanks[i as ValidScoreboardIndex] = player['_currentTank'];
			}
		} else if (this.arenaData.values.flags & ArenaFlags.showsLeaderArrow) this.arenaData.flags ^= ArenaFlags.showsLeaderArrow;
	}

	/**
	 * Updates the size of the map. It should be the only way to modify arena size.
	 */
	public updateBounds(arenaWidth: number, arenaHeight: number) {
		this.width = arenaWidth;
		this.height = arenaHeight;

		this.arenaData.topY = -arenaHeight / 2;
		this.arenaData.bottomY = arenaHeight / 2;
		this.arenaData.leftX = -arenaWidth / 2;
		this.arenaData.rightX = arenaWidth / 2;
	}

	/**
	 * Allows the arena to decide how players are spawned into the game.
	 */
	public spawnPlayer(tank: TankBody, client: Client) {
		const { x, y } = this.findSpawnLocation();

		tank.positionData.values.x = x;
		tank.positionData.values.y = y;
	}

	/**
	 * Closes the arena.
	 */
	public close() {
		for (const client of this.game.clients) {
			client.notify("Arena closed: No players can join", 0xFF0000, -1);
		}

		this.state = ArenaState.CLOSING;
		this.arenaData.flags |= ArenaFlags.noJoining;

		setTimeout(() => {

			const acCount = Math.floor(Math.sqrt(this.width) / 10);
			const radius = this.width * Math.SQRT1_2 + 500;
			for (let i = 0; i < acCount; ++i) {
				const ac = new ArenaCloser(this.game);

				const angle = (i / acCount) * PI2;
				ac.positionData.values.x = Math.cos(angle) * radius;
				ac.positionData.values.y = Math.sin(angle) * radius;
				ac.positionData.values.angle = angle + Math.PI;
			}

			saveToLog("Arena Closing", "Arena running at `" + this.game.gamemode + "` is now closing.", 0xFFE869);
		}, 5000);
	}

	/** Spawns the boss into the arena */
	protected spawnBoss() {
		const TBoss = [Guardian, Summoner, FallenOverlord, FallenBooster, Defender]
			[~~(Math.random() * 5)];
		
		this.boss = new TBoss(this.game);
	}

	public tick(tick: number) {
		this.shapes.tick();

		if (this.allowBoss && this.game.tick >= 1 && (this.game.tick % bossSpawningInterval) === 0 && !this.boss) {
			this.spawnBoss();
		}

        if (this.state === ArenaState.CLOSED) return;

		const players: TankBody[] = [];
		
		for (let id = 0; id <= this.game.entities.lastId; ++id) {
			const entity = this.game.entities.inner[id];
			
			if (Entity.exists(entity) && entity instanceof TankBody && entity.cameraEntity instanceof ClientCamera && entity.cameraEntity.cameraData.values.player === entity) players.push(entity);

			players.sort((p1, p2) => p2.scoreData.values.score - p1.scoreData.values.score);
			this.leader = players[0];
			if (this.leader && this.arenaData.values.flags & ArenaFlags.showsLeaderArrow) {
				this.arenaData.leaderX = this.leader.positionData.values.x;
				this.arenaData.leaderY = this.leader.positionData.values.y;
			}
		}

		// Sorts them too DONT FORGET
		if ((this.game.tick % scoreboardUpdateInterval) === 0) this.updateScoreboard(players);

		if (players.length === 0 && this.state === ArenaState.CLOSING) {
			this.state = ArenaState.CLOSED;

			setTimeout(() => {
				this.game.end();
			}, 10000);
			return;
		}
	}

}
`

~src/Native/Camera.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import Client from "../Client";
import Writer from "../Coder/Writer";
import TankBody from "../Entity/Tank/TankBody";
import ObjectEntity from "../Entity/Object";

import { Entity, EntityStateFlags } from "./Entity";
import { CameraGroup, RelationsGroup } from "./FieldGroups";
import { CameraFlags, ClientBound, levelToScore, levelToScoreTable, PhysicsFlags, Stat } from "../Const/Enums";
import { getTankById } from "../Const/TankDefinitions";
import { removeFast } from "../util";

import { compileCreation, compileUpdate } from "./UpcreateCompiler";
import { maxPlayerLevel } from "../config";

/**
 * Represents any entity with a camera field group.
 */
export class CameraEntity extends Entity {
    /** Always existant camera field group. Present in all GUI/camera entities. */
    public cameraData: CameraGroup = new CameraGroup(this);

    /** The current size of the tank the camera is in charge of. Calculated with level stuff */
    public sizeFactor: number = 1;

    /** Used to set the current camera's level. Should be the only way used to set level. */
    public setLevel(level: number) {
        const previousLevel = this.cameraData.values.level;
        this.cameraData.level = level;
        this.sizeFactor = Math.pow(1.01, level - 1);
        this.cameraData.levelbarMax = level < maxPlayerLevel ? 1 : 0; // quick hack, not correct values
        if (level <= maxPlayerLevel) {
            this.cameraData.score = levelToScore(level);

            const player = this.cameraData.values.player;
            if (Entity.exists(player) && player instanceof TankBody) {
                player.scoreData.score = this.cameraData.values.score;
                player.scoreReward = this.cameraData.values.score;
            }
        }

        // Update stats available
        const statIncrease = ClientCamera.calculateStatCount(level) - ClientCamera.calculateStatCount(previousLevel);
        this.cameraData.statsAvailable += statIncrease;

        this.setFieldFactor(getTankById(this.cameraData.values.tank)?.fieldFactor || 1);
    }

    /** Sets the current FOV by field factor. */
    public setFieldFactor(fieldFactor: number) {
        this.cameraData.FOV = (.55 * fieldFactor) / Math.pow(1.01, (this.cameraData.values.level - 1) / 2);
    }

    public tick(tick: number) {
        if (Entity.exists(this.cameraData.values.player)) {
            const focus = this.cameraData.values.player;
            if (!(this.cameraData.values.flags & CameraFlags.usesCameraCoords) && focus instanceof ObjectEntity) {
                this.cameraData.cameraX = focus.rootParent.positionData.values.x;
                this.cameraData.cameraY = focus.rootParent.positionData.values.y;
            }

            if (this.cameraData.values.player instanceof TankBody) {
                // Update player related data
                const player = this.cameraData.values.player as TankBody;

                const score = this.cameraData.values.score;
                let newLevel = this.cameraData.values.level;
                while (newLevel < levelToScoreTable.length && score - levelToScore(newLevel + 1) >= 0) newLevel += 1

                if (newLevel !== this.cameraData.values.level) {
                    this.setLevel(newLevel);
                    this.cameraData.score = score;
                }

                if (newLevel < levelToScoreTable.length) {
                    const levelScore = levelToScore(this.cameraData.values.level)
                    this.cameraData.levelbarMax = levelToScore(this.cameraData.values.level + 1) - levelScore;
                    this.cameraData.levelbarProgress = score - levelScore;
                }

                this.cameraData.movementSpeed = player.definition.speed * 2.55 * Math.pow(1.07, this.cameraData.values.statLevels.values[Stat.MovementSpeed]) / Math.pow(1.015, this.cameraData.values.level - 1)
            }
        } else {
            this.cameraData.flags |= CameraFlags.usesCameraCoords;
        }
    }
}

/**
 * This is the entity that controls stats and other gui data.
 * It is also the class that compiles entity data and sends it to the client.
 */
export default class ClientCamera extends CameraEntity {
    /** Client interface. */
    public client: Client;
    /** All entities in the view of the camera. Represented by id. */
    private view: Entity[] = [];

    /** Always existant relations field group. Present in all GUI/camera entities. */
    public relationsData: RelationsGroup = new RelationsGroup(this);
    /** Entity being spectated if any (deathscreen). */
    public spectatee: ObjectEntity | null = null;

    /** Calculates the amount of stats available at a specific level. */
    public static calculateStatCount(level: number) {
        if (level <= 0) return 0;
        if (level <= 28) return level - 1;

        return Math.floor(level / 3) + 18;
    }

    public constructor(game: GameServer, client: Client) {
        super(game);

        this.client = client;

        this.cameraData.values.respawnLevel = this.cameraData.values.level = this.cameraData.values.score = 1;

        this.cameraData.values.FOV = .35;
        this.relationsData.values.team = this;
    }

    /** Adds an entity the camera's current view. */
    private addToView(entity: Entity) {
        let c = this.view.find(r => r.id === entity.id)
        if (c) {
            console.log(c.toString(), entity.toString(), c === entity)
        }
        this.view.push(entity);
    }

    /** Removes an entity the camera's current view. */
    private removeFromView(id: number) {
        const index = this.view.findIndex(r => r.id === id);
        if (index === -1) return;

        removeFast(this.view, index);
    }

    /** Updates the camera's current view. */
    private updateView(tick: number) {
        const w = this.client.write().u8(ClientBound.Update).vu(tick);

        const deletes: { id: number, hash: number, noDelete?: boolean }[] = [];
        const updates: Entity[] = [];
        const creations: Entity[] = [];

        const fov = this.cameraData.values.FOV;
        const width = (1920 / fov) / 1.5;
        const height = (1080 / fov) / 1.5;

        // TODO(speed)
        const entitiesNearRange = this.game.entities.collisionManager.retrieve(this.cameraData.values.cameraX, this.cameraData.values.cameraY, width, height);
        const entitiesInRange: ObjectEntity[] = [];

        const l = this.cameraData.values.cameraX - width;
        const r = this.cameraData.values.cameraX + width;
        const t = this.cameraData.values.cameraY - height;
        const b = this.cameraData.values.cameraY + height;
        for (let i = 0; i < entitiesNearRange.length; ++i) {
            const entity = entitiesNearRange[i];
            const width = entity.physicsData.values.sides === 2 ? entity.physicsData.values.size / 2 : entity.physicsData.values.size;
            const size = entity.physicsData.values.sides === 2 ? entity.physicsData.values.width / 2 : entity.physicsData.values.size;
                     
            if (entity.positionData.values.x - width < r &&
                entity.positionData.values.y + size > t &&
                entity.positionData.values.x + width > l &&
                entity.positionData.values.y - size < b) {
                    if (entity !== this.cameraData.values.player &&!(entity.styleData.values.opacity === 0 && !entity.deletionAnimation)) {
                        entitiesInRange.push(entity);
                    }
                }
        }

        for (let id = 0; id <= this.game.entities.lastId; ++id) {
            const entity = this.game.entities.inner[id];
            
            if (entity instanceof ObjectEntity && !entitiesInRange.includes(entity) && (entity.physicsData.values.flags & PhysicsFlags.showsOnMap)) entitiesInRange.push(entity);
        }

        if (Entity.exists(this.cameraData.values.player) && this.cameraData.values.player instanceof ObjectEntity) entitiesInRange.push(this.cameraData.values.player);

        for (let i = 0; i < this.view.length; ++i) {
            const entity = this.view[i]
            if (entity instanceof ObjectEntity) {
                // TODO(speed)
                // Orphan children must be destroyed
                if (!entitiesInRange.includes(entity.rootParent)) {
                    deletes.push({id: entity.id, hash: entity.preservedHash});
                    continue;
                }
            }
            // If the entity is gone, notify the client, if its updated, notify the client
            if (entity.hash === 0) {
                deletes.push({ id: entity.id, hash: entity.preservedHash });
            } else if (entity.entityState & EntityStateFlags.needsCreate) {
                if (entity.entityState & EntityStateFlags.needsDelete) deletes.push({hash: entity.hash, id: entity.id, noDelete: true});
                creations.push(entity);
            } else if (entity.entityState & EntityStateFlags.needsUpdate) {
                updates.push(entity);
            }
        }

        // Now compile
        w.vu(deletes.length);
        for (let i = 0; i < deletes.length; ++i) {
            w.entid(deletes[i]);
            if (!deletes[i].noDelete) this.removeFromView(deletes[i].id);
        }

        // Yeah.
        if (this.view.length === 0) {
            creations.push(this.game.arena, this);
            this.view.push(this.game.arena, this);
        }
        
        const entities = this.game.entities;
        for (const id of this.game.entities.otherEntities) {
            // TODO(speed)
            if (this.view.findIndex(r => r.id === id) === -1) {
                const entity = entities.inner[id];

                if (!entity) continue;
                if (entity instanceof CameraEntity) continue;

                creations.push(entity);

                this.addToView(entity);
            }
        }

        for (const entity of entitiesInRange) {
            if (this.view.indexOf(entity) === -1) {
                creations.push(entity);
                this.addToView(entity);

                if (entity instanceof ObjectEntity) {
                    if (entity.children.length && !entity.isChild) {
                        // add any of its children
                        this.view.push.apply(this.view, entity.children);
                        creations.push.apply(creations, entity.children);
                    }
                }
            } else {
                if (!Entity.exists(entity)) throw new Error("wtf");
                // add untracked children, if it has any
                if (entity.children.length && !entity.isChild) {
                    for (let child of entity.children) {
                        if (this.view.findIndex(r => r.id === child.id) === -1) {
                            this.view.push.apply(this.view, entity.children);
                            creations.push.apply(creations, entity.children);
                        } //else if (child.hash === 0) deletes.push({hash: child.preservedHash, id: child})
                    }
                }
            }
        }

        // Arrays of entities
        w.vu(creations.length + updates.length);
        for (let i = 0; i < updates.length; ++i) {
            this.compileUpdate(w, updates[i]);
        }
        for (let i = 0; i < creations.length; ++i) {
            this.compileCreation(w, creations[i]);
        }

        w.send();
    }

    /** Entity creation compiler function... Run! */
    private compileCreation(w: Writer, entity: Entity) {
        compileCreation(this, w, entity);
    }

    /** Entity update compiler function... Run! */
    private compileUpdate(w: Writer, entity: Entity) {
        compileUpdate(this, w, entity);
    }

    public tick(tick: number) {
        super.tick(tick);

        if (!Entity.exists(this.cameraData.values.player) || !(this.cameraData.values.player instanceof TankBody)) {
            if (Entity.exists(this.spectatee)) {
                const pos = this.spectatee.rootParent.positionData.values;
                this.cameraData.cameraX = pos.x;
                this.cameraData.cameraY = pos.y;
                this.cameraData.flags |= CameraFlags.usesCameraCoords;
            }
        }

        // always last
        this.updateView(tick);
    }
}
`

~src/Native/Entity.TEMPLATE:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
# for every group, import that group
import { ^<<EXISTANT_FIELD_GROUPS.map(e => e[0] + 'Group').join(', ')>>^ } from "./FieldGroups";


/**
 * The flags used for Entity.state property. Signals to the
 * manager and the camera what needs to be sent to the client.
 */
export const enum EntityStateFlags {
    needsUpdate = 1 << 0,
    needsCreate = 1 << 1,
    needsDelete = 1 << 2
}

/**
 * The abstract Entity class which is used for all data in the game.
 * All entities can be compiled by a Camera class.
 * For more details read [entities.md](https://github.com/ABCxFF/diepindepth/blob/main/entities.md).
 */
export class Entity {
    /**
     * Determines if the first parameter is an entity and not a deleted one.
     */
    public static exists(entity: Entity | null | undefined): entity is Entity {
        return entity instanceof Entity && entity.hash !== 0
    }

    /**
     * - `0b01`: Has updated
     * - `0b10`: Needs creation (unused)
     */
    public entityState = 0;
# for every group, define it on the Entity class
^<<
EXISTANT_FIELD_GROUPS.map(e =>
`\n    /**
     * ${e[0]} field group - contains fields relating to ${e[0].toLowerCase()}
     */
    public ${e[0].toLowerCase()}Data: ${e[0]}Group | null = null;`).join('');
>>^

    /** The current game server. */
    public game: GameServer;

    /** Entity id */
    public id: number = -1;
    /** Entity hash (will be 0 once the entity is deleted) */
    public hash: number = 0;
    /** Preserved entity hash (is never set to 0) */
    public preservedHash: number = 0;

    public constructor(game: GameServer) {
        this.game = game;

        game.entities.add(this);
    }

    /** Makes the  entity no longer in need of update. */
    public wipeState() {
# for every group, output code to clear (wipe) that group's state
^<<
EXISTANT_FIELD_GROUPS.map(e =>
`       if (this.${e[0].toLowerCase()}Data) this.${e[0].toLowerCase()}Data.wipe();`).join('\n');
>>^

        this.entityState = 0;
    }

    /** Deletes the entity from the entity manager system. */
    public delete() {
        this.wipeState();
        this.game.entities.delete(this.id);
    }

    /** Ticks the entity */
    public tick(tick: number) {}

    public toString() {
        return `${this.constructor.name} <${this.id}, ${this.preservedHash}>${this.hash === 0 ? "(deleted)" : ""}`
    }

    public [Symbol.toPrimitive](type: string) {
        if (type === "string") return this.toString()
        return this.preservedHash * 0x10000 + this.id;
    }
}
`

~src/Native/Entity.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import GameServer from "../Game";
import {  /* <template> auto-generated */ RelationsGroup, BarrelGroup, PhysicsGroup, HealthGroup, ArenaGroup, NameGroup, CameraGroup, PositionGroup, StyleGroup, ScoreGroup, TeamGroup } from "./FieldGroups";


/**
 * The flags used for Entity.state property. Signals to the
 * manager and the camera what needs to be sent to the client.
 */
export const enum EntityStateFlags {
    needsUpdate = 1 << 0,
    needsCreate = 1 << 1,
    needsDelete = 1 << 2
}

/**
 * The abstract Entity class which is used for all data in the game.
 * All entities can be compiled by a Camera class.
 * For more details read [entities.md](https://github.com/ABCxFF/diepindepth/blob/main/entities.md).
 */
export class Entity {
    /**
     * Determines if the first parameter is an entity and not a deleted one.
     */
    public static exists(entity: Entity | null | undefined): entity is Entity {
        return entity instanceof Entity && entity.hash !== 0
    }

    /**
     * - `0b01`: Has updated
     * - `0b10`: Needs creation (unused)
     */
    public entityState = 0;
 /* <template> auto-generated */ 
    /**
     * Relations field group - contains fields relating to relations
     */
    public relationsData: RelationsGroup | null = null;
    /**
     * Barrel field group - contains fields relating to barrel
     */
    public barrelData: BarrelGroup | null = null;
    /**
     * Physics field group - contains fields relating to physics
     */
    public physicsData: PhysicsGroup | null = null;
    /**
     * Health field group - contains fields relating to health
     */
    public healthData: HealthGroup | null = null;
    /**
     * Arena field group - contains fields relating to arena
     */
    public arenaData: ArenaGroup | null = null;
    /**
     * Name field group - contains fields relating to name
     */
    public nameData: NameGroup | null = null;
    /**
     * Camera field group - contains fields relating to camera
     */
    public cameraData: CameraGroup | null = null;
    /**
     * Position field group - contains fields relating to position
     */
    public positionData: PositionGroup | null = null;
    /**
     * Style field group - contains fields relating to style
     */
    public styleData: StyleGroup | null = null;
    /**
     * Score field group - contains fields relating to score
     */
    public scoreData: ScoreGroup | null = null;
    /**
     * Team field group - contains fields relating to team
     */
    public teamData: TeamGroup | null = null;

    /** The current game server. */
    public game: GameServer;

    /** Entity id */
    public id: number = -1;
    /** Entity hash (will be 0 once the entity is deleted) */
    public hash: number = 0;
    /** Preserved entity hash (is never set to 0) */
    public preservedHash: number = 0;

    public constructor(game: GameServer) {
        this.game = game;

        game.entities.add(this);
    }

    /** Makes the  entity no longer in need of update. */
    public wipeState() {
 /* <template> auto-generated */        if (this.relationsData) this.relationsData.wipe();
       if (this.barrelData) this.barrelData.wipe();
       if (this.physicsData) this.physicsData.wipe();
       if (this.healthData) this.healthData.wipe();
       if (this.arenaData) this.arenaData.wipe();
       if (this.nameData) this.nameData.wipe();
       if (this.cameraData) this.cameraData.wipe();
       if (this.positionData) this.positionData.wipe();
       if (this.styleData) this.styleData.wipe();
       if (this.scoreData) this.scoreData.wipe();
       if (this.teamData) this.teamData.wipe();

        this.entityState = 0;
    }

    /** Deletes the entity from the entity manager system. */
    public delete() {
        this.wipeState();
        this.game.entities.delete(this.id);
    }

    /** Ticks the entity */
    public tick(tick: number) {}

    public toString() {
        return `${this.constructor.name} <${this.id}, ${this.preservedHash}>${this.hash === 0 ? "(deleted)" : ""}`
    }

    public [Symbol.toPrimitive](type: string) {
        if (type === "string") return this.toString()
        return this.preservedHash * 0x10000 + this.id;
    }
}
`

~src/Native/FieldGroups.TEMPLATE:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

#
#   ==========================================
#           CHANGE NOTHING IN THIS FILE
#   ==========================================
#

import { ValidScoreboardIndex, Stat, Tank, Color } from "../Const/Enums";
import { DevTank } from "../Const/DevTankDefinitions";
import { Entity } from "./Entity";
# For every group, create and export a type of every single possible field name in that group
^<<EXISTANT_FIELD_GROUPS.map(group => `\nexport type ${group[0]}FieldName = ${group[1].map(field => '"' + field.name + '"').join(" | ")};`).join("");>>^

# Assume scoreboard and camera are forever present

export class ScoreboardTable<ValueType> implements Record<ValidScoreboardIndex, ValueType> {
    state: Uint8Array;
    values: ValueType[];
    owner: ArenaGroup;
    fieldId: number;

    constructor(defaultValue: ValueType, fieldId: number, owner: ArenaGroup) {
        this.state = new Uint8Array(10);
        this.values = Array(10).fill(defaultValue);
        this.fieldId = fieldId;
        this.owner = owner;
    }

    public wipe() {
        this.state.fill(0);
    }

    get [0]() {
        return this.values[0];
    }
    set [0](value) {
        if (value === this.values[0]) return;
            
        this.state[0] |= 1;
        this.values[0] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [1]() {
        return this.values[1];
    }
    set [1](value) {
        if (value === this.values[1]) return;
            
        this.state[1] |= 1;
        this.values[1] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [2]() {
        return this.values[2];
    }
    set [2](value) {
        if (value === this.values[2]) return;
            
        this.state[2] |= 1;
        this.values[2] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [3]() {
        return this.values[3];
    }
    set [3](value) {
        if (value === this.values[3]) return;
            
        this.state[3] |= 1;
        this.values[3] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [4]() {
        return this.values[4];
    }
    set [4](value) {
        if (value === this.values[4]) return;
            
        this.state[4] |= 1;
        this.values[4] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [5]() {
        return this.values[5];
    }
    set [5](value) {
        if (value === this.values[5]) return;
            
        this.state[5] |= 1;
        this.values[5] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [6]() {
        return this.values[6];
    }
    set [6](value) {
        if (value === this.values[6]) return;
            
        this.state[6] |= 1;
        this.values[6] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [7]() {
        return this.values[7];
    }
    set [7](value) {
        if (value === this.values[7]) return;
            
        this.state[7] |= 1;
        this.values[7] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [8]() {
        return this.values[8];
    }
    set [8](value) {
        if (value === this.values[8]) return;
            
        this.state[8] |= 1;
        this.values[8] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [9]() {
        return this.values[9];
    }
    set [9](value) {
        if (value === this.values[9]) return;
            
        this.state[9] |= 1;
        this.values[9] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
}

export class CameraTable<ValueType> implements Record<Stat, ValueType> {
    state: Uint8Array;
    values: ValueType[];
    owner: CameraGroup;
    fieldId: number;

    constructor(defaultValue: ValueType, fieldId: number, owner: CameraGroup) {
        this.state = new Uint8Array(8);
        this.values = Array(8).fill(defaultValue);
        this.fieldId = fieldId;
        this.owner = owner;
    }

    public wipe() {
        this.state.fill(0);
    }

    get [0]() {
        return this.values[0];
    }
    set [0](value) {
        if (value === this.values[0]) return;
            
        this.state[0] |= 1;
        this.values[0] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [1]() {
        return this.values[1];
    }
    set [1](value) {
        if (value === this.values[1]) return;
            
        this.state[1] |= 1;
        this.values[1] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [2]() {
        return this.values[2];
    }
    set [2](value) {
        if (value === this.values[2]) return;
            
        this.state[2] |= 1;
        this.values[2] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [3]() {
        return this.values[3];
    }
    set [3](value) {
        if (value === this.values[3]) return;
            
        this.state[3] |= 1;
        this.values[3] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [4]() {
        return this.values[4];
    }
    set [4](value) {
        if (value === this.values[4]) return;
            
        this.state[4] |= 1;
        this.values[4] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [5]() {
        return this.values[5];
    }
    set [5](value) {
        if (value === this.values[5]) return;
            
        this.state[5] |= 1;
        this.values[5] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [6]() {
        return this.values[6];
    }
    set [6](value) {
        if (value === this.values[6]) return;
            
        this.state[6] |= 1;
        this.values[6] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [7]() {
        return this.values[7];
    }
    set [7](value) {
        if (value === this.values[7]) return;
            
        this.state[7] |= 1;
        this.values[7] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
}

# First fill out the default field value if its unset
^<<(EXISTANT_FIELD_GROUPS.forEach(group => group[1].forEach(field => {
    // if it has a default value, stringify it and return
    if (field.hasOwnProperty('default')) return field.default = JSON.stringify(field.default);
    // find the default based off of the type
    field.default = ({
        Color: "Color.Border",
        string: '""',
        number: 0,
        "Tank | DevTank": "Tank.Basic",
        Entity: "null"
    })[field.type];
})), "")>>^
# Define out field groups using field groups
^<<EXISTANT_FIELD_GROUPS.map(group =>
`\nexport class ${group[0]}Group {
    entity: Entity;
    state: Uint8Array = new Uint8Array(${group[1].length});
    values: { ${group[1].map((f,i) => f.name + ": " + (
        f.amount ? f.name.startsWith("scoreboard") ? "ScoreboardTable<" + f.type + ">" : f.name.startsWith("stat") ? "CameraTable<" + f.type + ">" : "null"
        : (f.type === "Entity" ? ("null | Entity") : f.type)
)).join(", ")} } = {
        ${group[1].map((f,i) => f.name + ": " + (
            f.amount ? f.name.startsWith("scoreboard") ? `new ScoreboardTable(${f.default}, ${i}, this)` : f.name.startsWith("stat") ? `new CameraTable(${f.default}, ${i}, this)` : "null"
            : (f.default)
        )).join(",\n        ")}
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); ${group[1].filter(f=>f.amount).map(f => "this.values." + f.name + ".wipe(); ").join("")}}${
        `${group[1].map(f => "\n    get " + f.name + "() { return this.values." + f.name + "; }").join("")}`
        +
        `${group[1]
.map((f,i) => f.amount ? "" : "\n    set " + f.name + "(" + f.name + ": " + (f.type === "Entity" ? ("null | Entity") : f.type) + ") { " +
`if (${f.name} === this.values.${f.name}) { return; }; this.state[${i}] |= 1; this.entity.entityState |= 1; this.values.${f.name} = ${f.name};`
+ " }").join("")}`
    }
}`
).join('\n')>>^
`

~src/Native/FieldGroups.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/


import { ValidScoreboardIndex, Stat, Tank, Color } from "../Const/Enums";
import { DevTank } from "../Const/DevTankDefinitions";
import { Entity } from "./Entity";
 /* <template> auto-generated */ 
export type RelationsFieldName = "parent" | "owner" | "team";
export type BarrelFieldName = "flags" | "reloadTime" | "trapezoidDirection";
export type PhysicsFieldName = "flags" | "sides" | "size" | "width" | "absorbtionFactor" | "pushFactor";
export type HealthFieldName = "flags" | "health" | "maxHealth";
export type ArenaFieldName = "flags" | "leftX" | "topY" | "rightX" | "bottomY" | "scoreboardAmount" | "scoreboardNames" | "scoreboardScores" | "scoreboardColors" | "scoreboardSuffixes" | "scoreboardTanks" | "leaderX" | "leaderY" | "playersNeeded" | "ticksUntilStart";
export type NameFieldName = "flags" | "name";
export type CameraFieldName = "unusedClientId" | "flags" | "player" | "FOV" | "level" | "tank" | "levelbarProgress" | "levelbarMax" | "statsAvailable" | "statNames" | "statLevels" | "statLimits" | "cameraX" | "cameraY" | "score" | "respawnLevel" | "killedBy" | "spawnTick" | "deathTick" | "tankOverride" | "movementSpeed";
export type PositionFieldName = "x" | "y" | "angle" | "flags";
export type StyleFieldName = "flags" | "color" | "borderWidth" | "opacity" | "zIndex";
export type ScoreFieldName = "score";
export type TeamFieldName = "teamColor" | "mothershipX" | "mothershipY" | "flags";


export class ScoreboardTable<ValueType> implements Record<ValidScoreboardIndex, ValueType> {
    state: Uint8Array;
    values: ValueType[];
    owner: ArenaGroup;
    fieldId: number;

    constructor(defaultValue: ValueType, fieldId: number, owner: ArenaGroup) {
        this.state = new Uint8Array(10);
        this.values = Array(10).fill(defaultValue);
        this.fieldId = fieldId;
        this.owner = owner;
    }

    public wipe() {
        this.state.fill(0);
    }

    get [0]() {
        return this.values[0];
    }
    set [0](value) {
        if (value === this.values[0]) return;
            
        this.state[0] |= 1;
        this.values[0] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [1]() {
        return this.values[1];
    }
    set [1](value) {
        if (value === this.values[1]) return;
            
        this.state[1] |= 1;
        this.values[1] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [2]() {
        return this.values[2];
    }
    set [2](value) {
        if (value === this.values[2]) return;
            
        this.state[2] |= 1;
        this.values[2] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [3]() {
        return this.values[3];
    }
    set [3](value) {
        if (value === this.values[3]) return;
            
        this.state[3] |= 1;
        this.values[3] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [4]() {
        return this.values[4];
    }
    set [4](value) {
        if (value === this.values[4]) return;
            
        this.state[4] |= 1;
        this.values[4] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [5]() {
        return this.values[5];
    }
    set [5](value) {
        if (value === this.values[5]) return;
            
        this.state[5] |= 1;
        this.values[5] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [6]() {
        return this.values[6];
    }
    set [6](value) {
        if (value === this.values[6]) return;
            
        this.state[6] |= 1;
        this.values[6] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [7]() {
        return this.values[7];
    }
    set [7](value) {
        if (value === this.values[7]) return;
            
        this.state[7] |= 1;
        this.values[7] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [8]() {
        return this.values[8];
    }
    set [8](value) {
        if (value === this.values[8]) return;
            
        this.state[8] |= 1;
        this.values[8] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [9]() {
        return this.values[9];
    }
    set [9](value) {
        if (value === this.values[9]) return;
            
        this.state[9] |= 1;
        this.values[9] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
}

export class CameraTable<ValueType> implements Record<Stat, ValueType> {
    state: Uint8Array;
    values: ValueType[];
    owner: CameraGroup;
    fieldId: number;

    constructor(defaultValue: ValueType, fieldId: number, owner: CameraGroup) {
        this.state = new Uint8Array(8);
        this.values = Array(8).fill(defaultValue);
        this.fieldId = fieldId;
        this.owner = owner;
    }

    public wipe() {
        this.state.fill(0);
    }

    get [0]() {
        return this.values[0];
    }
    set [0](value) {
        if (value === this.values[0]) return;
            
        this.state[0] |= 1;
        this.values[0] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [1]() {
        return this.values[1];
    }
    set [1](value) {
        if (value === this.values[1]) return;
            
        this.state[1] |= 1;
        this.values[1] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [2]() {
        return this.values[2];
    }
    set [2](value) {
        if (value === this.values[2]) return;
            
        this.state[2] |= 1;
        this.values[2] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [3]() {
        return this.values[3];
    }
    set [3](value) {
        if (value === this.values[3]) return;
            
        this.state[3] |= 1;
        this.values[3] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [4]() {
        return this.values[4];
    }
    set [4](value) {
        if (value === this.values[4]) return;
            
        this.state[4] |= 1;
        this.values[4] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [5]() {
        return this.values[5];
    }
    set [5](value) {
        if (value === this.values[5]) return;
            
        this.state[5] |= 1;
        this.values[5] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [6]() {
        return this.values[6];
    }
    set [6](value) {
        if (value === this.values[6]) return;
            
        this.state[6] |= 1;
        this.values[6] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
    get [7]() {
        return this.values[7];
    }
    set [7](value) {
        if (value === this.values[7]) return;
            
        this.state[7] |= 1;
        this.values[7] = value;

        this.owner.state[this.fieldId] |= 1;
        this.owner.entity.entityState |= 1;
    }
}

 /* <template> auto-generated */ 
 /* <template> auto-generated */ 
export class RelationsGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(3);
    values: { parent: null | Entity, owner: null | Entity, team: null | Entity } = {
        parent: null,
        owner: null,
        team: null
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get parent() { return this.values.parent; }
    get owner() { return this.values.owner; }
    get team() { return this.values.team; }
    set parent(parent: null | Entity) { if (parent === this.values.parent) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.parent = parent; }
    set owner(owner: null | Entity) { if (owner === this.values.owner) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.owner = owner; }
    set team(team: null | Entity) { if (team === this.values.team) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.team = team; }
}

export class BarrelGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(3);
    values: { flags: number, reloadTime: number, trapezoidDirection: number } = {
        flags: 0,
        reloadTime: 15,
        trapezoidDirection: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get flags() { return this.values.flags; }
    get reloadTime() { return this.values.reloadTime; }
    get trapezoidDirection() { return this.values.trapezoidDirection; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set reloadTime(reloadTime: number) { if (reloadTime === this.values.reloadTime) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.reloadTime = reloadTime; }
    set trapezoidDirection(trapezoidDirection: number) { if (trapezoidDirection === this.values.trapezoidDirection) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.trapezoidDirection = trapezoidDirection; }
}

export class PhysicsGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(6);
    values: { flags: number, sides: number, size: number, width: number, absorbtionFactor: number, pushFactor: number } = {
        flags: 0,
        sides: 0,
        size: 0,
        width: 0,
        absorbtionFactor: 1,
        pushFactor: 8
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get flags() { return this.values.flags; }
    get sides() { return this.values.sides; }
    get size() { return this.values.size; }
    get width() { return this.values.width; }
    get absorbtionFactor() { return this.values.absorbtionFactor; }
    get pushFactor() { return this.values.pushFactor; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set sides(sides: number) { if (sides === this.values.sides) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.sides = sides; }
    set size(size: number) { if (size === this.values.size) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.size = size; }
    set width(width: number) { if (width === this.values.width) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.width = width; }
    set absorbtionFactor(absorbtionFactor: number) { if (absorbtionFactor === this.values.absorbtionFactor) { return; }; this.state[4] |= 1; this.entity.entityState |= 1; this.values.absorbtionFactor = absorbtionFactor; }
    set pushFactor(pushFactor: number) { if (pushFactor === this.values.pushFactor) { return; }; this.state[5] |= 1; this.entity.entityState |= 1; this.values.pushFactor = pushFactor; }
}

export class HealthGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(3);
    values: { flags: number, health: number, maxHealth: number } = {
        flags: 0,
        health: 1,
        maxHealth: 1
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get flags() { return this.values.flags; }
    get health() { return this.values.health; }
    get maxHealth() { return this.values.maxHealth; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set health(health: number) { if (health === this.values.health) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.health = health; }
    set maxHealth(maxHealth: number) { if (maxHealth === this.values.maxHealth) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.maxHealth = maxHealth; }
}

export class ArenaGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(15);
    values: { flags: number, leftX: number, topY: number, rightX: number, bottomY: number, scoreboardAmount: number, scoreboardNames: ScoreboardTable<string>, scoreboardScores: ScoreboardTable<number>, scoreboardColors: ScoreboardTable<Color>, scoreboardSuffixes: ScoreboardTable<string>, scoreboardTanks: ScoreboardTable<Tank | DevTank>, leaderX: number, leaderY: number, playersNeeded: number, ticksUntilStart: number } = {
        flags: 2,
        leftX: 0,
        topY: 0,
        rightX: 0,
        bottomY: 0,
        scoreboardAmount: 0,
        scoreboardNames: new ScoreboardTable("", 6, this),
        scoreboardScores: new ScoreboardTable(0, 7, this),
        scoreboardColors: new ScoreboardTable(13, 8, this),
        scoreboardSuffixes: new ScoreboardTable("", 9, this),
        scoreboardTanks: new ScoreboardTable(Tank.Basic, 10, this),
        leaderX: 0,
        leaderY: 0,
        playersNeeded: 1,
        ticksUntilStart: 250
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); this.values.scoreboardNames.wipe(); this.values.scoreboardScores.wipe(); this.values.scoreboardColors.wipe(); this.values.scoreboardSuffixes.wipe(); this.values.scoreboardTanks.wipe(); }
    get flags() { return this.values.flags; }
    get leftX() { return this.values.leftX; }
    get topY() { return this.values.topY; }
    get rightX() { return this.values.rightX; }
    get bottomY() { return this.values.bottomY; }
    get scoreboardAmount() { return this.values.scoreboardAmount; }
    get scoreboardNames() { return this.values.scoreboardNames; }
    get scoreboardScores() { return this.values.scoreboardScores; }
    get scoreboardColors() { return this.values.scoreboardColors; }
    get scoreboardSuffixes() { return this.values.scoreboardSuffixes; }
    get scoreboardTanks() { return this.values.scoreboardTanks; }
    get leaderX() { return this.values.leaderX; }
    get leaderY() { return this.values.leaderY; }
    get playersNeeded() { return this.values.playersNeeded; }
    get ticksUntilStart() { return this.values.ticksUntilStart; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set leftX(leftX: number) { if (leftX === this.values.leftX) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.leftX = leftX; }
    set topY(topY: number) { if (topY === this.values.topY) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.topY = topY; }
    set rightX(rightX: number) { if (rightX === this.values.rightX) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.rightX = rightX; }
    set bottomY(bottomY: number) { if (bottomY === this.values.bottomY) { return; }; this.state[4] |= 1; this.entity.entityState |= 1; this.values.bottomY = bottomY; }
    set scoreboardAmount(scoreboardAmount: number) { if (scoreboardAmount === this.values.scoreboardAmount) { return; }; this.state[5] |= 1; this.entity.entityState |= 1; this.values.scoreboardAmount = scoreboardAmount; }
    set leaderX(leaderX: number) { if (leaderX === this.values.leaderX) { return; }; this.state[11] |= 1; this.entity.entityState |= 1; this.values.leaderX = leaderX; }
    set leaderY(leaderY: number) { if (leaderY === this.values.leaderY) { return; }; this.state[12] |= 1; this.entity.entityState |= 1; this.values.leaderY = leaderY; }
    set playersNeeded(playersNeeded: number) { if (playersNeeded === this.values.playersNeeded) { return; }; this.state[13] |= 1; this.entity.entityState |= 1; this.values.playersNeeded = playersNeeded; }
    set ticksUntilStart(ticksUntilStart: number) { if (ticksUntilStart === this.values.ticksUntilStart) { return; }; this.state[14] |= 1; this.entity.entityState |= 1; this.values.ticksUntilStart = ticksUntilStart; }
}

export class NameGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(2);
    values: { flags: number, name: string } = {
        flags: 0,
        name: ""
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get flags() { return this.values.flags; }
    get name() { return this.values.name; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set name(name: string) { if (name === this.values.name) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.name = name; }
}

export class CameraGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(21);
    values: { unusedClientId: number, flags: number, player: null | Entity, FOV: number, level: number, tank: Tank | DevTank, levelbarProgress: number, levelbarMax: number, statsAvailable: number, statNames: CameraTable<string>, statLevels: CameraTable<number>, statLimits: CameraTable<number>, cameraX: number, cameraY: number, score: number, respawnLevel: number, killedBy: string, spawnTick: number, deathTick: number, tankOverride: string, movementSpeed: number } = {
        unusedClientId: 0,
        flags: 1,
        player: null,
        FOV: 0.35,
        level: 1,
        tank: 53,
        levelbarProgress: 0,
        levelbarMax: 0,
        statsAvailable: 0,
        statNames: new CameraTable("", 9, this),
        statLevels: new CameraTable(0, 10, this),
        statLimits: new CameraTable(0, 11, this),
        cameraX: 0,
        cameraY: 0,
        score: 0,
        respawnLevel: 0,
        killedBy: "",
        spawnTick: 0,
        deathTick: -1,
        tankOverride: "",
        movementSpeed: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); this.values.statNames.wipe(); this.values.statLevels.wipe(); this.values.statLimits.wipe(); }
    get unusedClientId() { return this.values.unusedClientId; }
    get flags() { return this.values.flags; }
    get player() { return this.values.player; }
    get FOV() { return this.values.FOV; }
    get level() { return this.values.level; }
    get tank() { return this.values.tank; }
    get levelbarProgress() { return this.values.levelbarProgress; }
    get levelbarMax() { return this.values.levelbarMax; }
    get statsAvailable() { return this.values.statsAvailable; }
    get statNames() { return this.values.statNames; }
    get statLevels() { return this.values.statLevels; }
    get statLimits() { return this.values.statLimits; }
    get cameraX() { return this.values.cameraX; }
    get cameraY() { return this.values.cameraY; }
    get score() { return this.values.score; }
    get respawnLevel() { return this.values.respawnLevel; }
    get killedBy() { return this.values.killedBy; }
    get spawnTick() { return this.values.spawnTick; }
    get deathTick() { return this.values.deathTick; }
    get tankOverride() { return this.values.tankOverride; }
    get movementSpeed() { return this.values.movementSpeed; }
    set unusedClientId(unusedClientId: number) { if (unusedClientId === this.values.unusedClientId) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.unusedClientId = unusedClientId; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set player(player: null | Entity) { if (player === this.values.player) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.player = player; }
    set FOV(FOV: number) { if (FOV === this.values.FOV) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.FOV = FOV; }
    set level(level: number) { if (level === this.values.level) { return; }; this.state[4] |= 1; this.entity.entityState |= 1; this.values.level = level; }
    set tank(tank: Tank | DevTank) { if (tank === this.values.tank) { return; }; this.state[5] |= 1; this.entity.entityState |= 1; this.values.tank = tank; }
    set levelbarProgress(levelbarProgress: number) { if (levelbarProgress === this.values.levelbarProgress) { return; }; this.state[6] |= 1; this.entity.entityState |= 1; this.values.levelbarProgress = levelbarProgress; }
    set levelbarMax(levelbarMax: number) { if (levelbarMax === this.values.levelbarMax) { return; }; this.state[7] |= 1; this.entity.entityState |= 1; this.values.levelbarMax = levelbarMax; }
    set statsAvailable(statsAvailable: number) { if (statsAvailable === this.values.statsAvailable) { return; }; this.state[8] |= 1; this.entity.entityState |= 1; this.values.statsAvailable = statsAvailable; }
    set cameraX(cameraX: number) { if (cameraX === this.values.cameraX) { return; }; this.state[12] |= 1; this.entity.entityState |= 1; this.values.cameraX = cameraX; }
    set cameraY(cameraY: number) { if (cameraY === this.values.cameraY) { return; }; this.state[13] |= 1; this.entity.entityState |= 1; this.values.cameraY = cameraY; }
    set score(score: number) { if (score === this.values.score) { return; }; this.state[14] |= 1; this.entity.entityState |= 1; this.values.score = score; }
    set respawnLevel(respawnLevel: number) { if (respawnLevel === this.values.respawnLevel) { return; }; this.state[15] |= 1; this.entity.entityState |= 1; this.values.respawnLevel = respawnLevel; }
    set killedBy(killedBy: string) { if (killedBy === this.values.killedBy) { return; }; this.state[16] |= 1; this.entity.entityState |= 1; this.values.killedBy = killedBy; }
    set spawnTick(spawnTick: number) { if (spawnTick === this.values.spawnTick) { return; }; this.state[17] |= 1; this.entity.entityState |= 1; this.values.spawnTick = spawnTick; }
    set deathTick(deathTick: number) { if (deathTick === this.values.deathTick) { return; }; this.state[18] |= 1; this.entity.entityState |= 1; this.values.deathTick = deathTick; }
    set tankOverride(tankOverride: string) { if (tankOverride === this.values.tankOverride) { return; }; this.state[19] |= 1; this.entity.entityState |= 1; this.values.tankOverride = tankOverride; }
    set movementSpeed(movementSpeed: number) { if (movementSpeed === this.values.movementSpeed) { return; }; this.state[20] |= 1; this.entity.entityState |= 1; this.values.movementSpeed = movementSpeed; }
}

export class PositionGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(4);
    values: { x: number, y: number, angle: number, flags: number } = {
        x: 0,
        y: 0,
        angle: 0,
        flags: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get x() { return this.values.x; }
    get y() { return this.values.y; }
    get angle() { return this.values.angle; }
    get flags() { return this.values.flags; }
    set x(x: number) { if (x === this.values.x) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.x = x; }
    set y(y: number) { if (y === this.values.y) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.y = y; }
    set angle(angle: number) { if (angle === this.values.angle) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.angle = angle; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
}

export class StyleGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(5);
    values: { flags: number, color: Color, borderWidth: number, opacity: number, zIndex: number } = {
        flags: 1,
        color: Color.Border,
        borderWidth: 7.5,
        opacity: 1,
        zIndex: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get flags() { return this.values.flags; }
    get color() { return this.values.color; }
    get borderWidth() { return this.values.borderWidth; }
    get opacity() { return this.values.opacity; }
    get zIndex() { return this.values.zIndex; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
    set color(color: Color) { if (color === this.values.color) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.color = color; }
    set borderWidth(borderWidth: number) { if (borderWidth === this.values.borderWidth) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.borderWidth = borderWidth; }
    set opacity(opacity: number) { if (opacity === this.values.opacity) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.opacity = opacity; }
    set zIndex(zIndex: number) { if (zIndex === this.values.zIndex) { return; }; this.state[4] |= 1; this.entity.entityState |= 1; this.values.zIndex = zIndex; }
}

export class ScoreGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(1);
    values: { score: number } = {
        score: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get score() { return this.values.score; }
    set score(score: number) { if (score === this.values.score) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.score = score; }
}

export class TeamGroup {
    entity: Entity;
    state: Uint8Array = new Uint8Array(4);
    values: { teamColor: Color, mothershipX: number, mothershipY: number, flags: number } = {
        teamColor: Color.Border,
        mothershipX: 0,
        mothershipY: 0,
        flags: 0
    };

    constructor(entity: Entity) { this.entity = entity; }
    wipe() { this.state.fill(0); }
    get teamColor() { return this.values.teamColor; }
    get mothershipX() { return this.values.mothershipX; }
    get mothershipY() { return this.values.mothershipY; }
    get flags() { return this.values.flags; }
    set teamColor(teamColor: Color) { if (teamColor === this.values.teamColor) { return; }; this.state[0] |= 1; this.entity.entityState |= 1; this.values.teamColor = teamColor; }
    set mothershipX(mothershipX: number) { if (mothershipX === this.values.mothershipX) { return; }; this.state[1] |= 1; this.entity.entityState |= 1; this.values.mothershipX = mothershipX; }
    set mothershipY(mothershipY: number) { if (mothershipY === this.values.mothershipY) { return; }; this.state[2] |= 1; this.entity.entityState |= 1; this.values.mothershipY = mothershipY; }
    set flags(flags: number) { if (flags === this.values.flags) { return; }; this.state[3] |= 1; this.entity.entityState |= 1; this.values.flags = flags; }
}
`

~src/Native/Manager.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as config from "../config";
import GameServer from "../Game";
import ObjectEntity from "../Entity/Object";

import CollisionManager from "../Physics/CollisionManager";
import SpatialHashing from "../Physics/SpatialHashing";
import DiepQuadTree from "../Physics/QuadTree";

import { CameraEntity } from "./Camera";
import { Entity } from "./Entity";
import { AI } from "../Entity/AI";
import { removeFast } from "../util";

/**
 * Manages all entities in the game.
 */
export default class EntityManager {
    /** The current game. */
    public game: GameServer;
    /** Quad tree or Spatial Hashing system. Used to organize ObjectEntitys. */
    public collisionManager: CollisionManager;

    /** Keeps a count of how many objects existed. */
    public zIndex = 0;

    /** Array of all camera entities. */
    public cameras: number[] = [];
    /** List of all not ObjectEntitys */
    public otherEntities: number[] = [];
    /** List of all Entitys. */
    public inner: (Entity | null)[] = Array(16384);
    /** List of all AIs. */
    public AIs: AI[] = [];

    /** The current hash for each id.  */
    public hashTable = new Uint8Array(16384);
    /** The last used ID */
    public lastId = -1;

    public constructor(game: GameServer) {
        this.game = game;
        this.collisionManager = config.spatialHashingCellSize ? new SpatialHashing(config.spatialHashingCellSize) : new DiepQuadTree(0, 0);
    }

    /** Adds an entity to the system. */
    public add(entity: Entity) {
        const lastId = this.lastId + 1;
        // Until it can find a free id, it goes up.
        for (let id = 0; id <= lastId; ++id) {
            if (this.inner[id]) continue;

            entity.id = id;
            entity.hash = entity.preservedHash = this.hashTable[id] += 1;
            this.inner[id] = entity;
            

            if (this.collisionManager && entity instanceof ObjectEntity) {
            } else if (entity instanceof CameraEntity) this.cameras.push(id);
            else this.otherEntities.push(id);

            if (this.lastId < id) this.lastId = entity.id;

            return entity;
        }

        throw new Error("OOEI: Out Of Entity IDs"); // joy
    }

    /** Removes an entity from the system. */
    public delete(id: number) {
        const entity = this.inner[id];

        if (!entity) throw new RangeError("Deleting entity that isn't in the game?");
        entity.hash = 0;

        if (this.collisionManager && entity instanceof ObjectEntity) {
            // Nothing I guess
        } else if (entity instanceof CameraEntity) removeFast(this.cameras, this.cameras.indexOf(id));
        else removeFast(this.otherEntities, this.otherEntities.indexOf(id));

        // TODO(speed)[not super important]:
        // possibly use removeFast instead of leaving these wholes in the entity array
        this.inner[id] = null;
    }

    /** Wipes all entities from the game. */
    public clear() {
        this.lastId = -1;
        this.collisionManager.reset(0, 0);
        this.hashTable.fill(0);
        this.AIs.length = 0;
        this.otherEntities.length = 0;
        this.cameras.length = 0;

        for (let i = 0; i < this.inner.length; ++i) {
            const entity = this.inner[i];
            if (entity) {
                entity.hash = 0
                this.inner[i] = null;
            }
        }
    }

    /** Ticks all entities in the game. */
    public tick(tick: number) {
        this.collisionManager.reset(this.game.arena.arenaData.values.rightX, this.game.arena.arenaData.values.bottomY);

        while (!this.inner[this.lastId] && this.lastId >= 0) {
            this.lastId -= 1;
        }

        scanner: for (let id = 0; id <= this.lastId; ++id) {
            const entity = this.inner[id];

            if (!Entity.exists(entity)) continue;

            if (entity instanceof ObjectEntity && !entity.isChild) {
                // This loop keeps entities far away from cameras to not be calculated collision for.
                // if (config.usingSpatialHashGrid) {
                //     entity.isViewed = true;
                //     this.collisionManager.insertEntity(entity);
                //     continue;
                // }
                // For quadtree only because its not c++ :(
                // for (let i = 0; i < this.cameras.length; ++i) {
                //     const camera = this.inner[this.cameras[i]] as CameraEntity;// small trick
                //     if ((camera.camera.values.cameraX - entity.position.values.x) ** 2 + (camera.camera.values.cameraY - entity.position.values.y) ** 2 < (4500 + entity.physics.values.size + entity.physics.values.width) ** 2) {
                //         this.collisionManager.insertEntity(entity);
                //         entity.isViewed = true;
                //         continue scanner;
                //     }
                // }
                this.collisionManager.insertEntity(entity);
                entity.isViewed = true;
            }
        }

        for (let id = 0; id <= this.lastId; ++id) {
            const entity = this.inner[id];

            if (entity && entity instanceof ObjectEntity && entity.isPhysical) {
                entity.applyPhysics();
            }
        }

        for (let id = 0; id <= this.lastId; ++id) {
            const entity = this.inner[id];

            if (!Entity.exists(entity)) continue;

            if (!(entity instanceof CameraEntity)) {
                if (!(entity instanceof ObjectEntity) || !entity.isChild) entity.tick(tick);
            }
        }

        for (let i = this.AIs.length; --i >= 0;) {
            if (!Entity.exists(this.AIs[i].owner)) {
                removeFast(this.game.entities.AIs, i);
                continue;
            }
            this.AIs[i].tick(tick);
        }

        for (let i = 0; i < this.cameras.length; ++i) {
            (this.inner[this.cameras[i]] as CameraEntity).tick(tick);
        }

        for (let id = 0; id <= this.lastId; ++id) {
            const entity = this.inner[id];

            if (entity) {
                entity.wipeState();
            }
        }
    }
}
`

~src/Native/TEMPLATIZER.js:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/*
    ==========================================
            CHANGE NOTHING IN THIS FILE
    ==========================================
*/

const FIELD_ORDER = "Position:y,Position:x,Position:angle,Physics:size,Camera:player,Arena:flags,Style:color,Arena:scoreboardColors,Camera:killedBy,Arena:playersNeeded,Physics:sides,Team:flags,Health:flags,Arena:scoreboardTanks,Camera:respawnLevel,Camera:levelbarProgress,Camera:spawnTick,Physics:absorbtionFactor,Arena:leaderX,Health:maxHealth,Style:flags,!Example:exampleTest,Barrel:trapezoidDirection,Position:flags,Arena:scoreboardNames,Camera:score,Team:mothershipY,Arena:scoreboardSuffixes,Name:flags,Camera:movementSpeed,Arena:leaderY,Arena:bottomY,Relations:team,Camera:level,Team:teamColor,Camera:FOV,Camera:statLimits,Arena:leftX,Arena:scoreboardScores,Camera:statLevels,Camera:tankOverride,Camera:tank,Style:borderWidth,Camera:deathTick,Physics:width,Camera:statsAvailable,Barrel:flags,Camera:levelbarMax,Name:name,Relations:owner,Health:health,Camera:cameraY,Style:opacity,Barrel:reloadTime,Camera:statNames,Camera:cameraX,Team:mothershipX,Camera:unusedClientId,Relations:parent,Style:zIndex,Camera:flags,Arena:rightX,Physics:pushFactor,Physics:flags,Arena:scoreboardAmount,Arena:ticksUntilStart,Arena:topY,Score:score".split(',').map(e => e.split(':'));

const FIELD_GROUPS = [
    [
        "Relations",
        [
            {
                "name": "parent",
                "encType": "entid",
                "type": "Entity"
            },
            {
                "name": "owner",
                "encType": "entid",
                "type": "Entity"
            },
            {
                "name": "team",
                "encType": "entid",
                "type": "Entity"
            }
        ]
    ],
    ["!CPUControllable", []], // Unused, Unsupported
    [
        "Barrel",
        [
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "default": 15,
                "name": "reloadTime",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "trapezoidDirection",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    [
        "Physics",
        [
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "sides",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "size",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "width",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 1,
                "name": "absorbtionFactor",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 8,
                "name": "pushFactor",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    [
        "Health",
        [
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "default": 1.0,
                "name": "health",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 1.0,
                "name": "maxHealth",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    ["!Dominator", []], // Unused, Unsupported
    ["!Example",[{"name": "exampleTest","encType": "vi","type": "number"}]], // Unused
    // [
    //     "Lobby",
    //     [
    //         {
    //             "name": "playerCount",
    //             "encType": "vu"
    //         },
    //         {
    //             "amount": 80,
    //             "name": "playerIds",
    //             "encType": "stringNT",
    //         },
    //         {
    //             "amount": 80,
    //             "name": "playerNames",
    //             "encType": "stringNT",
    //         }
    //     ]
    // ],
    [
        "Arena",
        [
            {
                "default": 2,
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "leftX",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "topY",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "rightX",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "bottomY",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "scoreboardAmount",
                "encType": "vu",
                "type": "number"
            },
            {
                "amount": 10,
                "name": "scoreboardNames",
                "encType": "stringNT",
                "type": "string"
            },
            {
                "amount": 10,
                "name": "scoreboardScores",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 13,
                "amount": 10,
                "name": "scoreboardColors",
                "encType": "vu",
                "type": "Color"
            },
            {
                "amount": 10,
                "name": "scoreboardSuffixes",
                "encType": "stringNT",
                "type": "string"
            },
            {
                "amount": 10,
                "name": "scoreboardTanks",
                "encType": "vi",
                "type": "Tank | DevTank"
            },
            {
                "name": "leaderX",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "leaderY",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 1,
                "name": "playersNeeded",
                "encType": "vi",
                "type": "number"
            },
            {
                "default": 250,
                "name": "ticksUntilStart",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    [
        "Name",
        [
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "name",
                "encType": "stringNT",
                "type": "string"
            }
        ]
    ],
    [
        "Camera",
        [
            {
                "name": "unusedClientId",
                "encType": "vu",
                "type": "number"
            },
            {
                "default": 1,
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "player",
                "encType": "entid",
                "type": "Entity"
            },
            {
                "default": 0.35,
                "name": "FOV",
                "encType": "float",
                "type": "number"
            },
            {
                "default": 1,
                "name": "level",
                "encType": "vi",
                "type": "number"
            },
            {
                "default": 53,
                "name": "tank",
                "encType": "vi",
                "type": "Tank | DevTank"
            },
            {
                "name": "levelbarProgress",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "levelbarMax",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "statsAvailable",
                "encType": "vi",
                "type": "number"
            },
            {
                "amount": 8,
                "name": "statNames",
                "encType": "stringNT",
                "type": "string"
            },
            {
                "amount": 8,
                "name": "statLevels",
                "encType": "vi",
                "type": "number"
            },
            {
                "amount": 8,
                "name": "statLimits",
                "encType": "vi",
                "type": "number"
            },
            {
                "name": "cameraX",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "cameraY",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "score",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "respawnLevel",
                "encType": "vi",
                "type": "number"
            },
            {
                "name": "killedBy",
                "encType": "stringNT",
                "type": "string"
            },
            // {
            //     "name": "spectatorID",
            //     "encType": "stringNT"
            // },
            {
                "name": "spawnTick",
                "encType": "vi",
                "type": "number"
            },
            {
                "default": -1,
                "name": "deathTick",
                "encType": "vi",
                "type": "number"
            },
            {
                "name": "tankOverride",
                "encType": "stringNT",
                "type": "string"
            },
            {
                "name": "movementSpeed",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    [
        "Position",
        [
            {
                "name": "x",
                "encType": "vi",
                "type": "number"
            },
            {
                "name": "y",
                "encType": "vi",
                "type": "number"
            },
            {
                "name": "angle",
                "encType": "float64Precision",
                "type": "number"
            },
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            }
        ]
    ],
    [
        "Style",
        [
            {
                "default": 1,
                "name": "flags",
                "encType": "vu",
                "type": "number"
            },
            {
                "name": "color",
                "encType": "vu",
                "type": "Color"
            },
            {
                "default": 7.5,
                "name": "borderWidth",
                "encType": "float64Precision",
                "type": "number"
            },
            {
                "default": 1,
                "name": "opacity",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "zIndex",
                "encType": "vu",
                "type": "number"
            }
        ]
    ],
    ["!Tagged", []], // unused, Unsupported
    [
        "Score",
        [
            {
                "name": "score",
                "encType": "float",
                "type": "number"
            }
        ]
    ],
    [
        "Team",
        [
            {
                "name": "teamColor",
                "encType": "vu",
                "type": "Color"
            },
            {
                "name": "mothershipX",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "mothershipY",
                "encType": "float",
                "type": "number"
            },
            {
                "name": "flags",
                "encType": "vu",
                "type": "number"
            }
        ]
    ]
];

const EXISTANT_FIELD_GROUPS = FIELD_GROUPS.filter(e => !e[0].startsWith('!'));
const EXISTANT_FIELD_GROUP_NAMES = EXISTANT_FIELD_GROUPS.map(e => e[0]);



const OUTPUTS = [
    ["./FieldGroups.TEMPLATE", "./FieldGroups.ts"],
    ["./Entity.TEMPLATE", "./Entity.ts"],
    ["./UpcreateCompiler.TEMPLATE", "./UpcreateCompiler.ts"]
]

const evalMatch = /\^\<\<((\n|.)+?)\>\>\^/gm;

const fs = require('fs');
const interpolate = (template, context) => {
    return template.split('\n').filter(e=>!e.startsWith('#')).join('\n').replace(evalMatch, (_, statement) => {
        with (context) {
            return ' /* <template> auto-generated */ ' + eval(statement);
        }
    });
}
for (const [templateName, outFileName] of OUTPUTS) {
    const template = fs.readFileSync(templateName, 'utf-8');
    const outFile = interpolate(template, {
        EXISTANT_FIELD_GROUPS,
        EXISTANT_FIELD_GROUP_NAMES,
        FIELD_GROUPS,
        FIELD_ORDER
    });
    fs.writeFileSync(outFileName, outFile);
}
`

~src/Native/UpcreateCompiler.TEMPLATE:
`
// @ts-nocheck

/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Writer from "../Coder/Writer";
import { Color } from "../Const/Enums";
import { CameraEntity } from "./Camera";
import { Entity } from "./Entity";

/** Entity creation compiler function... Run! */
export const compileCreation = (camera: CameraEntity, w: Writer, entity: Entity) => {
    w.entid(entity).u8(1);

# for every field group, define if its set on the entity
^<<
FIELD_GROUPS.map((e,i) => e[0].startsWith('!') ? "" : (
`\n    const has${e[0]} = !!entity.${e[0].toLowerCase()}Data;`
)).join('')
>>^

    // Field group def
    let at = -1;
# if the entity has the field group, add the field group to the field group xor table
^<<
FIELD_GROUPS.map((e,i) => e[0].startsWith('!') ? "" : (
`\n    if (has${e[0]}) { w.u8((${i} - at) ^ 1); at = ${i}; };`
)).join('')
>>^
    w.u8(1); // close table

# now for every field, in the order of FIELD_ORDER, check if the group of that field is set on the entity, and if it is, write the field to the packet
^<<
FIELD_ORDER.map(([group, name], idx) => {
    const g = EXISTANT_FIELD_GROUPS.find(e => e[0] === group);
    if (!g) return '';
    const field = g[1].find(f => f.name === name);
    if (field.name === "color") {
        return `\n    if (has${group}) w.${field.encType}(entity.${group.toLowerCase()}Data.values.${name} === Color.Tank && !(entity.relationsData && entity.relationsData.values.team === (camera.cameraData.values.player && camera.cameraData.values.player.relationsData && camera.cameraData.values.player.relationsData.values.team)) ? Color.EnemyTank : entity.${group.toLowerCase()}Data.values.${name});`
    }
    if (!field.amount) return `\n    if (has${group}) w.${field.encType}(entity.${group.toLowerCase()}Data.values.${name});`
    else {
        return `\n    if (has${group}) for (at = 0; at < ${field.amount}; ++at) w.${field.encType}(entity.${group.toLowerCase()}Data.values.${name}.values[at]);`
    }
}).join('')
>>^
};

/** Entity update compiler function... Run! */
export const compileUpdate = (camera: Camera, w: Writer, entity: Entity) => {
    w.entid(entity).raw(0, 1);

# define variables that store if a field group exists
^<<
FIELD_GROUPS.map((e,i) => e[0].startsWith('!') ? "" : (
`\n    const has${e[0]} = !!entity.${e[0].toLowerCase()}Data;`
)).join('')
>>^

    let at = -1;
# for every field in the field order, check if the fieldgroup exists, if it does, check if the field has an update, and if it does, compile the field's value to the packet
^<<
FIELD_ORDER.map(([group, name], idx) => {
    const g = EXISTANT_FIELD_GROUPS.find(e => e[0] === group);
    if (!g) return '';
    const id = g[1].findIndex(f => f.name === name);
    const field = g[1][id];
    let encType = name === "scoreboardScores" ? "vi" : field.encType;
    if (field.name === "color") {
        return `\n    if (has${group}) if (entity.${group.toLowerCase()}Data.state[${id}]) { w.u8((${idx} - at) ^ 1); at = ${idx}; w.${encType}(entity.${group.toLowerCase()}Data.values.${name} === Color.Tank && !(entity.relationsData && entity.relationsData.values.team === (camera.cameraData.values.player && camera.cameraData.values.player.relationsData && camera.cameraData.values.player.relationsData.values.team)) ? Color.EnemyTank : entity.${group.toLowerCase()}Data.values.${name});};`
    }
    if (!field.amount) return `\n    if (has${group}) if (entity.${group.toLowerCase()}Data.state[${id}]) {w.u8((${idx} - at) ^ 1); at = ${idx}; w.${encType}(entity.${group.toLowerCase()}Data.values.${name});};`
    else {
        let updateFieldStr
        return `\n    if (has${group}) if (entity.${group.toLowerCase()}Data.state[${id}]) { w.u8((${idx} - at) ^ 1); at = -1; ${
            Array(field.amount).fill().map((_, i) => {
               return `if (entity.${group.toLowerCase()}Data.values.${name}.state[${i}]) { w.u8((${i} - at) ^ 1); w.${encType}(entity.${group.toLowerCase()}Data.values.${name}.values[${i}]); at = ${i}; } `
            }).join('')
        }w.u8(1); at = ${idx}; };`
    }
}).join('')
>>^

    w.u8(1); // close table
}
`

~src/Native/UpcreateCompiler.ts:
`
// @ts-nocheck

/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import Writer from "../Coder/Writer";
import { Color } from "../Const/Enums";
import { CameraEntity } from "./Camera";
import { Entity } from "./Entity";

/** Entity creation compiler function... Run! */
export const compileCreation = (camera: CameraEntity, w: Writer, entity: Entity) => {
    w.entid(entity).u8(1);

 /* <template> auto-generated */ 
    const hasRelations = !!entity.relationsData;
    const hasBarrel = !!entity.barrelData;
    const hasPhysics = !!entity.physicsData;
    const hasHealth = !!entity.healthData;
    const hasArena = !!entity.arenaData;
    const hasName = !!entity.nameData;
    const hasCamera = !!entity.cameraData;
    const hasPosition = !!entity.positionData;
    const hasStyle = !!entity.styleData;
    const hasScore = !!entity.scoreData;
    const hasTeam = !!entity.teamData;

    // Field group def
    let at = -1;
 /* <template> auto-generated */ 
    if (hasRelations) { w.u8((0 - at) ^ 1); at = 0; };
    if (hasBarrel) { w.u8((2 - at) ^ 1); at = 2; };
    if (hasPhysics) { w.u8((3 - at) ^ 1); at = 3; };
    if (hasHealth) { w.u8((4 - at) ^ 1); at = 4; };
    if (hasArena) { w.u8((7 - at) ^ 1); at = 7; };
    if (hasName) { w.u8((8 - at) ^ 1); at = 8; };
    if (hasCamera) { w.u8((9 - at) ^ 1); at = 9; };
    if (hasPosition) { w.u8((10 - at) ^ 1); at = 10; };
    if (hasStyle) { w.u8((11 - at) ^ 1); at = 11; };
    if (hasScore) { w.u8((13 - at) ^ 1); at = 13; };
    if (hasTeam) { w.u8((14 - at) ^ 1); at = 14; };
    w.u8(1); // close table

 /* <template> auto-generated */ 
    if (hasPosition) w.vi(entity.positionData.values.y);
    if (hasPosition) w.vi(entity.positionData.values.x);
    if (hasPosition) w.float64Precision(entity.positionData.values.angle);
    if (hasPhysics) w.float(entity.physicsData.values.size);
    if (hasCamera) w.entid(entity.cameraData.values.player);
    if (hasArena) w.vu(entity.arenaData.values.flags);
    if (hasStyle) w.vu(entity.styleData.values.color === Color.Tank && !(entity.relationsData && entity.relationsData.values.team === (camera.cameraData.values.player && camera.cameraData.values.player.relationsData && camera.cameraData.values.player.relationsData.values.team)) ? Color.EnemyTank : entity.styleData.values.color);
    if (hasArena) for (at = 0; at < 10; ++at) w.vu(entity.arenaData.values.scoreboardColors.values[at]);
    if (hasCamera) w.stringNT(entity.cameraData.values.killedBy);
    if (hasArena) w.vi(entity.arenaData.values.playersNeeded);
    if (hasPhysics) w.vu(entity.physicsData.values.sides);
    if (hasTeam) w.vu(entity.teamData.values.flags);
    if (hasHealth) w.vu(entity.healthData.values.flags);
    if (hasArena) for (at = 0; at < 10; ++at) w.vi(entity.arenaData.values.scoreboardTanks.values[at]);
    if (hasCamera) w.vi(entity.cameraData.values.respawnLevel);
    if (hasCamera) w.float(entity.cameraData.values.levelbarProgress);
    if (hasCamera) w.vi(entity.cameraData.values.spawnTick);
    if (hasPhysics) w.float(entity.physicsData.values.absorbtionFactor);
    if (hasArena) w.float(entity.arenaData.values.leaderX);
    if (hasHealth) w.float(entity.healthData.values.maxHealth);
    if (hasStyle) w.vu(entity.styleData.values.flags);
    if (hasBarrel) w.float(entity.barrelData.values.trapezoidDirection);
    if (hasPosition) w.vu(entity.positionData.values.flags);
    if (hasArena) for (at = 0; at < 10; ++at) w.stringNT(entity.arenaData.values.scoreboardNames.values[at]);
    if (hasCamera) w.float(entity.cameraData.values.score);
    if (hasTeam) w.float(entity.teamData.values.mothershipY);
    if (hasArena) for (at = 0; at < 10; ++at) w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[at]);
    if (hasName) w.vu(entity.nameData.values.flags);
    if (hasCamera) w.float(entity.cameraData.values.movementSpeed);
    if (hasArena) w.float(entity.arenaData.values.leaderY);
    if (hasArena) w.float(entity.arenaData.values.bottomY);
    if (hasRelations) w.entid(entity.relationsData.values.team);
    if (hasCamera) w.vi(entity.cameraData.values.level);
    if (hasTeam) w.vu(entity.teamData.values.teamColor);
    if (hasCamera) w.float(entity.cameraData.values.FOV);
    if (hasCamera) for (at = 0; at < 8; ++at) w.vi(entity.cameraData.values.statLimits.values[at]);
    if (hasArena) w.float(entity.arenaData.values.leftX);
    if (hasArena) for (at = 0; at < 10; ++at) w.float(entity.arenaData.values.scoreboardScores.values[at]);
    if (hasCamera) for (at = 0; at < 8; ++at) w.vi(entity.cameraData.values.statLevels.values[at]);
    if (hasCamera) w.stringNT(entity.cameraData.values.tankOverride);
    if (hasCamera) w.vi(entity.cameraData.values.tank);
    if (hasStyle) w.float64Precision(entity.styleData.values.borderWidth);
    if (hasCamera) w.vi(entity.cameraData.values.deathTick);
    if (hasPhysics) w.float(entity.physicsData.values.width);
    if (hasCamera) w.vi(entity.cameraData.values.statsAvailable);
    if (hasBarrel) w.vu(entity.barrelData.values.flags);
    if (hasCamera) w.float(entity.cameraData.values.levelbarMax);
    if (hasName) w.stringNT(entity.nameData.values.name);
    if (hasRelations) w.entid(entity.relationsData.values.owner);
    if (hasHealth) w.float(entity.healthData.values.health);
    if (hasCamera) w.float(entity.cameraData.values.cameraY);
    if (hasStyle) w.float(entity.styleData.values.opacity);
    if (hasBarrel) w.float(entity.barrelData.values.reloadTime);
    if (hasCamera) for (at = 0; at < 8; ++at) w.stringNT(entity.cameraData.values.statNames.values[at]);
    if (hasCamera) w.float(entity.cameraData.values.cameraX);
    if (hasTeam) w.float(entity.teamData.values.mothershipX);
    if (hasCamera) w.vu(entity.cameraData.values.unusedClientId);
    if (hasRelations) w.entid(entity.relationsData.values.parent);
    if (hasStyle) w.vu(entity.styleData.values.zIndex);
    if (hasCamera) w.vu(entity.cameraData.values.flags);
    if (hasArena) w.float(entity.arenaData.values.rightX);
    if (hasPhysics) w.float(entity.physicsData.values.pushFactor);
    if (hasPhysics) w.vu(entity.physicsData.values.flags);
    if (hasArena) w.vu(entity.arenaData.values.scoreboardAmount);
    if (hasArena) w.float(entity.arenaData.values.ticksUntilStart);
    if (hasArena) w.float(entity.arenaData.values.topY);
    if (hasScore) w.float(entity.scoreData.values.score);
};

/** Entity update compiler function... Run! */
export const compileUpdate = (camera: Camera, w: Writer, entity: Entity) => {
    w.entid(entity).raw(0, 1);

 /* <template> auto-generated */ 
    const hasRelations = !!entity.relationsData;
    const hasBarrel = !!entity.barrelData;
    const hasPhysics = !!entity.physicsData;
    const hasHealth = !!entity.healthData;
    const hasArena = !!entity.arenaData;
    const hasName = !!entity.nameData;
    const hasCamera = !!entity.cameraData;
    const hasPosition = !!entity.positionData;
    const hasStyle = !!entity.styleData;
    const hasScore = !!entity.scoreData;
    const hasTeam = !!entity.teamData;

    let at = -1;
 /* <template> auto-generated */ 
    if (hasPosition) if (entity.positionData.state[1]) {w.u8((0 - at) ^ 1); at = 0; w.vi(entity.positionData.values.y);};
    if (hasPosition) if (entity.positionData.state[0]) {w.u8((1 - at) ^ 1); at = 1; w.vi(entity.positionData.values.x);};
    if (hasPosition) if (entity.positionData.state[2]) {w.u8((2 - at) ^ 1); at = 2; w.float64Precision(entity.positionData.values.angle);};
    if (hasPhysics) if (entity.physicsData.state[2]) {w.u8((3 - at) ^ 1); at = 3; w.float(entity.physicsData.values.size);};
    if (hasCamera) if (entity.cameraData.state[2]) {w.u8((4 - at) ^ 1); at = 4; w.entid(entity.cameraData.values.player);};
    if (hasArena) if (entity.arenaData.state[0]) {w.u8((5 - at) ^ 1); at = 5; w.vu(entity.arenaData.values.flags);};
    if (hasStyle) if (entity.styleData.state[1]) { w.u8((6 - at) ^ 1); at = 6; w.vu(entity.styleData.values.color === Color.Tank && !(entity.relationsData && entity.relationsData.values.team === (camera.cameraData.values.player && camera.cameraData.values.player.relationsData && camera.cameraData.values.player.relationsData.values.team)) ? Color.EnemyTank : entity.styleData.values.color);};
    if (hasArena) if (entity.arenaData.state[8]) { w.u8((7 - at) ^ 1); at = -1; if (entity.arenaData.values.scoreboardColors.state[0]) { w.u8((0 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[0]); at = 0; } if (entity.arenaData.values.scoreboardColors.state[1]) { w.u8((1 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[1]); at = 1; } if (entity.arenaData.values.scoreboardColors.state[2]) { w.u8((2 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[2]); at = 2; } if (entity.arenaData.values.scoreboardColors.state[3]) { w.u8((3 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[3]); at = 3; } if (entity.arenaData.values.scoreboardColors.state[4]) { w.u8((4 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[4]); at = 4; } if (entity.arenaData.values.scoreboardColors.state[5]) { w.u8((5 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[5]); at = 5; } if (entity.arenaData.values.scoreboardColors.state[6]) { w.u8((6 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[6]); at = 6; } if (entity.arenaData.values.scoreboardColors.state[7]) { w.u8((7 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[7]); at = 7; } if (entity.arenaData.values.scoreboardColors.state[8]) { w.u8((8 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[8]); at = 8; } if (entity.arenaData.values.scoreboardColors.state[9]) { w.u8((9 - at) ^ 1); w.vu(entity.arenaData.values.scoreboardColors.values[9]); at = 9; } w.u8(1); at = 7; };
    if (hasCamera) if (entity.cameraData.state[16]) {w.u8((8 - at) ^ 1); at = 8; w.stringNT(entity.cameraData.values.killedBy);};
    if (hasArena) if (entity.arenaData.state[13]) {w.u8((9 - at) ^ 1); at = 9; w.vi(entity.arenaData.values.playersNeeded);};
    if (hasPhysics) if (entity.physicsData.state[1]) {w.u8((10 - at) ^ 1); at = 10; w.vu(entity.physicsData.values.sides);};
    if (hasTeam) if (entity.teamData.state[3]) {w.u8((11 - at) ^ 1); at = 11; w.vu(entity.teamData.values.flags);};
    if (hasHealth) if (entity.healthData.state[0]) {w.u8((12 - at) ^ 1); at = 12; w.vu(entity.healthData.values.flags);};
    if (hasArena) if (entity.arenaData.state[10]) { w.u8((13 - at) ^ 1); at = -1; if (entity.arenaData.values.scoreboardTanks.state[0]) { w.u8((0 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[0]); at = 0; } if (entity.arenaData.values.scoreboardTanks.state[1]) { w.u8((1 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[1]); at = 1; } if (entity.arenaData.values.scoreboardTanks.state[2]) { w.u8((2 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[2]); at = 2; } if (entity.arenaData.values.scoreboardTanks.state[3]) { w.u8((3 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[3]); at = 3; } if (entity.arenaData.values.scoreboardTanks.state[4]) { w.u8((4 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[4]); at = 4; } if (entity.arenaData.values.scoreboardTanks.state[5]) { w.u8((5 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[5]); at = 5; } if (entity.arenaData.values.scoreboardTanks.state[6]) { w.u8((6 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[6]); at = 6; } if (entity.arenaData.values.scoreboardTanks.state[7]) { w.u8((7 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[7]); at = 7; } if (entity.arenaData.values.scoreboardTanks.state[8]) { w.u8((8 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[8]); at = 8; } if (entity.arenaData.values.scoreboardTanks.state[9]) { w.u8((9 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardTanks.values[9]); at = 9; } w.u8(1); at = 13; };
    if (hasCamera) if (entity.cameraData.state[15]) {w.u8((14 - at) ^ 1); at = 14; w.vi(entity.cameraData.values.respawnLevel);};
    if (hasCamera) if (entity.cameraData.state[6]) {w.u8((15 - at) ^ 1); at = 15; w.float(entity.cameraData.values.levelbarProgress);};
    if (hasCamera) if (entity.cameraData.state[17]) {w.u8((16 - at) ^ 1); at = 16; w.vi(entity.cameraData.values.spawnTick);};
    if (hasPhysics) if (entity.physicsData.state[4]) {w.u8((17 - at) ^ 1); at = 17; w.float(entity.physicsData.values.absorbtionFactor);};
    if (hasArena) if (entity.arenaData.state[11]) {w.u8((18 - at) ^ 1); at = 18; w.float(entity.arenaData.values.leaderX);};
    if (hasHealth) if (entity.healthData.state[2]) {w.u8((19 - at) ^ 1); at = 19; w.float(entity.healthData.values.maxHealth);};
    if (hasStyle) if (entity.styleData.state[0]) {w.u8((20 - at) ^ 1); at = 20; w.vu(entity.styleData.values.flags);};
    if (hasBarrel) if (entity.barrelData.state[2]) {w.u8((22 - at) ^ 1); at = 22; w.float(entity.barrelData.values.trapezoidDirection);};
    if (hasPosition) if (entity.positionData.state[3]) {w.u8((23 - at) ^ 1); at = 23; w.vu(entity.positionData.values.flags);};
    if (hasArena) if (entity.arenaData.state[6]) { w.u8((24 - at) ^ 1); at = -1; if (entity.arenaData.values.scoreboardNames.state[0]) { w.u8((0 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[0]); at = 0; } if (entity.arenaData.values.scoreboardNames.state[1]) { w.u8((1 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[1]); at = 1; } if (entity.arenaData.values.scoreboardNames.state[2]) { w.u8((2 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[2]); at = 2; } if (entity.arenaData.values.scoreboardNames.state[3]) { w.u8((3 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[3]); at = 3; } if (entity.arenaData.values.scoreboardNames.state[4]) { w.u8((4 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[4]); at = 4; } if (entity.arenaData.values.scoreboardNames.state[5]) { w.u8((5 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[5]); at = 5; } if (entity.arenaData.values.scoreboardNames.state[6]) { w.u8((6 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[6]); at = 6; } if (entity.arenaData.values.scoreboardNames.state[7]) { w.u8((7 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[7]); at = 7; } if (entity.arenaData.values.scoreboardNames.state[8]) { w.u8((8 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[8]); at = 8; } if (entity.arenaData.values.scoreboardNames.state[9]) { w.u8((9 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardNames.values[9]); at = 9; } w.u8(1); at = 24; };
    if (hasCamera) if (entity.cameraData.state[14]) {w.u8((25 - at) ^ 1); at = 25; w.float(entity.cameraData.values.score);};
    if (hasTeam) if (entity.teamData.state[2]) {w.u8((26 - at) ^ 1); at = 26; w.float(entity.teamData.values.mothershipY);};
    if (hasArena) if (entity.arenaData.state[9]) { w.u8((27 - at) ^ 1); at = -1; if (entity.arenaData.values.scoreboardSuffixes.state[0]) { w.u8((0 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[0]); at = 0; } if (entity.arenaData.values.scoreboardSuffixes.state[1]) { w.u8((1 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[1]); at = 1; } if (entity.arenaData.values.scoreboardSuffixes.state[2]) { w.u8((2 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[2]); at = 2; } if (entity.arenaData.values.scoreboardSuffixes.state[3]) { w.u8((3 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[3]); at = 3; } if (entity.arenaData.values.scoreboardSuffixes.state[4]) { w.u8((4 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[4]); at = 4; } if (entity.arenaData.values.scoreboardSuffixes.state[5]) { w.u8((5 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[5]); at = 5; } if (entity.arenaData.values.scoreboardSuffixes.state[6]) { w.u8((6 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[6]); at = 6; } if (entity.arenaData.values.scoreboardSuffixes.state[7]) { w.u8((7 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[7]); at = 7; } if (entity.arenaData.values.scoreboardSuffixes.state[8]) { w.u8((8 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[8]); at = 8; } if (entity.arenaData.values.scoreboardSuffixes.state[9]) { w.u8((9 - at) ^ 1); w.stringNT(entity.arenaData.values.scoreboardSuffixes.values[9]); at = 9; } w.u8(1); at = 27; };
    if (hasName) if (entity.nameData.state[0]) {w.u8((28 - at) ^ 1); at = 28; w.vu(entity.nameData.values.flags);};
    if (hasCamera) if (entity.cameraData.state[20]) {w.u8((29 - at) ^ 1); at = 29; w.float(entity.cameraData.values.movementSpeed);};
    if (hasArena) if (entity.arenaData.state[12]) {w.u8((30 - at) ^ 1); at = 30; w.float(entity.arenaData.values.leaderY);};
    if (hasArena) if (entity.arenaData.state[4]) {w.u8((31 - at) ^ 1); at = 31; w.float(entity.arenaData.values.bottomY);};
    if (hasRelations) if (entity.relationsData.state[2]) {w.u8((32 - at) ^ 1); at = 32; w.entid(entity.relationsData.values.team);};
    if (hasCamera) if (entity.cameraData.state[4]) {w.u8((33 - at) ^ 1); at = 33; w.vi(entity.cameraData.values.level);};
    if (hasTeam) if (entity.teamData.state[0]) {w.u8((34 - at) ^ 1); at = 34; w.vu(entity.teamData.values.teamColor);};
    if (hasCamera) if (entity.cameraData.state[3]) {w.u8((35 - at) ^ 1); at = 35; w.float(entity.cameraData.values.FOV);};
    if (hasCamera) if (entity.cameraData.state[11]) { w.u8((36 - at) ^ 1); at = -1; if (entity.cameraData.values.statLimits.state[0]) { w.u8((0 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[0]); at = 0; } if (entity.cameraData.values.statLimits.state[1]) { w.u8((1 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[1]); at = 1; } if (entity.cameraData.values.statLimits.state[2]) { w.u8((2 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[2]); at = 2; } if (entity.cameraData.values.statLimits.state[3]) { w.u8((3 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[3]); at = 3; } if (entity.cameraData.values.statLimits.state[4]) { w.u8((4 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[4]); at = 4; } if (entity.cameraData.values.statLimits.state[5]) { w.u8((5 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[5]); at = 5; } if (entity.cameraData.values.statLimits.state[6]) { w.u8((6 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[6]); at = 6; } if (entity.cameraData.values.statLimits.state[7]) { w.u8((7 - at) ^ 1); w.vi(entity.cameraData.values.statLimits.values[7]); at = 7; } w.u8(1); at = 36; };
    if (hasArena) if (entity.arenaData.state[1]) {w.u8((37 - at) ^ 1); at = 37; w.float(entity.arenaData.values.leftX);};
    if (hasArena) if (entity.arenaData.state[7]) { w.u8((38 - at) ^ 1); at = -1; if (entity.arenaData.values.scoreboardScores.state[0]) { w.u8((0 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[0]); at = 0; } if (entity.arenaData.values.scoreboardScores.state[1]) { w.u8((1 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[1]); at = 1; } if (entity.arenaData.values.scoreboardScores.state[2]) { w.u8((2 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[2]); at = 2; } if (entity.arenaData.values.scoreboardScores.state[3]) { w.u8((3 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[3]); at = 3; } if (entity.arenaData.values.scoreboardScores.state[4]) { w.u8((4 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[4]); at = 4; } if (entity.arenaData.values.scoreboardScores.state[5]) { w.u8((5 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[5]); at = 5; } if (entity.arenaData.values.scoreboardScores.state[6]) { w.u8((6 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[6]); at = 6; } if (entity.arenaData.values.scoreboardScores.state[7]) { w.u8((7 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[7]); at = 7; } if (entity.arenaData.values.scoreboardScores.state[8]) { w.u8((8 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[8]); at = 8; } if (entity.arenaData.values.scoreboardScores.state[9]) { w.u8((9 - at) ^ 1); w.vi(entity.arenaData.values.scoreboardScores.values[9]); at = 9; } w.u8(1); at = 38; };
    if (hasCamera) if (entity.cameraData.state[10]) { w.u8((39 - at) ^ 1); at = -1; if (entity.cameraData.values.statLevels.state[0]) { w.u8((0 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[0]); at = 0; } if (entity.cameraData.values.statLevels.state[1]) { w.u8((1 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[1]); at = 1; } if (entity.cameraData.values.statLevels.state[2]) { w.u8((2 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[2]); at = 2; } if (entity.cameraData.values.statLevels.state[3]) { w.u8((3 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[3]); at = 3; } if (entity.cameraData.values.statLevels.state[4]) { w.u8((4 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[4]); at = 4; } if (entity.cameraData.values.statLevels.state[5]) { w.u8((5 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[5]); at = 5; } if (entity.cameraData.values.statLevels.state[6]) { w.u8((6 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[6]); at = 6; } if (entity.cameraData.values.statLevels.state[7]) { w.u8((7 - at) ^ 1); w.vi(entity.cameraData.values.statLevels.values[7]); at = 7; } w.u8(1); at = 39; };
    if (hasCamera) if (entity.cameraData.state[19]) {w.u8((40 - at) ^ 1); at = 40; w.stringNT(entity.cameraData.values.tankOverride);};
    if (hasCamera) if (entity.cameraData.state[5]) {w.u8((41 - at) ^ 1); at = 41; w.vi(entity.cameraData.values.tank);};
    if (hasStyle) if (entity.styleData.state[2]) {w.u8((42 - at) ^ 1); at = 42; w.float64Precision(entity.styleData.values.borderWidth);};
    if (hasCamera) if (entity.cameraData.state[18]) {w.u8((43 - at) ^ 1); at = 43; w.vi(entity.cameraData.values.deathTick);};
    if (hasPhysics) if (entity.physicsData.state[3]) {w.u8((44 - at) ^ 1); at = 44; w.float(entity.physicsData.values.width);};
    if (hasCamera) if (entity.cameraData.state[8]) {w.u8((45 - at) ^ 1); at = 45; w.vi(entity.cameraData.values.statsAvailable);};
    if (hasBarrel) if (entity.barrelData.state[0]) {w.u8((46 - at) ^ 1); at = 46; w.vu(entity.barrelData.values.flags);};
    if (hasCamera) if (entity.cameraData.state[7]) {w.u8((47 - at) ^ 1); at = 47; w.float(entity.cameraData.values.levelbarMax);};
    if (hasName) if (entity.nameData.state[1]) {w.u8((48 - at) ^ 1); at = 48; w.stringNT(entity.nameData.values.name);};
    if (hasRelations) if (entity.relationsData.state[1]) {w.u8((49 - at) ^ 1); at = 49; w.entid(entity.relationsData.values.owner);};
    if (hasHealth) if (entity.healthData.state[1]) {w.u8((50 - at) ^ 1); at = 50; w.float(entity.healthData.values.health);};
    if (hasCamera) if (entity.cameraData.state[13]) {w.u8((51 - at) ^ 1); at = 51; w.float(entity.cameraData.values.cameraY);};
    if (hasStyle) if (entity.styleData.state[3]) {w.u8((52 - at) ^ 1); at = 52; w.float(entity.styleData.values.opacity);};
    if (hasBarrel) if (entity.barrelData.state[1]) {w.u8((53 - at) ^ 1); at = 53; w.float(entity.barrelData.values.reloadTime);};
    if (hasCamera) if (entity.cameraData.state[9]) { w.u8((54 - at) ^ 1); at = -1; if (entity.cameraData.values.statNames.state[0]) { w.u8((0 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[0]); at = 0; } if (entity.cameraData.values.statNames.state[1]) { w.u8((1 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[1]); at = 1; } if (entity.cameraData.values.statNames.state[2]) { w.u8((2 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[2]); at = 2; } if (entity.cameraData.values.statNames.state[3]) { w.u8((3 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[3]); at = 3; } if (entity.cameraData.values.statNames.state[4]) { w.u8((4 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[4]); at = 4; } if (entity.cameraData.values.statNames.state[5]) { w.u8((5 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[5]); at = 5; } if (entity.cameraData.values.statNames.state[6]) { w.u8((6 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[6]); at = 6; } if (entity.cameraData.values.statNames.state[7]) { w.u8((7 - at) ^ 1); w.stringNT(entity.cameraData.values.statNames.values[7]); at = 7; } w.u8(1); at = 54; };
    if (hasCamera) if (entity.cameraData.state[12]) {w.u8((55 - at) ^ 1); at = 55; w.float(entity.cameraData.values.cameraX);};
    if (hasTeam) if (entity.teamData.state[1]) {w.u8((56 - at) ^ 1); at = 56; w.float(entity.teamData.values.mothershipX);};
    if (hasCamera) if (entity.cameraData.state[0]) {w.u8((57 - at) ^ 1); at = 57; w.vu(entity.cameraData.values.unusedClientId);};
    if (hasRelations) if (entity.relationsData.state[0]) {w.u8((58 - at) ^ 1); at = 58; w.entid(entity.relationsData.values.parent);};
    if (hasStyle) if (entity.styleData.state[4]) {w.u8((59 - at) ^ 1); at = 59; w.vu(entity.styleData.values.zIndex);};
    if (hasCamera) if (entity.cameraData.state[1]) {w.u8((60 - at) ^ 1); at = 60; w.vu(entity.cameraData.values.flags);};
    if (hasArena) if (entity.arenaData.state[3]) {w.u8((61 - at) ^ 1); at = 61; w.float(entity.arenaData.values.rightX);};
    if (hasPhysics) if (entity.physicsData.state[5]) {w.u8((62 - at) ^ 1); at = 62; w.float(entity.physicsData.values.pushFactor);};
    if (hasPhysics) if (entity.physicsData.state[0]) {w.u8((63 - at) ^ 1); at = 63; w.vu(entity.physicsData.values.flags);};
    if (hasArena) if (entity.arenaData.state[5]) {w.u8((64 - at) ^ 1); at = 64; w.vu(entity.arenaData.values.scoreboardAmount);};
    if (hasArena) if (entity.arenaData.state[14]) {w.u8((65 - at) ^ 1); at = 65; w.float(entity.arenaData.values.ticksUntilStart);};
    if (hasArena) if (entity.arenaData.state[2]) {w.u8((66 - at) ^ 1); at = 66; w.float(entity.arenaData.values.topY);};
    if (hasScore) if (entity.scoreData.state[0]) {w.u8((67 - at) ^ 1); at = 67; w.float(entity.scoreData.values.score);};

    w.u8(1); // close table
}
`

~src/Physics/CollisionManager.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)
 
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.
 
    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import ObjectEntity from "../Entity/Object";

export default interface CollisionManager {
    reset(bottomY: number, rightX: number): void;
    insertEntity(entity: ObjectEntity): void;
    retrieve(x: number, y: number, radiW: number, radiH: number): ObjectEntity[];
    retrieveEntitiesByEntity(entity: ObjectEntity): ObjectEntity[];
}
`

~src/Physics/QuadTree.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/**
 * UNDOCUMENTED FILE
 **/

import ObjectEntity from "../Entity/Object";
import CollisionManager from "./CollisionManager";

interface Range<T> {
    x: number;
    y: number;
    radiW: number;
    radiH: number;
    content: T;
}

class QuadTreeNode<T> {
    protected x = 0;
    protected y = 0;
    protected radiW = 0;
    protected radiH = 0;
    protected level = 0;

    protected objects: Range<T>[] = [];

    protected topLeft: QuadTreeNode<T> | null = null;
    protected topRight: QuadTreeNode<T> | null = null;
    protected bottomLeft: QuadTreeNode<T> | null = null;
    protected bottomRight: QuadTreeNode<T> | null = null;

    protected constructor(x: number, y: number, radiW: number, radiH: number, level: number) {
        this.x = x;
        this.y = y;
        this.radiW = radiW;
        this.radiH = radiH;
        this.level = level;
    }

    protected _insert(object: Range<T>) {
        // {entity, x, y, radiW, radiH}
        if (this.topLeft) {
            const top = object.y - object.radiH < this.y,
                bottom = object.y + object.radiH > this.y,
                left = object.x - object.radiW < this.x,
                right = object.x + object.radiW > this.x;

            if (top && left) this.topLeft._insert(object);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(object);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(object);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(object);

            return;
        }

        this.objects[this.objects.length] = object;

        if (this.objects.length === 5 && this.level <= 9) {
            const halfW = this.radiW / 2,
                halfH = this.radiH / 2,
                level = this.level + 1;
            this.topLeft = new QuadTreeNode(this.x - halfW, this.y - halfH, halfW, halfH, level);
            this.topRight = new QuadTreeNode(this.x + halfW, this.y - halfH, halfW, halfH, level);
            this.bottomLeft = new QuadTreeNode(this.x - halfW, this.y + halfH, halfW, halfH, level);
            this.bottomRight = new QuadTreeNode(this.x + halfW, this.y + halfH, halfW, halfH, level);

            var top, bottom, left, right;
            
            top = this.objects[0].y - this.objects[0].radiH < this.y;
            bottom = this.objects[0].y + this.objects[0].radiH > this.y;
            left = this.objects[0].x - this.objects[0].radiW < this.x;
            right = this.objects[0].x + this.objects[0].radiW > this.x;
            if (top && left) this.topLeft._insert(this.objects[0]);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(this.objects[0]);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(this.objects[0]);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(this.objects[0]);

            top = this.objects[1].y - this.objects[1].radiH < this.y;
            bottom = this.objects[1].y + this.objects[1].radiH > this.y;
            left = this.objects[1].x - this.objects[1].radiW < this.x;
            right = this.objects[1].x + this.objects[1].radiW > this.x;
            if (top && left) this.topLeft._insert(this.objects[1]);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(this.objects[1]);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(this.objects[1]);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(this.objects[1]);

            top = this.objects[2].y - this.objects[2].radiH < this.y;
            bottom = this.objects[2].y + this.objects[2].radiH > this.y;
            left = this.objects[2].x - this.objects[2].radiW < this.x;
            right = this.objects[2].x + this.objects[2].radiW > this.x;
            if (top && left) this.topLeft._insert(this.objects[2]);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(this.objects[2]);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(this.objects[2]);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(this.objects[2]);

            top = this.objects[3].y - this.objects[3].radiH < this.y;
            bottom = this.objects[3].y + this.objects[3].radiH > this.y;
            left = this.objects[3].x - this.objects[3].radiW < this.x;
            right = this.objects[3].x + this.objects[3].radiW > this.x;
            if (top && left) this.topLeft._insert(this.objects[3]);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(this.objects[3]);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(this.objects[3]);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(this.objects[3]);

            top = this.objects[4].y - this.objects[4].radiH < this.y;
            bottom = this.objects[4].y + this.objects[4].radiH > this.y;
            left = this.objects[4].x - this.objects[4].radiW < this.x;
            right = this.objects[4].x + this.objects[4].radiW > this.x;
            if (top && left) this.topLeft._insert(this.objects[4]);
            /** @ts-ignore */
            if (top && right) this.topRight._insert(this.objects[4]);
            /** @ts-ignore */
            if (bottom && left) this.bottomLeft._insert(this.objects[4]);
            /** @ts-ignore */
            if (bottom && right) this.bottomRight._insert(this.objects[4]);
        }
    }

    protected _retrieve(x: number, y: number, radiW: number, radiH: number): Range<T>[] {
        if (this.topLeft) {
            let out: Range<T>[] = [];
            const top = y - radiH < this.y,
                bottom = y + radiH > this.y,
                left = x - radiW < this.x,
                right = x + radiW > this.x;

            if (top && left) out = out.concat(this.topLeft._retrieve(x, y, radiW, radiH));
            /** @ts-ignore */
            if (top && right) out = out.concat(this.topRight._retrieve(x, y, radiW, radiH));
            /** @ts-ignore */
            if (bottom && left) out = out.concat(this.bottomLeft._retrieve(x, y, radiW, radiH));
            /** @ts-ignore */
            if (bottom && right) out = out.concat(this.bottomRight._retrieve(x, y, radiW, radiH));
            
            return out;
        } else return this.objects;
    }
}

export default class DiepQuadTree extends QuadTreeNode<ObjectEntity> implements CollisionManager {
    public constructor(radiW: number, radiH: number) {
        super(0, 0, radiW, radiH, 0);
    }
    public insertEntity(entity: ObjectEntity) {
        this._insert({
            content: entity,
            x: entity.positionData.values.x,
            y: entity.positionData.values.y,
            radiW: entity.physicsData.values.sides === 2 ? entity.physicsData.values.size / 2 : entity.physicsData.values.size,
            radiH: entity.physicsData.values.sides === 2 ? entity.physicsData.values.width / 2 : entity.physicsData.values.size,
        });
    }

    public retrieve(x: number, y: number, radiW: number, radiH: number): ObjectEntity[] {
        const ranges = this._retrieve(x, y, radiW, radiH);

        const entities: ObjectEntity[] = [];

        for (let i = 0; i < ranges.length; ++i) {
            if (ranges[i].content.hash !== 0 && !entities.includes(ranges[i].content)) entities.push(ranges[i].content);
        }

        return entities;
    }

    public retrieveEntitiesByEntity(entity: ObjectEntity): ObjectEntity[] {
        return this.retrieve(entity.positionData.values.x,
            entity.positionData.values.y,
            entity.physicsData.values.sides === 2 ? entity.physicsData.values.size / 2 : entity.physicsData.values.size,
            entity.physicsData.values.sides === 2 ? entity.physicsData.values.width / 2 : entity.physicsData.values.size);
    }

    public reset(bottomY: number, rightX: number) {
        this.bottomRight = this.bottomLeft = this.topLeft = this.topRight = null;
        this.radiW = rightX;
        this.objects = [];
        this.radiH = bottomY;
    }
}
`

~src/Physics/SpatialHashing.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import ObjectEntity from "../Entity/Object";
import CollisionManager from "./CollisionManager";

// TODO(speed): speed up spatial hashing somehow someway

export default class SpatialHashing implements CollisionManager {
    public cellSize: number;
    public hashMap = new Map<number, ObjectEntity[]>();
    public queryId = 0;

    constructor(cellSize: number) {
        this.cellSize = cellSize;
    }

    public insertEntity(entity: ObjectEntity) {
        const { sides, size, width } = entity.physicsData.values;
        const { x, y } = entity.positionData.values;
        const isLine = sides === 2;
        const radiW = isLine ? size / 2 : size;
        const radiH = isLine ? width / 2 : size;
        
        const topX = (x - radiW) >> this.cellSize;
        const topY = (y - radiH) >> this.cellSize;
        const bottomX = (x + radiW) >> this.cellSize;
        const bottomY = (y + radiH) >> this.cellSize;

        // Iterating over the y axis first is more cache friendly.
        for(let y = topY; y <= bottomY; ++y) {
            for(let x = topX; x <= bottomX; ++x) {
                const key: number = x | (y << 10);
                if(!this.hashMap.has(key)) {
                    this.hashMap.set(key, []);
                }
                /** @ts-ignore the key is guaranteed to be set */
                this.hashMap.get(key).push(entity);
            }
        }
    }

    public retrieve(x: number, y: number, width: number, height: number): ObjectEntity[] {
        const result: ObjectEntity[] = [];

        const startX = (x - width) >> this.cellSize;
        const startY = (y - height) >> this.cellSize;
        const endX = ((x + width) >> this.cellSize);
        const endY = ((y + height) >> this.cellSize);

        for (let y = startY; y <= endY; ++y) {
            for (let x = startX; x <= endX; ++x) {
                const key = x | (y << 10);
                const cell = this.hashMap.get(key);
                if (cell == null) continue;
                for (let i = 0; i < cell.length; ++i) {
                    if (cell[i]['_queryId'] != this.queryId) {
                        cell[i]['_queryId'] = this.queryId;
                        if (cell[i].hash !== 0) result.push(cell[i]);
                    }
                }
            }
        }

        // Force uint32 to prevent the queryId from going too high for javascript to handle.
        this.queryId = (this.queryId + 1) >>> 0;

        return result;
    }

    public retrieveEntitiesByEntity(entity: ObjectEntity): ObjectEntity[] {
        const { sides, size, width } = entity.physicsData.values;
        const { x, y } = entity.positionData;
        const isLine = sides === 2;
        const w = isLine ? size / 2 : size;
        const h = isLine ? width / 2 : size;
        return this.retrieve(x, y, w, h);
    }

    public reset() {
        this.hashMap = new Map();
    }
}
`

~src/Physics/Vector.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

export interface VectorAbstract {
    x: number;
    y: number;
};

export default class Vector implements VectorAbstract {
    public x: number;
    public y: number;

    public constructor(x: number = 0, y: number = 0) {
        this.x = x;
        this.y = y;
    }

    /** Returns true if the `Vector` only contains real numbers in the `x` and `y` property. */
    public static isFinite(vector: Vector | VectorAbstract) {
        return Number.isFinite(vector.x) && Number.isFinite(vector.y);
    }

    /** Converts coordinates from the polar coordinate system to the cartesian coordinate system */
    public static fromPolar(theta: number, distance: number) {
        // TODO(speed):
        // This might be overused
        return new Vector(
            distance * Math.cos(theta),
            distance * Math.sin(theta)
        );
    }

    /** Replaces this vector's properties with another vector */
    public set(vector: VectorAbstract) {
        this.x = vector.x;
        this.y = vector.y;
    }

    public add(vector: VectorAbstract) {
        this.x += vector.x,
        this.y += vector.y;
    }

    public subtract(vector: VectorAbstract) {
        this.x -= vector.x;
        this.y -= vector.y;
    }

    public distanceToSQ(vector: VectorAbstract) {
        return (vector.x - this.x) ** 2 + (vector.y - this.y) ** 2;
    } 

    public angleComponent(angle: number) {
        // if angle is 0, it will get the x component
        // if angle is pi/2, it will get the y component
        return Math.cos(this.angle - angle) * this.magnitude;
    }
    
    public set angle(angle: number) {
        const currentMag = this.magnitude;

        this.set({
            x: Math.cos(angle) * currentMag, 
            y: Math.sin(angle) * currentMag
        });
    }

    public set magnitude(magnitude: number) {
        const currentDir = this.angle;

        this.set({
            x: Math.cos(currentDir) * magnitude,
            y: Math.sin(currentDir) * magnitude
        });
    }

    public get magnitude() {
       return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
    }

    public get angle() {
        // TODO(speed):
        // This also might be overused
        return Math.atan2(this.y, this.x);
    }
}
`

~src/Physics/Velocity.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/**
 * UNDOCUMENTED FILE
 **/
import Vector, { VectorAbstract } from "./Vector";

export default class Velocity extends Vector {
    private previousPosition = new Vector();
    public position = new Vector();
    private firstTime = true;

    public updateVelocity() {
        this.x = this.position.x - this.previousPosition.x;
        this.y = this.position.y - this.previousPosition.y;
    }

    public setPosition(newPosition: VectorAbstract) {
        this.previousPosition.set(this.position);
        this.position.set(newPosition);
        if (this.firstTime) {
            this.previousPosition.set(newPosition);
            this.firstTime = false;
        }
        this.updateVelocity();
    }
}
`

~src/Client.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as config from "./config";
import * as util from "./util";
import { createHash } from "crypto";
import { WebSocket } from "uWebSockets.js";
import Reader from "./Coder/Reader";
import Writer from "./Coder/Writer";
import GameServer from "./Game";
import ClientCamera from "./Native/Camera";
import { ArenaState } from "./Native/Arena";
import ObjectEntity from "./Entity/Object";
import TankDefinitions, { getTankById, TankCount } from "./Const/TankDefinitions";
import DevTankDefinitions, { DevTank } from "./Const/DevTankDefinitions";
import TankBody from "./Entity/Tank/TankBody";
import Vector, { VectorAbstract } from "./Physics/Vector";
import { Entity, EntityStateFlags } from "./Native/Entity";
import { CameraFlags, ClientBound, ArenaFlags, InputFlags, NameFlags, ServerBound, Stat, StatCount, Tank } from "./Const/Enums";
import { AI, AIState, Inputs } from "./Entity/AI";
import AbstractBoss from "./Entity/Boss/AbstractBoss";
import { executeCommand } from "./Const/Commands";
import { bannedClients } from ".";

/** XORed onto the tank id in the Tank Upgrade packet. */
const TANK_XOR = config.magicNum % TankCount;
/** XORed onto the stat id in the Stat Upgrade packet. */
const STAT_XOR = config.magicNum % StatCount;
/** Cached ping packet */
const PING_PACKET = new Uint8Array([ClientBound.Ping]);

/** Used to write data in Writer class form to the socket. */
class WSWriterStream extends Writer {
    private client: Client;

    public constructor(client: Client) {
        super();

        this.client = client;
    }

    public send() {
        this.client.send(this.write());
    }
}

/** Used to store flags and apply once a tick. */
export class ClientInputs extends Inputs {
    /** Used to store flags and apply once a tick. */
    public cachedFlags = 0;

    /** Just a place to store whether or not the client is possessing an AI. */
    public isPossessing = false;

    /** The Client owner */
    public client: Client;

    public constructor(client: Client) {
        super();
        this.client = client;
    }
}

export interface ClientWrapper {
    client: Client | null;
    ipAddress: string;
    gamemode: string;
}

export default class Client {
    /** Set to true if the client socket has been terminated. */
    public terminated = false;
    /** The game tick at which the client connected. */
    private connectTick: number;
    /** The last tick that the client received a ping. */
    private lastPingTick: number;
    /** The client's access level. */
    public accessLevel: config.AccessLevel = config.AccessLevel.NoAccess;

    /** Cache of all incoming packets of the current tick. */
    private incomingCache: Uint8Array[][] = [];
    /** The parsed input data from the socket. */
    public inputs: ClientInputs = new ClientInputs(this);

    /** Current game server. */
    public game: GameServer;
    /** Inner websocket connection. */
    public ws: WebSocket<ClientWrapper> | null;
    /** Client's camera entity. */
    public camera: ClientCamera | null = null;

    /** Whether or not the player has used in game dev cheats before (such as level up or godmode). */
    private devCheatsUsed: boolean = false;
    /** Wether or not the player is in godmode. */
    public isInvulnerable: boolean = false;

    /** Returns a new writer stream connected to the socket. */
    public write() {
        return new WSWriterStream(this);
    }

    public constructor(ws: WebSocket<ClientWrapper>, game: GameServer) {
        this.game = game;
        this.game.clients.add(this);
        this.ws = ws;
        this.lastPingTick = this.connectTick = game.tick;
    }

    /** Accepts the client and creates a camera for it. */
    private acceptClient() {
        this.write().u8(ClientBound.ServerInfo).stringNT(this.game.gamemode).stringNT(config.host).send();
        this.write().u8(ClientBound.PlayerCount).vu(GameServer.globalPlayerCount).send();
        this.write().u8(ClientBound.Accept).vi(this.accessLevel).send();
        this.camera = new ClientCamera(this.game, this);
    }

    /** Sends data to client. */
    public send(data: Uint8Array) {
        const ws = this.ws;
        if (!ws) throw new Error("Can't write to a closed websocket - shouldn't be referencing a closed client");
        ws.send(data, true, true);
    }

    /** Handles close event. */
    public onClose(code: number, message: ArrayBuffer) {
        this.ws = null;

        this.terminate();
    }

    /** Terminates the connection and related things. */
    public terminate() {
        // if there's still a ws connected, close it - onclose handler will then terminate for us
        if (this.terminated) return;
        if (this.ws) return this.ws.close();

        this.terminated = true;
        this.game.clients.delete(this);

        this.inputs.deleted = true;
        this.inputs.movement.magnitude = 0;

        if (Entity.exists(this.camera)) this.camera.delete();
    }

    /** Handles to incoming messages. */
    public onMessage(buffer: ArrayBuffer, isBinary: boolean) {
        if (!isBinary) return this.terminate();
        const data = new Uint8Array(buffer).slice();
        if (data[0] === 0x00 && data.byteLength === 1) return this.terminate(); // We do not host ping servers.
        const header = data[0];
        if (header === ServerBound.Ping) {
            this.lastPingTick = this.game.tick;
            this.send(PING_PACKET);
        } else {
            if(!this.incomingCache[header]) this.incomingCache[header] = [];
            if (this.incomingCache[header].length) {
                if (header === ServerBound.Input) {
                    // Otherwise store the flags
                    const r = new Reader(data);
                    r.at = 1;

                    const flags = r.vu();
                    this.inputs.cachedFlags |= flags & 0b110111100001; // WASD and gamepad are not stored.
                } else if (header === ServerBound.StatUpgrade) {
                    this.incomingCache[header].push(data);
                }
                return;
            }
            this.incomingCache[header][0] = data;
        }
    }

    /** Parses incoming packets. */
    private handleIncoming(header: number, data: Uint8Array): void {
        if (this.terminated) return;

        const r = new Reader(data);
        r.at = 1;

        const camera = this.camera;

        if (header === ServerBound.Init) {
            if (camera) return this.terminate(); // only one connection;

            const buildHash = r.stringNT();
            const pw = r.stringNT();
            // const party = r.stringNT();
            // const debugId = r.vu();

            if (buildHash !== config.buildHash) {

                util.log("Kicking client. Invalid build hash " + JSON.stringify(buildHash));
                util.saveToVLog(this.toString() + " being kicked, wrong version hash " + JSON.stringify(buildHash));

                this.write().u8(ClientBound.OutdatedClient).stringNT(config.buildHash).send();
                setTimeout(() => this.terminate(), 100);
                return;
            }

            // Hardcoded dev password
            if (config.devPasswordHash && createHash("sha256").update(pw).digest("hex") === config.devPasswordHash) {
                this.accessLevel = config.AccessLevel.FullAccess;
                util.saveToLog("Developer Connected", "A client connected to the server (`" + this.game.gamemode + "`) with `full` access.", 0x5A65EA);
            } else {
                this.accessLevel = config.defaultAccessLevel;
            }

            if (this.accessLevel === config.AccessLevel.NoAccess) {
                util.saveToVLog("Possibly unknown, client terminated due to lack of authentication:: " + this.toString());
                return this.terminate();
            }

            this.acceptClient();

            return;
        }

        if (!Entity.exists(camera)) return;

        switch (header) {
            case ServerBound.Input: {
                // Beware, this code gets less readable as you scroll
                const previousFlags = this.inputs.flags;
                const flags = this.inputs.flags = r.vu() | this.inputs.cachedFlags;
                this.inputs.cachedFlags = 0;

                this.inputs.mouse.x = r.vf();
                this.inputs.mouse.y = r.vf();

                if (!Vector.isFinite(this.inputs.mouse)) break;
                const movement: VectorAbstract = {
                    x: 0,
                    y: 0
                };

                if (flags & InputFlags.up) movement.y -= 1;
                if (flags & InputFlags.down) movement.y += 1;
                if (flags & InputFlags.right) movement.x += 1;
                if (flags & InputFlags.left) movement.x -= 1;
                
                if (movement.x || movement.y) {
                    const angle = Math.atan2(movement.y, movement.x);

                    const magnitude = util.constrain(Math.sqrt(movement.x ** 2 + movement.y ** 2), -1, 1);

                    this.inputs.movement.magnitude = magnitude;
                    this.inputs.movement.angle = angle;
                }

                const player = camera.cameraData.values.player;
                if (!Entity.exists(player) || !(player instanceof TankBody)) return;

                // No AI
                if (this.inputs.isPossessing && this.accessLevel !== config.AccessLevel.FullAccess) return;
                
                if ((flags & InputFlags.godmode)) {
                    if (this.accessLevel >= config.AccessLevel.BetaAccess) {
                        this.setHasCheated(true);
                        player.setTank(player.currentTank < 0 ? Tank.Basic : DevTank.Developer);
                    } else if (this.game.arena.arenaData.values.flags & ArenaFlags.canUseCheats) {
                        if (this.game.clients.size === 1 && this.game.arena.state === ArenaState.OPEN) {
                            this.setHasCheated(true);

                            player.setInvulnerability(!player.isInvulnerable);
                            
                            this.notify(`God mode: ${player.isInvulnerable ? "ON" : "OFF"}`, 0x000000, 1000, 'godmode');
                        }
                    }
                }

                if ((flags & InputFlags.rightclick) && !(previousFlags & InputFlags.rightclick) && (player.currentTank === DevTank.Developer || player.currentTank === DevTank.Spectator)) {
                    player.positionData.x = this.inputs.mouse.x;
                    player.positionData.y = this.inputs.mouse.y;
                    player.setVelocity(0, 0);
                    player.entityState |= EntityStateFlags.needsCreate | EntityStateFlags.needsDelete;
                }

                if ((flags & InputFlags.switchtank) && !(previousFlags & InputFlags.switchtank)) {
                    if (this.accessLevel >= config.AccessLevel.BetaAccess || (this.game.arena.arenaData.values.flags & ArenaFlags.canUseCheats)) {
                        this.setHasCheated(true);
                        
                        let tank = player.currentTank;
                        if (tank >= 0) {
                            tank = (tank + TankDefinitions.length - 1) % TankDefinitions.length;

                            while (!TankDefinitions[tank] || (TankDefinitions[tank]?.flags.devOnly && this.accessLevel < config.AccessLevel.FullAccess)) {
                                tank = (tank + TankDefinitions.length - 1) % TankDefinitions.length;
                            }
                        } else {
                            const isDeveloper = this.accessLevel === config.AccessLevel.FullAccess;
                            tank = ~tank;
                            
                            tank = (tank + 1) % DevTankDefinitions.length;
                            while (!DevTankDefinitions[tank] || DevTankDefinitions[tank].flags.devOnly === true && !isDeveloper) {
                                tank = (tank + 1) % DevTankDefinitions.length;
                            }
                            tank = ~tank;
                        }

                        player.setTank(tank);
                    }
                }

                if (flags & InputFlags.levelup) {
                    // If full access, or if the game allows cheating and lvl is < maxLevel, or if the player is a BT access level and lvl is < maxLevel
                    if ((this.accessLevel === config.AccessLevel.FullAccess) || (camera.cameraData.values.level < config.maxPlayerLevel && ((this.game.arena.arenaData.values.flags & ArenaFlags.canUseCheats) || (this.accessLevel === config.AccessLevel.BetaAccess)))) {
                        this.setHasCheated(true);
                        
                        camera.setLevel(camera.cameraData.values.level + 1);
                    }
                }

                if ((flags & InputFlags.suicide) && (!player.deletionAnimation || !player.deletionAnimation) && !this.inputs.isPossessing) {
                    if (this.accessLevel >= config.AccessLevel.BetaAccess || (this.game.arena.arenaData.values.flags & ArenaFlags.canUseCheats)) {
                        this.setHasCheated(true);
                        
                        this.notify("You've killed " + (player.nameData.values.name === "" ? "an unnamed tank" : player.nameData.values.name));
                        camera.cameraData.killedBy = player.nameData.values.name;
                        player.destroy();
                    }
                }
                
                return;
            }
            case ServerBound.Spawn: {
                util.log("Client wants to spawn");

                if ((this.game.arena.state >= ArenaState.CLOSING)) return;
                if (Entity.exists(camera.cameraData.values.player)) return this.terminate();

                camera.cameraData.values.statsAvailable = 0;
                camera.cameraData.values.level = 1;

                for (let i = 0; i < StatCount; ++i) {
                    camera.cameraData.values.statLevels.values[i] = 0;
                }

                const name = r.stringNT().slice(0, 16);

                const tank = camera.cameraData.player = camera.relationsData.owner = camera.relationsData.parent = new TankBody(this.game, camera, this.inputs);
                tank.setTank(Tank.Basic);
                this.game.arena.spawnPlayer(tank, this);
                camera.setLevel(camera.cameraData.values.respawnLevel);
                tank.nameData.values.name = name;

                if (this.hasCheated()) this.setHasCheated(true);

                // Force-send a creation to the client - Only if it is not new
                camera.entityState = EntityStateFlags.needsCreate | EntityStateFlags.needsDelete;
                camera.spectatee = null;
                this.inputs.isPossessing = false;
                this.inputs.movement.magnitude = 0;

                return;
            }
            case ServerBound.StatUpgrade: {
                if (camera.cameraData.statsAvailable <= 0) return;

                const player = camera.cameraData.values.player;
                if (!Entity.exists(player) || !(player instanceof TankBody)) return;
                // No AI
                if (this.inputs.isPossessing) return;

                const definition = getTankById(player.currentTank);
                if (!definition || !definition.stats.length) return;

                const statId: Stat = r.vi() ^ STAT_XOR;

                if (statId < 0 || statId >= StatCount) return;

                const statLimit = camera.cameraData.values.statLimits.values[statId];

                if (camera.cameraData.values.statLevels.values[statId] >= statLimit) return;

                camera.cameraData.statLevels[statId] += 1;
                camera.cameraData.statsAvailable -= 1;

                return;
            }
            case ServerBound.TankUpgrade: {
                const player = camera.cameraData.values.player;
                // No AI
                if (this.inputs.isPossessing) return;
                if (!Entity.exists(player) || !(player instanceof TankBody)) return;

                const definition = getTankById(player.currentTank);
                const tankId: Tank = r.vi() ^ TANK_XOR;
                const tankDefinition = getTankById(tankId);
                if (!definition || !definition.upgrades.includes(tankId) || !tankDefinition || tankDefinition.levelRequirement > camera.cameraData.values.level) return;

                player.setTank(tankId);

                return;
            }
            case ServerBound.ExtensionFound: {
                util.log("Someone is cheating");
                this.ban();

                return;
            }
            case ServerBound.ToRespawn: {
                // Doesn't matter if the player is alive or not in real diep.
                camera.cameraData.flags &= ~CameraFlags.showingDeathStats;

                return;
            }
            case ServerBound.TakeTank: {
                if (!Entity.exists(camera.cameraData.player)) return;
                if (!this.game.entities.AIs.length) return this.notify("Someone has already taken that tank", 0x000000, 5000, "cant_claim_info");
                if (!this.inputs.isPossessing) {
                    const x = camera.cameraData.player.positionData?.values.x || 0;
                    const y = camera.cameraData.player.positionData?.values.y || 0;
                    const AIs = Array.from(this.game.entities.AIs);
                    AIs.sort((a: AI, b: AI) => {
                        const {x: x1, y: y1} = a.owner.getWorldPosition();
                        const {x: x2, y: y2} = b.owner.getWorldPosition();

                        return ((x1 - x) ** 2 + (y1 - y) ** 2) - ((x2 - x) ** 2 + (y2 - y) ** 2);
                    });

                    for (let i = 0; i < AIs.length; ++i) {
                        if ((AIs[i].state !== AIState.possessed) && ((AIs[i].owner.relationsData.values.team === camera.relationsData.values.team && AIs[i].isClaimable) || this.accessLevel === config.AccessLevel.FullAccess)) {
                            if(!this.possess(AIs[i])) continue;
                            this.notify("Press H to surrender control of your tank", 0x000000, 5000);
                            return;
                        }
                    }

                    this.notify("Someone has already taken that tank", 0x000000, 5000, "cant_claim_info");
                } else {
                    this.inputs.deleted = true;
                }

                return;
            }
            case ServerBound.TCPInit:
                if(!config.enableCommands) return;
                const cmd = r.stringNT();
                const argsLength = r.u8();
                const args: string[] = [];
                for(let i = 0; i < argsLength; ++i) {
                    args.push(r.stringNT());
                }
                executeCommand(this, cmd, args);
                return;
            default:
                util.log("Suspicious activies have been evaded")
                return this.ban();
        }
    }

    /** Defines whether the player used cheats or not. This also defines whether the name is highlighted or not. */
    public setHasCheated(value: boolean) {
        const player = this.camera?.cameraData.values.player;
        if (player && player.nameData) {
            if (value) player.nameData.flags |= NameFlags.highlightedName;
            else player.nameData.flags &= ~NameFlags.highlightedName;
        }

        this.devCheatsUsed = value;
    }

    /** Exposes devCheatsUsed */
    public hasCheated(): boolean {
        return this.devCheatsUsed;
    }

    /** Attempts possession of an AI */
    public possess(ai: AI) {
        if (!this.camera?.cameraData || ai.state === AIState.possessed) return false;

        this.inputs.deleted = true;
        ai.inputs = this.inputs = new ClientInputs(this);
        this.inputs.isPossessing = true;
        ai.state = AIState.possessed;

        // Silly workaround to change color of player when needed
        if (this.camera?.cameraData.values.player instanceof ObjectEntity) {
            const color = this.camera.cameraData.values.player.styleData.values.color;
            this.camera.cameraData.values.player.styleData.values.color = -1;
            this.camera.cameraData.values.player.styleData.color = color;
        }

        this.camera.cameraData.tankOverride = ai.owner.nameData?.values.name || "";
        
        this.camera.cameraData.tank = 53;
        
        // AI stats, confirmed by Mounted Turret videos
        for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statLevels[i as Stat] = 0;
        for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statLimits[i as Stat] = 7;
        for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statNames[i as Stat] = "";


        this.camera.cameraData.killedBy = "";
        this.camera.cameraData.player = ai.owner;
        this.camera.cameraData.movementSpeed = ai.movementSpeed;

        if (ai.owner instanceof TankBody) {
            // If its a TankBody, set the stats, level, and tank to that of the TankBody
            this.camera.cameraData.tank = ai.owner.cameraEntity.cameraData.values.tank;
            this.camera.setLevel(ai.owner.cameraEntity.cameraData.values.level);
            
            for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statLevels[i as Stat] = ai.owner.cameraEntity.cameraData.statLevels.values[i];
            for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statLimits[i as Stat] = ai.owner.cameraEntity.cameraData.statLimits.values[i];
            for (let i = 0; i < StatCount; ++i) this.camera.cameraData.statNames[i as Stat] = ai.owner.cameraEntity.cameraData.statNames.values[i];

            this.camera.cameraData.FOV = ai.owner.cameraEntity.cameraData.FOV;
        } else if (ai.owner instanceof AbstractBoss) {
            this.camera.setLevel(75);
            this.camera.cameraData.FOV = 0.35;
        } else {
            this.camera.setLevel(30);
        }
        
        this.camera.cameraData.statsAvailable = 0;
        this.camera.cameraData.score = 0;
        return true;
    }

    /** Sends a notification packet to the client. */
    public notify(text: string, color = 0x000000, time = 4000, id = "") {
        this.write().u8(ClientBound.Notification).stringNT(text).u32(color).float(time).stringNT(id).send();
    }

    /** Bans the ip from all servers until restart. */
    public ban() {
        if(!this.ws) return;
        util.saveToLog("IP Banned", "Banned " + this.ws.getUserData().ipAddress + this.toString(true), 0xEE326A);
        if (this.accessLevel >= config.unbannableLevelMinimum) {
            util.saveToLog("IP Ban Cancelled", "Cancelled ban on " + this.ws.getUserData().ipAddress + this.toString(true), 0x6A32EE);
            return;
        }

        bannedClients.add(this.ws.getUserData().ipAddress)

        for (const client of this.game.clients) {
            if(client.ws?.getUserData().ipAddress === this.ws.getUserData().ipAddress) client.terminate();
        }
    }

    public tick(tick: number) {
        for (let header = 0; header <= this.incomingCache.length; ++header) {
            if (header === ServerBound.Ping) continue;

            if (this.incomingCache[header]?.length === 1) this.handleIncoming(header, this.incomingCache[header][0]);
            else if (this.incomingCache[header]?.length > 1) {
                for (let i = 0, len = this.incomingCache[header].length; i < len; ++i) this.handleIncoming(header, this.incomingCache[header][i]);
            } else continue;
            this.incomingCache[header] = [];
        }

        if (!this.camera) {
            if (tick === this.connectTick + 300) {
                return this.terminate();
            }
        } else if (this.inputs.deleted) {
            this.inputs = new ClientInputs(this);
            this.camera.cameraData.player = null;
            this.camera.cameraData.respawnLevel = 0;
            this.camera.cameraData.cameraX = this.camera.cameraData.cameraY = 0;
            this.camera.cameraData.flags &= ~CameraFlags.showingDeathStats;
        }
        if (tick >= this.lastPingTick + 60 * config.tps) {
            return this.terminate();
        }
    }
    /** toString override from base Object. Adds debug info */
    public toString(verbose: boolean = false): string {
        const tokens: string[] = [];

        if (this.camera?.cameraData?.player?.nameData?.name) tokens.push("name=" + JSON.stringify(this.camera?.cameraData?.player?.nameData?.name));
        if (verbose) {
            if (this.ws) tokens.push("ip=" + this.ws.getUserData().ipAddress);
            if (this.game.gamemode) tokens.push("game.gamemode=" + this.game.gamemode);
        }
        if (this.terminated) tokens.push("(terminated)");
        if (!tokens.length) return `Client(${this.accessLevel}) {}`

        return `Client(${this.accessLevel}) { ${tokens.join(', ')} }`
    }
}
`

~src/Game.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as config from "./config";
import * as util from "./util";
import Writer from "./Coder/Writer";
import EntityManager from "./Native/Manager";
import Client from "./Client";
import ArenaEntity from "./Native/Arena";
import FFAArena from "./Gamemodes/FFA";
import Teams2Arena from "./Gamemodes/Team2";
import SandboxArena from "./Gamemodes/Sandbox";
import { ClientBound } from "./Const/Enums";
import Teams4Arena from "./Gamemodes/Team4";
import DominationArena from "./Gamemodes/Domination";
import MothershipArena from "./Gamemodes/Mothership";
import TestingArena from "./Gamemodes/Misc/Testing";
import SpikeboxArena from "./Gamemodes/Misc/Spikebox";
import DominationTestingArena from "./Gamemodes/Misc/DomTest";
import JungleArena from "./Gamemodes/Misc/Jungle";
import FactoryTestArena from "./Gamemodes/Misc/FactoryTest";
import BallArena from "./Gamemodes/Misc/Ball";
import MazeArena from "./Gamemodes/Maze";

/**
 * WriterStream that broadcasts to all of the game's WebSockets.
 */
class WSSWriterStream extends Writer {
    private game: GameServer;

    public constructor(game: GameServer) {
        super();
        this.game = game;
    }

    public send() {
        const bytes = this.write();

        for (let client of this.game.clients) {
            client.send(bytes);
        }
    }
}

type DiepGamemodeID = "ffa" | "sandbox" | "teams" | "4teams" | "mot" | "dom" | "maze" | "tag" | "survival" | "testing" | "spike" | "domtest" | "jungle" | "factest" | "ball";

const GamemodeToArenaClass: Record<DiepGamemodeID, (typeof ArenaEntity) | null> & { "*": typeof ArenaEntity }= {
    "ffa": FFAArena,
    "teams": Teams2Arena,
    "4teams": Teams4Arena,
    "sandbox": SandboxArena,
    "*": SandboxArena,
    "dom": DominationArena,
    "survival": null,
    "tag": null,
    "mot": MothershipArena,
    "maze": MazeArena,
    "testing": TestingArena,
    "spike": SpikeboxArena,
    "domtest": DominationTestingArena,
    "jungle": JungleArena,
    "factest": FactoryTestArena,
    "ball": BallArena
}

/**
 * Used for determining which endpoints go to the default.
 */
export default class GameServer {
    /** Stores total player count. */
    public static globalPlayerCount = 0;
    /** Whether or not the game server is running. */
    public running = true;
    /** The gamemode the game is running. */
    public gamemode: DiepGamemodeID;
    /** The arena's display name. */
    public name: string;
    /** Whether or not to put players on the map. */
    public playersOnMap: boolean = false;
    /** All clients connected. */
    public clients: Set<Client>;
    /** Entity manager of the game. */
    public entities: EntityManager;
    /** The current game tick. */
    public tick: number;
    /** The game's arena entity. */
    public arena: ArenaEntity;
    /** The interval timer of the tick loop. */
    private _tickInterval: NodeJS.Timeout;

    public constructor(gamemode: DiepGamemodeID, name: string | "*") {
        this.gamemode = gamemode;
        this.name = name;

        this.clients = new Set();
        // Keeps player count updating per addition
        const _add = this.clients.add;
        this.clients.add = (client: Client) => {
            GameServer.globalPlayerCount += 1;
            this.broadcastPlayerCount();
            
            return _add.call(this.clients, client);
        }
        const _delete = this.clients.delete;
        this.clients.delete = (client: Client) => {
            let success = _delete.call(this.clients, client);
            if (success) {
                GameServer.globalPlayerCount -= 1;
                this.broadcastPlayerCount();
            }

            return success;
        }
        const _clear = this.clients.clear;
        this.clients.clear = () => {
            GameServer.globalPlayerCount -= this.clients.size;
            this.broadcastPlayerCount();
            
            return _clear.call(this.clients);
        }

        this.entities = new EntityManager(this);
        this.tick = 0;

        this.arena = new (GamemodeToArenaClass[this.gamemode] || GamemodeToArenaClass["*"])(this);

        this._tickInterval = setInterval(() => {
            if (this.clients.size) this.tickLoop();
        }, config.mspt);
    }

    /** Returns a WebSocketServer Writer Broadcast Stream. */
    public broadcast() {
        return new WSSWriterStream(this);
    }
    /** Broadcasts a player count packet. */
    public broadcastPlayerCount() {
        this.broadcast().vu(ClientBound.PlayerCount).vu(GameServer.globalPlayerCount).send();
    }

    /** Ends the game instance. */
    public end() {
        util.saveToLog("Game Instance Ending", "Game running " + this.gamemode + " at `" + this.gamemode + "` is now closing.", 0xEE4132);
        util.log("Ending Game instance");

        clearInterval(this._tickInterval);

        for (const client of this.clients) {
            client.terminate()
        }

        this.tick = 0;
        this.clients.clear();
        this.entities.clear();

        this.running = false;
        this.onEnd();
    }

    /** Can be overwritten to call things when the game is over */
    public onEnd() {
        util.log("Game instance is now over");
        this.start();
    }

    /** Reinitializes a game instance */
    public start() {
        if (this.running) return;

        util.log("New game instance booting up")

        this.clients.clear();

        this.entities = new EntityManager(this);
        this.tick = 0;

        this.arena = new (GamemodeToArenaClass[this.gamemode] || GamemodeToArenaClass["*"])(this);

        this._tickInterval = setInterval(() => {
            if (this.clients.size) this.tickLoop();
        }, config.mspt);
    }

    /** Ticks the game. */
    private tickLoop() {
        
        this.tick += 1;
        this.entities.tick(this.tick);

        for (const client of this.clients) client.tick(this.tick);
    }
}
`

~src/config.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/** The build supported by the server. */
export const buildHash: string = "6f59094d60f98fafc14371671d3ff31ef4d75d9e";

/** The port the server is hosting its game server on. */
export const serverPort: number = parseInt(process.env.PORT || "8080");

/** Milliseconds per tick in the game. */
export const mspt: number = 40;

/** Ticks per second in the game */
export const tps: number = 1000 / mspt;

/** Max connections per ip. Infinity = no limit */
export const connectionsPerIp: number = Infinity;

/** Max incoming packet size (HARD LIMIT), not the max read / write size */
export const wssMaxMessageSize: number = 4096; // 4 kb

/** Output Chunk Size for the Writer (during resize) */
export const writtenBufferChunkSize = Buffer.poolSize || 2048;

/** Host id to be sent to client. */
export const host: string = process.env.SERVER_INFO || (process.env.NODE_ENV === "development" ? "localhost" : "");

/** Runtime mode. */
export const mode: string = process.env.NODE_ENV || "development";

/** Is hosting a rest api */
export const enableApi: boolean = true;

/** Rest API location (root of all other endpoints), ignored if enableApi is false */
export const apiLocation: string = "api";

/** Allows execution of custom commands */
export const enableCommands: boolean = true;

/** Is hosting a client */
export const enableClient: boolean = true;

/** Client files location, ignored if enableClient is false, path from the root dir of the project */
export const clientLocation: string = "./client";

/** Magic number used for tank xor and stat xor. */
export const magicNum = (function magicNum(build: string) {
    let char;
    for (var i = 0, seed = 1, res = 0, timer = 0; i < 40; i++) {
        char = parseInt(build[i], 16);
        res ^= ((char << ((seed & 1) << 2)) << (timer << 3));
        timer = (timer + 1) & 3;
        seed ^= +!(timer);
    };
    return res >>> 0; // unsigned
})(buildHash);

/** Spatial Hashing CellSize for physics. Zero = quadtree. */
export const spatialHashingCellSize: number = 7;

/** Amount of TICKs before the next boss spawn attempt */
export const bossSpawningInterval = 45 * 60 * tps;

/** Amount of TICKs before the scoreboard update */
export const scoreboardUpdateInterval = 0.5 * tps;

/** Hashed (sha256) dev password */
export const devPasswordHash: string | undefined = process.env.DEV_PASSWORD_HASH;

/** Whether or not Verbose Logs should be logged */
export const doVerboseLogs: boolean = false;

/** Access levels of each client. */
export const enum AccessLevel {
    FullAccess   = 3,
    BetaAccess   = 2,
    kReserved    = 1,
    PublicAccess = 0,
    NoAccess     = -1
}

/** Every access level, including and above this one is unbannable via client.ban() */
export const unbannableLevelMinimum: AccessLevel = AccessLevel.FullAccess;

/** Default access level, client's without valid password's will get set to this */
export const defaultAccessLevel: AccessLevel = AccessLevel.BetaAccess;

/** Maximum level that player tanks can have. Default: 45 */
export const maxPlayerLevel = 45;
`

~src/config.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

/** The build supported by the server. */
export const buildHash: string = "6f59094d60f98fafc14371671d3ff31ef4d75d9e";

/** The port the server is hosting its game server on. */
export const serverPort: number = parseInt(process.env.PORT || "8080");

/** Milliseconds per tick in the game. */
export const mspt: number = 40;

/** Ticks per second in the game */
export const tps: number = 1000 / mspt;

/** Max connections per ip. Infinity = no limit */
export const connectionsPerIp: number = Infinity;

/** Max incoming packet size (HARD LIMIT), not the max read / write size */
export const wssMaxMessageSize: number = 4096; // 4 kb

/** Output Chunk Size for the Writer (during resize) */
export const writtenBufferChunkSize = Buffer.poolSize || 2048;

/** Host id to be sent to client. */
export const host: string = process.env.SERVER_INFO || (process.env.NODE_ENV === "development" ? "localhost" : "");

/** Runtime mode. */
export const mode: string = process.env.NODE_ENV || "development";

/** Is hosting a rest api */
export const enableApi: boolean = true;

/** Rest API location (root of all other endpoints), ignored if enableApi is false */
export const apiLocation: string = "api";

/** Allows execution of custom commands */
export const enableCommands: boolean = true;

/** Is hosting a client */
export const enableClient: boolean = true;

/** Client files location, ignored if enableClient is false, path from the root dir of the project */
export const clientLocation: string = "./client";

/** Magic number used for tank xor and stat xor. */
export const magicNum = (function magicNum(build: string) {
    let char;
    for (var i = 0, seed = 1, res = 0, timer = 0; i < 40; i++) {
        char = parseInt(build[i], 16);
        res ^= ((char << ((seed & 1) << 2)) << (timer << 3));
        timer = (timer + 1) & 3;
        seed ^= +!(timer);
    };
    return res >>> 0; // unsigned
})(buildHash);

/** Spatial Hashing CellSize for physics. Zero = quadtree. */
export const spatialHashingCellSize: number = 7;

/** Amount of TICKs before the next boss spawn attempt */
export const bossSpawningInterval = 45 * 60 * tps;

/** Amount of TICKs before the scoreboard update */
export const scoreboardUpdateInterval = 0.5 * tps;

/** Hashed (sha256) dev password */
export const devPasswordHash: string | undefined = process.env.DEV_PASSWORD_HASH;

/** Whether or not Verbose Logs should be logged */
export const doVerboseLogs: boolean = false;

/** Access levels of each client. */
export const enum AccessLevel {
    FullAccess   = 3,
    BetaAccess   = 2,
    kReserved    = 1,
    PublicAccess = 0,
    NoAccess     = -1
}

/** Every access level, including and above this one is unbannable via client.ban() */
export const unbannableLevelMinimum: AccessLevel = AccessLevel.FullAccess;

/** Default access level, client's without valid password's will get set to this */
export const defaultAccessLevel: AccessLevel = AccessLevel.BetaAccess;

/** Maximum level that player tanks can have. Default: 45 */
export const maxPlayerLevel = 45;
`

~src/index.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as fs from "fs";
import { App, SHARED_COMPRESSOR, WebSocket, WebSocketBehavior } from "uWebSockets.js";
import Client, { ClientWrapper } from "./Client";
import * as config from "./config"
import * as util from "./util";
import GameServer from "./Game";
import TankDefinitions from "./Const/TankDefinitions";
import { commandDefinitions } from "./Const/Commands";
import { ColorsHexCode } from "./Const/Enums";

const PORT = config.serverPort;
const ENABLE_API = config.enableApi && config.apiLocation;
const ENABLE_CLIENT = config.enableClient && config.clientLocation && fs.existsSync(config.clientLocation);

if (ENABLE_API) util.log(`Rest API hosting is enabled and is now being hosted at /${config.apiLocation}`);
if (ENABLE_CLIENT) util.log(`Client hosting is enabled and is now being hosted from ${config.clientLocation}`);

export const bannedClients = new Set<string>();
const connections = new Map<string, number>();
const allClients = new Set<Client>();
const app = App({});
const games: GameServer[] = [];

app.ws("/*", {
    compression: SHARED_COMPRESSOR,
    sendPingsAutomatically: true,
    maxPayloadLength: config.wssMaxMessageSize,
    idleTimeout: 10,
    upgrade: (res, req, context) => {
        res.upgrade({ client: null, ipAddress: "", gamemode: req.getUrl().slice(1) } as ClientWrapper,
            req.getHeader('sec-websocket-key'),
            req.getHeader('sec-websocket-protocol'),
            req.getHeader('sec-websocket-extensions'),
            context);
    },
    open: (ws: WebSocket<ClientWrapper>) => {
        const ipAddress = Buffer.from(ws.getRemoteAddressAsText()).toString();
        let conns = 0;
        if (connections.has(ipAddress)) conns = connections.get(ipAddress) as number;
        if (conns >= config.connectionsPerIp || bannedClients.has(ipAddress)) {
            return ws.close();
        }
        connections.set(ipAddress, conns + 1);
        const game = games.find(({ gamemode }) => gamemode === ws.getUserData().gamemode);
        if (!game) {
            return ws.close();
        }
        const client = new Client(ws, game);
        allClients.add(client);
        ws.getUserData().ipAddress = ipAddress;
        ws.getUserData().client = client;
    },
    message: (ws: WebSocket<ClientWrapper>, message, isBinary) => {
        const {client} = ws.getUserData();
        if (!client) throw new Error("Unexistant client for websocket");
        client.onMessage(message, isBinary);
    },
    close: (ws: WebSocket<ClientWrapper>, code, message) => {
        const {client, ipAddress} = ws.getUserData();
        if (client) {
            connections.set(ipAddress, connections.get(ipAddress) as number - 1);
            client.onClose(code, message);
            allClients.delete(client);
        }
    }
} as WebSocketBehavior<ClientWrapper>);

app.get("/*", (res, req) => {
    util.saveToVLog("Incoming request to " + req.getUrl());
    res.onAborted(() => {});
    if (ENABLE_API && req.getUrl().startsWith(`/${config.apiLocation}`)) {
        switch (req.getUrl().slice(config.apiLocation.length + 1)) {
            case "/":
                res.writeStatus("200 OK").end();
                return;
            case "/tanks":
                res.writeStatus("200 OK").end(JSON.stringify(TankDefinitions));
                return;
            case "/servers":
                res.writeStatus("200 OK").end(JSON.stringify(games.map(({ gamemode, name }) => ({ gamemode, name }))));
                return;
            case "/commands":
                res.writeStatus("200 OK").end(JSON.stringify(config.enableCommands ? Object.values(commandDefinitions) : []));
                return;
            case "/colors":
                res.writeStatus("200 OK").end(JSON.stringify(ColorsHexCode));
                return;
        }
    }

    if (ENABLE_CLIENT) {
        let file: string | null = null;
        let contentType = "text/html"
        switch (req.getUrl()) {
            case "/":
                file = config.clientLocation + "/index.html";
                contentType = "text/html";
                break;
            case "/loader.js":
                file = config.clientLocation + "/loader.js";
                contentType = "application/javascript";
                break;
            case "/input.js":
                file = config.clientLocation + "/input.js";
                contentType = "application/javascript";
                break;
            case "/dma.js":
                file = config.clientLocation + "/dma.js";
                contentType = "application/javascript";
                break;
            case "/config.js":
                file = config.clientLocation + "/config.js";
                contentType = "application/javascript";
                break;
        }

        res.writeHeader("Content-Type", contentType + "; charset=utf-8");

        if (file && fs.existsSync(file)) {
            res.writeStatus("200 OK").end(fs.readFileSync(file));
            return;
        }

        res.writeStatus("404 Not Found").end(fs.readFileSync(config.clientLocation + "/404.html"));
        return;
    } 
});

app.listen(PORT, (success) => {
    if (!success) throw new Error("Server failed");

    util.log(`Listening on port ${PORT}`);

    // RULES(0): No two game servers should share the same endpoint
    //
    // NOTES(0): As of now, both servers run on the same process (and thread) here
    const ffa = new GameServer("ffa", "FFA");
    const sbx = new GameServer("sandbox", "Sandbox");
    
    games.push(ffa, sbx);

    util.saveToLog("Servers up", "All servers booted up.", 0x37F554);
    util.log("Dumping endpoint -> gamemode routing table");
    for (const game of games) console.log("> " + `localhost:${config.serverPort}/${game.gamemode}`.padEnd(40, " ") + " -> " + game.name);
});

process.on("uncaughtException", (error) => {
    util.saveToLog("Uncaught Exception", '```\n' + error.stack + '\n```', 0xFF0000);

    throw error;
});
`

~src/util.ts:
`
/*
    DiepCustom - custom tank game server that shares diep.io's WebSocket protocol
    Copyright (C) 2022 ABCxFF (github.com/ABCxFF)

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>
*/

import * as chalk from "chalk";
import { inspect } from "util";
import { doVerboseLogs } from "./config";

/** Logs data prefixed with the Date. */
export const log = (...args: any[]) => {
    console.log(`[${Date().split(" ")[4]}]`, ...args)
}
/** Logs data prefixed with the Date in a yellow format. */
export const warn = (...args: any[]) => {
    args = args.map(s => typeof s === "string" ? chalk.yellow(s) : s);
    console.log(chalk.yellow(`[${Date().split(" ")[4]}] WARNING: `), ...args);
}
/** Logs a raw object. */
export const inspectLog = (object: any, c = 14) => {
    console.log(inspect(object, false, c, true));
}

/**
 * Removes an element from an array by index quickly.
 * Unordered removal.
 */
export const removeFast = (array: any[], index: number) => {
    if (index < 0 || index >= array.length) throw new RangeError("Index out of range. In `removeFast`")

    if (index === array.length - 1) array.pop();
    else array[index] = array.pop();
}

/**
 * Contrains a value between bounds
 */
export const constrain = (value: number, min: number, max: number) => {
    return Math.max(min, Math.min(max, value));
}

/** 2π */
export const PI2 = Math.PI * 2;

/**
 * Normalize angle (ex: 4π-> 0π, 3π -> 1π)
 */
export const normalizeAngle = (angle: number) => {
    return ((angle % PI2) + PI2) % PI2;
}

/**
 * Logs - Used to have a webhook log here
 */
export const saveToLog = (title: string, description: string, color: number) => {
    console.log("[!] " + title + " (#" + color.toString(16).padStart(6, "0") + ")\n :: " + description);
}

/**
 * Verbose log (if config.doVerboseLogs is set, it will log)
 *  - Used to have a webhook log here
 */
export const saveToVLog = (text: string) => {
   if (doVerboseLogs) console.log("[v] " + text);
}
`