API made to easily modify your tanks with code
// ==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();