TankEditor API

API made to easily modify your tanks with code

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         TankEditor API
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  API made to easily modify your tanks with code
// @author       r!PsAw
// @match        https://havre.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=havre.io
// @grant        none
// @license      MIT
// ==/UserScript==

const allowed_keys = ["isStartingTank", "isSecret", "hideFromClientDefs", "advancedObjectDef", "key", "name", "upgradeMessage", "levelRequirement", "colliderRadiusFactor", "isRotationControllable", "disableTankAimSnapping", "zoomAbilityRange", "cameraFollowsWhenUsingZoom", "extraStatPoints", "statPointMultiplier", "bonusStats", "health", "bodyDamagePerTick", "regenPerTick", "movementSpeed", "statFactors", "invisibility", "increaseRate", "movementDecreaseRate", "shootingDecreaseRate", "max", "min", "shapeCaptureAbility", "baseLimit", "extraLimitPerReload", "shapeEntries", "spinInfo", "isAffectedByReload", "resetRate", "canSpinWithoutFiring", "canFireWithoutSpinning", "spinUpRate", "spinDownRate", "maxRotationSpeed", "minSpeedWhileFiring", "statPointSetup", "fovFactor", "upgradeKeys", "upgradesFrom", "upgradeIndex", "tankCategory", "rankedSoloPickingEntries", "category", "relativeChance", "build", "rankedTeamPickingEntries", "secondaryFireName", "botAISettings", "botTargetOptions", "aiTypeOverride", "buildOptions", "radius", "shapeID", "playerStatCategory", "shinyPlayerStatCategory", "score", "canBeShiny", "chasingSpeed", "orbitAngularRateFactor", "orbitVelocityFactor", "rotationRateFactor", "spawningAreas", "area", "knockbackTakenFactor", "knockbackGivenFactor", "canMove", "frictionFactor", "type", "sizeFactor", "healthFactor", "damageFactor", "speedFactor", "spreadFactor", "speedVarianceFactor", "lifeLengthFactor", "isDroneControllable", "isDroneControlledBySecondaryFire", "autoSpinSpeed", "spawnAtRandomAngle", "isAutoSpinReversible", "parentVelocityContinuousFactor", "parentVelocityInitialFactor", "missileRotationSpeed", "canCollideWithOtherTraps", "aiSettingsPreset", "aiSettings", "droneReturns", "initialSpeedBoostFactor", "aiType", "predictionBulletSpeedFactor", "droneFleeThreshold", "droneCurveFactor", "wideSpammerAngleSpread", "shootAndRunDoFlip", "shootAndRunUseBothClicks", "order", "randomisableCategories", "isFallenBoss", "isRam", "scoreValue", "predictionBulletSpeed", "maxHealth", "relativeRegenPerTick", "statPoints", "bossCategory", "width", "length", "baseWidthFactor", "endWidthFactor", "points", "xOffset", "yOffset", "animationTranslateFactor", "animationStretchFactor", "color", "customColor", "opacity", "angle", "renderingShapes", "stats", "bulletXOffset", "spinAngle", "spinRadius", "visualSpinSpeed", "chargeXOffset", "firingSpeedRequirement", "sidewaysAngle", "reduceSidewaysAngleWithSpeed", "bulletCount", "reloadFactor", "recoilFactor", "delay", "delayAbusePrevention", "flipfireToggleCategory", "scheduledShotsRequireHolding", "bulletDef", "droneLimit", "firingCondition", "hasDroneStorage", "spawnAtFullCooldown", "angleCalculationReloadFactor", "isNecroBarrel", "shapeCaptureEntryIndex", "isAbove", "isControllable", "maxAngle", "blockAutomaticBehaviour", "autoSpinWhenUnused", "isLayer", "spottingRange", "maxRange", "minPreferredDistance", "maxPreferredDistance", "canAttackShapes", "canAttackTanks", "keepLookingWhileShootingShapes", "keepLookingWhileShootingTanks", "enablePrediction", "enableMovement", "flipWhenCloserThan", "aggroOnParentDamage", "targetLevelThreshold", "radiusFactor", "vertices", "horizontalRadiusFactor", "verticalRadiusFactor", "rotationSpeed", "alwaysRenderAbove", "canRotate", "baseSize", "allowRecentering", "reCenteringOffset", "renderingLayers", "baseDecorations", "barrels", "autoTurrets"];

function compress_tankDef(str) {
    return str
        .replaceAll("true", "\x1D")
        .replaceAll("false", "\x1E")
        .replace(/,?"\w+":/g, t => {
            const a = t[0] == ",";
            const s = t.slice(a ? 2 : 1, -2);

            let o = allowed_keys.indexOf(s);

            return o == -1 ?
                t :
                (
                    a && (o |= 2048),
                    "\x1F" +
                    String.fromCharCode(32 + (63 & o)) +
                    String.fromCharCode(32 + ((o >> 6) & 63))
                );
        });
};

class TankEditor {
    constructor(keyName = 'savedTanks', maxLayers = 255, maxTankBytes = 65535) {
        this.keyName = keyName;
        this.maxLayers = maxLayers;
        this.maxTankBytes = maxTankBytes;
        try {
            this.AllTanks = JSON.parse(localStorage.getItem(this.keyName)) || [];
        } catch {
            console.warn(`[TankEditor]: failed to parse localStorage item "${this.keyName}"`);
            this.AllTanks = [];
        }

        if (!Array.isArray(this.AllTanks)) {
            console.warn(`[TankEditor]: "${this.keyName}" is not an array`);
            this.AllTanks = [];
        }

        this.backup = structuredClone(this.AllTanks);
    };
    //functions for the API
    isValidIndex(array, index) {
        return Number.isInteger(index) && index >= 0 && index < array.length;
    }
    //getters
    getAlltank_names() {
        let set = new Set();
        this.AllTanks.map(tank => set.add(tank.name));
        return Array.from(set);
    };
    getAllfolder_names() {
        const folders = new Set();

        this.AllTanks.forEach(tank => {
            if (tank.hasOwnProperty("editorFolder")) {
                folders.add(tank.editorFolder);
            }
        });

        return Array.from(folders);
    };
    getTanksByName(name) {
        return this.AllTanks.filter(tank => tank.name === name);
    };
    getTanksByFolder(folder_name) {
        const output = [];

        if (folder_name === '') {
            this.AllTanks.forEach((tank) => {
                if (!(tank.hasOwnProperty('editorFolder'))) output.push(tank);
            });
        } else {
            this.AllTanks.forEach((tank) => {
                if (tank.editorFolder === folder_name) output.push(tank);
            });
        }

        return output;
    };
    getTankKeyByName(name) {
        const tanks = this.getTanksByName(name);
        switch (tanks.length) {
            case 0:
                console.warn('[getTankKeyByName]: no Tanks with that name were found!');
                return '';
            case 1:
                return tanks[0].key;
            default:
                console.log(`
                %c[getTankKeyByName]: ${name} has duplicates. I will proceed to use the first result.
                If you don't want that to happen, DO NOT use this function!
                `, "color:red");
                return tanks[0].key;
        };
    };
    getTankByKey(key) {
        return this.AllTanks.find(tank => tank.key === key);
    };
    getTankStringByKey(key) { //get stringified Tank Information
        return JSON.stringify(this.getTankByKey(key));
    };
    getCompressedTankStringByKey(key) { //use the compression function from the source code
        return compress_tankDef(this.getTankStringByKey(key));
    };
    getTanksByFolder(folder_name) {
        const output = [];
        if (folder_name === '') { //tanks without a folder
            this.AllTanks.forEach((tank) => {
                if (!(tank.hasOwnProperty('editorFolder'))) output.push(tank)
            });
        } else { //tanks with a folder
            this.AllTanks.forEach((tank) => {
                if (tank.editorFolder === folder_name) output.push(tank)
            });
        }
        return output;
    };
    getDefinition(tank_key) {
        return this.getTankByKey(tank_key).advancedObjectDef;
    };
    getBarrels(tank_key) {
        return this.getDefinition(tank_key).barrels;
    };
    getBody(tank_key) {
        return this.getDefinition(tank_key).renderingLayers;
    };
    //setters
    redefineAllTanks() {
        try {
            this.AllTanks = JSON.parse(localStorage.getItem(this.keyName)) || [];
        } catch {
            console.warn(`[TankEditor]: failed to parse localStorage item "${this.keyName}"`);
            this.AllTanks = [];
        }

        if (!Array.isArray(this.AllTanks)) {
            console.warn(`[TankEditor]: "${this.keyName}" is not an array`);
            this.AllTanks = [];
        }

        this.createBackUp();
    };
    swapBarrels(tank_key, barrel_index1, barrel_index2) {
        const barrels = this.getBarrels(tank_key);


        if (!this.isValidIndex(barrels, barrel_index1)) {
            return console.warn(`swapping failed. ${barrel_index1} is invalid index`)
        }

        if (!this.isValidIndex(barrels, barrel_index2)) {
            return console.warn(`swapping failed. ${barrel_index2} is invalid index`);
        }

        const temp = barrels[barrel_index1];
        barrels[barrel_index1] = barrels[barrel_index2];
        barrels[barrel_index2] = temp;
    };
    addSyncedBarrel(tank_key, barrel_index) {
        const barrels = this.getBarrels(tank_key);

        if (!this.isValidIndex(barrels, barrel_index)) {
            return console.warn(`adding synced barrel failed. ${barrel_index} is invalid index`);
        }

        const synced_obj = barrels[barrel_index];
        barrels.push(synced_obj);
    };
    createBackUp() {
        this.backup = structuredClone(this.AllTanks);
    };
    loadBackUp() {
        this.AllTanks = structuredClone(this.backup);
    };
    renameFolder(old_folder_name, new_folder_name) {
        const tanks = this.getTanksByFolder(old_folder_name);

        tanks.forEach(tank => {
            if (new_folder_name === "") {
                delete tank.editorFolder;
            } else {
                tank.editorFolder = new_folder_name;
            }
        });
    };
    //helper methods
    saveChanges() { //save all changes you made to the AllTanks object (some changes are made inside the helper functions)
        localStorage.setItem(this.keyName, JSON.stringify(this.AllTanks));
    };
    deleteTank(tank_key) {
        const index = this.AllTanks.findIndex(tank => tank.key === tank_key);

        if (index === -1) {
            return console.warn(`deleting failed. Tank "${tank_key}" was not found`);
        }

        return this.AllTanks.splice(index, 1)[0];
    };
    bytesToKb(bytes) { //convert bytes to Kilobytes
        return bytes / 1024;
    };
    countTankBytes(tank_key) { //size of saved tank in bytes
        return new TextEncoder().encode(this.getCompressedTankStringByKey(tank_key)).length;
    };
    calculateOccupiedTankCapacity(tank_key) { //how much % of allowed tank size is occupied
        return this.countTankBytes(tank_key) / this.maxTankBytes;
    };
    printTankSize(tank_key) {
        const tank_name = this.getTankByKey(tank_key).name;
        const tanksize = this.countTankBytes(tank_key);
        const in_percent = this.calculateOccupiedTankCapacity(tank_key);
        console.log(`
        Your tank: ${tank_name} has reached the size of ${tanksize} bytes, or ${(this.bytesToKb(tanksize)).toFixed(2)} Kb
        [ %c◼ %c◼ %c◼ %c◼ %c◼ %c◼ %c◼ %c◼ %c◼ %c◼ %c] ${(in_percent * 100).toFixed(2)} / 100
        `, `color: ${in_percent > 0 ? 'green' : 'red'}`, `color: ${in_percent >= 0.1 ? 'green' : 'red'}`, `color: ${in_percent >= 0.2 ? 'green' : 'red'}`, `color: ${in_percent >= 0.3 ? 'green' : 'red'}`, `color: ${in_percent >= 0.4 ? 'green' : 'red'}`, `color: ${in_percent >= 0.5 ? 'green' : 'red'}`, `color: ${in_percent >= 0.6 ? 'green' : 'red'}`, `color: ${in_percent >= 0.7 ? 'green' : 'red'}`, `color: ${in_percent >= 0.8 ? 'green' : 'red'}`, `color: ${in_percent >= 0.9 ? 'green' : 'red'}`, "color:black");
    };
    isTankAllowed(tank_key) {
        /*
        So we want to check all the boundaries and limitations for tanks.
        */
        const layerLimit = this.getBody(tank_key).length <= this.maxLayers;
        const tankSize = this.countTankBytes(tank_key) <= this.maxTankBytes;
        return layerLimit && tankSize;
    };
    help() {
        console.log(`
!!! TANK EDITOR API HELP !!!

Important:
- Most functions use tank.key, NOT tank.name.
- Tank names are not unique.
- Changes are only made in memory until you run Tank_Editor.saveChanges().
- After saving, reload the page to see the changes in the game/editor.
- For detailed examples, run Tank_Editor.advancedHelp().
`);

        console.table({
            "Tank_Editor.help()": {
                Description: "Prints this short command overview"
            },
            "Tank_Editor.advancedHelp()": {
                Description: "Prints detailed examples and usage notes"
            },
            "Tank_Editor.isValidIndex(array, index)": {
                Description: "Checks if an index is valid for an array"
            },
            "Tank_Editor.getAlltank_names()": {
                Description: "Returns all unique tank names"
            },
            "Tank_Editor.getAllfolder_names()": {
                Description: "Returns all folder names"
            },
            "Tank_Editor.getTanksByName(name)": {
                Description: "Returns all tanks with the given name"
            },
            "Tank_Editor.getTanksByFolder(folder_name)": {
                Description: 'Returns all tanks inside a folder; use "" for tanks without a folder'
            },
            "Tank_Editor.getTankKeyByName(name)": {
                Description: "Returns a tank key by name; if duplicates exist, returns the first match"
            },
            "Tank_Editor.getTankByKey(tank_key)": {
                Description: "Returns one tank object by key"
            },
            "Tank_Editor.getTankStringByKey(tank_key)": {
                Description: "Returns one tank object as a JSON string"
            },
            "Tank_Editor.getCompressedTankStringByKey(tank_key)": {
                Description: "Returns one tank object as a compressed string"
            },
            "Tank_Editor.getDefinition(tank_key)": {
                Description: "Returns the tank's advancedObjectDef"
            },
            "Tank_Editor.getBarrels(tank_key)": {
                Description: "Returns the tank's barrels array"
            },
            "Tank_Editor.getBody(tank_key)": {
                Description: "Returns the tank's renderingLayers array"
            },
            "Tank_Editor.redefineAllTanks()": {
                Description: "Reloads tanks from localStorage and discards unsaved in-memory changes"
            },
            "Tank_Editor.swapBarrels(tank_key, barrel_index1, barrel_index2)": {
                Description: "Swaps two barrels by index"
            },
            "Tank_Editor.addSyncedBarrel(tank_key, barrel_index)": {
                Description: "Adds another reference to the same barrel object"
            },
            "Tank_Editor.createBackUp()": {
                Description: "Creates a backup of the current in-memory tank state"
            },
            "Tank_Editor.loadBackUp()": {
                Description: "Restores the last backup into memory"
            },
            "Tank_Editor.renameFolder(old_folder_name, new_folder_name)": {
                Description: 'Renames a folder; use "" as new name to remove the folder'
            },
            "Tank_Editor.saveChanges()": {
                Description: "Saves the current in-memory tanks to localStorage"
            },
            "Tank_Editor.deleteTank(tank_key)": {
                Description: "Deletes one tank by key; does not save automatically"
            },
            "Tank_Editor.bytesToKb(bytes)": {
                Description: "Converts bytes to kilobytes"
            },
            "Tank_Editor.countTankBytes(tank_key)": {
                Description: "Returns the compressed tank size in bytes"
            },
            "Tank_Editor.calculateOccupiedTankCapacity(tank_key)": {
                Description: "Returns how much of the byte limit is used"
            },
            "Tank_Editor.printTankSize(tank_key)": {
                Description: "Prints the tank size with a visual console bar"
            },
            "Tank_Editor.isTankAllowed(tank_key)": {
                Description: "Checks if the tank is within the layer and byte limits"
            }
        });
    }

    advancedHelp() {
        const printSection = (title, rows) => {
            console.group(title);
            console.table(rows);
            console.groupEnd();
        };

        console.log(`
!!! TANK EDITOR API ADVANCED HELP !!!

Important:
- Most functions use tank.key, NOT tank.name.
- Tank names are not unique. havre.io allows multiple tanks with the same name.
- Use Tank_Editor.getTankKeyByName(name) only if you are okay with using the first matching tank.
- Changes are only made in memory until you run Tank_Editor.saveChanges().
- After saving, reload the page to see the changes in the game/editor.
- Unsaved changes can be reverted with Tank_Editor.loadBackUp().
`);

        printSection("Getting tanks", {
            "Tank_Editor.getAlltank_names()": {
                Description: "Returns all unique tank names.",
                Example: "const names = Tank_Editor.getAlltank_names();"
            },
            "Tank_Editor.getAllfolder_names()": {
                Description: "Returns all folder names.",
                Example: "const folders = Tank_Editor.getAllfolder_names();"
            },
            "Tank_Editor.getTanksByName(name)": {
                Description: "Returns all tanks with the given name.",
                Example: 'const tanks = Tank_Editor.getTanksByName("Lag bomb");'
            },
            "Tank_Editor.getTanksByFolder(folder_name)": {
                Description: 'Returns all tanks inside a folder. Use "" for tanks without a folder.',
                Example: 'const tanks = Tank_Editor.getTanksByFolder("My Folder");'
            },
            "Tank_Editor.getTankKeyByName(name)": {
                Description: "Returns the key of the first tank with the given name. Warns if duplicates exist.",
                Example: 'const tank_key = Tank_Editor.getTankKeyByName("Lag bomb");'
            },
            "Tank_Editor.getTankByKey(tank_key)": {
                Description: "Returns one tank object by key.",
                Example: "const tank = Tank_Editor.getTankByKey(tank_key);"
            },
            "Tank_Editor.getTankStringByKey(tank_key)": {
                Description: "Returns one tank object as a JSON string.",
                Example: "const tank_string = Tank_Editor.getTankStringByKey(tank_key);"
            },
            "Tank_Editor.getCompressedTankStringByKey(tank_key)": {
                Description: "Returns one tank object as a compressed string.",
                Example: "const compressed = Tank_Editor.getCompressedTankStringByKey(tank_key);"
            }
        });

        printSection("Getting tank parts", {
            "Tank_Editor.getDefinition(tank_key)": {
                Description: "Returns the tank's advancedObjectDef.",
                Example: "const def = Tank_Editor.getDefinition(tank_key);"
            },
            "Tank_Editor.getBarrels(tank_key)": {
                Description: "Returns the tank's barrels array.",
                Example: "const barrels = Tank_Editor.getBarrels(tank_key);"
            },
            "Tank_Editor.getBody(tank_key)": {
                Description: "Returns the tank's renderingLayers array.",
                Example: "const body = Tank_Editor.getBody(tank_key);"
            }
        });

        printSection("Editing tanks", {
            "Tank_Editor.swapBarrels(tank_key, barrel_index1, barrel_index2)": {
                Description: "Swaps two barrels by index.",
                Example: "Tank_Editor.swapBarrels(tank_key, 0, 1);"
            },
            "Tank_Editor.addSyncedBarrel(tank_key, barrel_index)": {
                Description: "Adds another reference to the same barrel object.",
                Example: "Tank_Editor.addSyncedBarrel(tank_key, 0);"
            },
            "Tank_Editor.renameFolder(old_folder_name, new_folder_name)": {
                Description: 'Renames a folder for all tanks inside it. Use "" to remove the folder.',
                Example: 'Tank_Editor.renameFolder("Old Folder", "New Folder");'
            },
            "Tank_Editor.deleteTank(tank_key)": {
                Description: "Deletes one tank by key. Does not save automatically.",
                Example: "Tank_Editor.deleteTank(tank_key);"
            }
        });

        printSection("Size and limits", {
            "Tank_Editor.countTankBytes(tank_key)": {
                Description: "Returns the compressed tank size in bytes.",
                Example: "const bytes = Tank_Editor.countTankBytes(tank_key);"
            },
            "Tank_Editor.bytesToKb(bytes)": {
                Description: "Converts bytes to kilobytes.",
                Example: "const kb = Tank_Editor.bytesToKb(2048);"
            },
            "Tank_Editor.calculateOccupiedTankCapacity(tank_key)": {
                Description: "Returns how much of the byte limit is used. 0.5 means 50%.",
                Example: "const capacity = Tank_Editor.calculateOccupiedTankCapacity(tank_key);"
            },
            "Tank_Editor.printTankSize(tank_key)": {
                Description: "Prints the tank size with a visual console bar.",
                Example: "Tank_Editor.printTankSize(tank_key);"
            },
            "Tank_Editor.isTankAllowed(tank_key)": {
                Description: "Checks if the tank is within the layer and byte limits.",
                Example: "const allowed = Tank_Editor.isTankAllowed(tank_key);"
            }
        });

        printSection("Save and backup", {
            "Tank_Editor.saveChanges()": {
                Description: "Saves the current in-memory tanks to localStorage.",
                Example: "Tank_Editor.saveChanges();"
            },
            "Tank_Editor.createBackUp()": {
                Description: "Creates a backup of the current in-memory tank state.",
                Example: "Tank_Editor.createBackUp();"
            },
            "Tank_Editor.loadBackUp()": {
                Description: "Restores the last backup into memory. Does not save automatically.",
                Example: "Tank_Editor.loadBackUp();"
            },
            "Tank_Editor.redefineAllTanks()": {
                Description: "Reloads tanks from localStorage and discards unsaved in-memory changes.",
                Example: "Tank_Editor.redefineAllTanks();"
            }
        });

        printSection("Utility", {
            "Tank_Editor.help()": {
                Description: "Prints the short command overview.",
                Example: "Tank_Editor.help();"
            },
            "Tank_Editor.advancedHelp()": {
                Description: "Prints this detailed help output.",
                Example: "Tank_Editor.advancedHelp();"
            },
            "Tank_Editor.isValidIndex(array, index)": {
                Description: "Checks if an index is valid for an array.",
                Example: "const valid = Tank_Editor.isValidIndex(barrels, 0);"
            }
        });

        console.log(`
--------------------------------------------------
COMMON WORKFLOW EXAMPLE
--------------------------------------------------

const tank_key = Tank_Editor.getTankKeyByName("Lag bomb");

Tank_Editor.printTankSize(tank_key);

Tank_Editor.addSyncedBarrel(tank_key, 0);

if(Tank_Editor.isTankAllowed(tank_key)){
    Tank_Editor.saveChanges();
    console.log("Saved successfully. Reload the page to see changes.");
} else {
    Tank_Editor.loadBackUp();
    console.warn("Tank became too large, changes were reverted.");
}
`);
    };
};

window.Tank_Editor = new TankEditor();