TankEditor API

API made to easily modify your tanks with code

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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();