The In-Game Script
public/script.js — the ~4,900-line overlay that turns the modded client into Banshee.
script.js is loaded straight after the official client bundle and instantiates a single global,
window.ban = new Script(), at the very bottom of the file. Everything described on this page —
the settings menu, every automation toggle, the chat-command interpreter, even the in-browser multibox system
covered on the Multibox page — lives inside that one object.
Boot & wiring
Following the exact pattern documented in zombscontext/03-userscripts.md, the Script
constructor waits for window.game to be live, then hooks the three things every Banshee feature is
built on: a per-tick handler (opcode 0), an RPC handler (opcode 9), and an interception of the world-enter
event (opcode 4) plus the client's own sendRpc (used to read outgoing chat messages for the
command interpreter):
class Script {
constructor() {
// …
game.network.addPacketHandler(4, this.onEnterWorld.bind(this));
game.network.addPacketHandler(9, this.onRpc.bind(this));
game.network.addPacketHandler(0, this.onEntityUpdate.bind(this));
document.addEventListener("keydown", this.onKeyDown.bind(this));
document.addEventListener("keyup", this.onKeyUp.bind(this));
document.addEventListener("mousedown", this.onMouseDown.bind(this));
document.addEventListener("mouseup", this.onMouseUp.bind(this));
const originalSendRpc = game.network.sendRpc.bind(game.network);
game.network.sendRpc = (rpc) => {
this.onSendRpc(rpc); // chat-command interpreter hooks in here
return originalSendRpc(rpc);
};
this.scripts = new Scripts(); // every toggle, with its default value
this.harvesters = new Map();
this.rebuilder = new Map(); // !record / Auto Build saved-base state
this.reupgrader = new Map(); // !record / Auto Upgrade saved-tier state
// … inactiveRebuilder / inactiveReupgrader mirror these for "buildings currently missing/under-tier"
this.createScriptMenu();
this.enableDraggableSettingsMenu();
this.createVpsSessionPanel();
this.createZombsLeaderboardPanel();
this.installScoreLoggerRpcHandlers();
}
}
// …
window.ban = new Script();
That harvesterTicks table is worth calling out — it's the timing data behind AHRC
(Auto Harvester Resource Collector, see below): each harvester tier deposits and collects
on a different cadence as it gets more efficient at converting gold into wood/stone:
this.harvesterTicks = [
{ tick: 0, resetTick: 31, deposit: 0.4, tier: 1 },
{ tick: 0, resetTick: 29, deposit: 0.6, tier: 2 },
{ tick: 0, resetTick: 27, deposit: 0.7, tier: 3 },
{ tick: 0, resetTick: 24, deposit: 1, tier: 4 },
{ tick: 0, resetTick: 22, deposit: 1.2, tier: 5 },
{ tick: 0, resetTick: 20, deposit: 1.2, tier: 6 },
{ tick: 0, resetTick: 18, deposit: 2.4, tier: 7 },
{ tick: 0, resetTick: 16, deposit: 3, tier: 8 }
];
The toggle system
Every automation in Banshee is, at its core, a boolean flag on a single object. class Scripts just
declares ~38 of them with their defaults — only autoheal and autorespawn start
enabled, everything else starts off:
class Scripts {
constructor() {
this.autoheal = true;
this.autorespawn = true;
this.autobow = false;
this.autospear = false;
this.autoshield = false;
this.autoaim = false;
this.autotimeout = false;
this.autopetheal = false;
this.autopetpotion = false;
this.autorevivepets = false;
this.autoevolvepets = false;
this.autobuild = false;
this.autoupgrade = false;
this.upgradeall = false;
this.sellall = false;
this.uth = false;
this.towerheal = false;
this.autorefiller = false;
this.mousemove = false;
this.positionlock = false;
this.wasd = false;
this.autofollow = false;
this.ahrc = false;
this.chatspam = false;
this.clearchat = false;
this.showrss = false;
// … walls3x3 / walls5x5 / walls7x7 / walls9x9 / harvs4x4 / harvs8x8 / xkey / joindelay / sas …
}
}
Toggling one of these on is rarely just a flag flip, though — setScriptToggle(key, enabled)
is the chokepoint that handles all the bookkeeping a toggle needs: mutual exclusion between conflicting modes,
rebuilding the rebuild/upgrade maps, cleaning up DOM overlays, and resetting related counters:
setScriptToggle(key, enabled) {
this.scripts[key] = enabled;
// Wall/harvester "spam placement" toggles are mutually exclusive — turning
// one on turns the others off so MakeBuilding interception doesn't fight itself.
if (enabled && ["walls3x3", "walls5x5", "walls7x7", "walls9x9"].includes(key)) {
for (const other of ["walls3x3", "walls5x5", "walls7x7", "walls9x9"]) {
if (other !== key) this.scripts[other] = false;
}
}
if (enabled && ["harvs4x4", "harvs8x8"].includes(key)) {
for (const other of ["harvs4x4", "harvs8x8"]) {
if (other !== key) this.scripts[other] = false;
}
}
if (key === "autobuild") {
this.rebuilder.clear();
this.inactiveRebuilder.clear();
if (enabled && this.gs) {
// … repopulate from the currently saved base (see !record on the Commands page) …
}
}
if (key === "autoupgrade") {
this.reupgrader.clear();
this.inactiveReupgrader.clear();
// … same pattern, keyed by saved tiers instead of building types …
}
if (key === "showrss" && !enabled) {
// … remove the on-screen player-info overlay it created …
}
if (key === "mousemove" && !enabled) {
// … stop relaying cursor position to sockets …
}
}
Settings menu
createScriptMenu() defines the entire UI as a declarative schema — four tabs, each broken into
titled sections of [toggleKey, label] pairs. The renderer walks this structure to build the
actual DOM, so adding a new automation to the menu is just adding a row here (plus, of course, the logic that
backs it):
const tabs = [
{ id: "localhost", title: "Localhost", sections: [
{ title: "Defense", items: [
["autoheal", "Auto Heal"], ["autobow", "Auto Bow"], ["autospear", "Auto Spear"],
["autoshield", "Auto Shield"], ["autoaim", "Auto Aim"], ["autotimeout", "Auto Timeout"]
]},
{ title: "Pets", items: [
["autopetheal", "Pet Heal"], ["autopetpotion", "Buy Pet Potion"],
["autorevivepets", "Revive Pets"], ["autoevolvepets", "Evolve Pets"]
]},
{ title: "Base", items: [
["autobuild", "Auto Build"], ["autoupgrade", "Auto Upgrade"], ["upgradeall", "Upgrade All"],
["sellall", "Sell All"], ["uth", "UTH"], ["towerheal", "Tower Heal"], ["autorefiller", "Auto Refiller"]
]},
{ title: "Placement", items: [
["walls3x3", "3x3 Walls"], ["walls5x5", "5x5 Walls"], ["walls7x7", "7x7 Walls"], ["walls9x9", "9x9 Walls"],
["harvs4x4", "4x4 Harvesters"], ["harvs8x8", "8x8 Harvesters"]
]},
{ title: "Multibox", items: [
["mousemove", "Mouse Move"], ["positionlock", "Position Lock"], ["wasd", "WASD"], ["autofollow", "Auto Follow"]
]},
{ title: "Tools", items: [ /* chat spam, clear chat, show player info, … */ ] }
]},
{ id: "sessions", title: "Sessions", session: true, sections: [
/* mirrors Localhost, but every row is an [eXX, dXX] command pair relayed to a Session — see Chat Commands */
]},
{ id: "statistics", title: "Statistics", statistics: true, sections: [] },
{ id: "render", title: "Render", render: true, sections: [] }
];
The Sessions tab is the visual mirror of the Localhost tab — same categories, same labels —
but instead of flipping a local this.scripts[key] flag, each control sends an
e<cmd>/d<cmd> pair (e.g. eab/dab for Auto Build)
through user.sendMessage() to the attached Session Saver connection, which flips the equivalent
flag on the headless Bot. See Session Saver → command relay.
Render tab
The Render tab is a thin, persistent wrapper around the game's own FPS/graphics menu. getRenderActions()
returns a declarative list of { id, label, section, includes, excludes } entries that map a
friendly toggle to one or more of the underlying buttons in that menu — includes/excludes
let one Banshee toggle drive several native settings at once (or specifically avoid disturbing others). State
is persisted to localStorage.zombsRenderStates so your render preferences survive a reload:
getRenderActions() {
return [
{ id: "hideZombies", label: "Hide Zombies", section: "Entities",
includes: ["Zombie"], excludes: [] },
{ id: "hideProjectiles", label: "Hide Projectiles", section: "Entities",
includes: ["Arrow", "Cannonball", "Spear"], excludes: [] },
// … dozens more, covering players, pets, particles, terrain detail, UI panels …
];
}
getSavedRenderStates() {
try { return JSON.parse(localStorage.zombsRenderStates || "{}"); } catch { return {}; }
}
setSavedRenderState(id, on) {
const states = this.getSavedRenderStates();
states[id] = on;
localStorage.zombsRenderStates = JSON.stringify(states);
}
// toggleRenderActionById() flips the saved state and calls handleRenderAction(),
// which clicks through to the matching native FPS-menu button(s);
// applySavedRenderStates() replays everything on boot; syncRenderButtons()
// keeps the Banshee UI in sync if the native menu changes underneath it.
Defense automations
These all live inline inside the giant per-tick handler, onEntityUpdate(data) — the single
function that does almost everything in script.js (it runs ~20 times a second, on every
entity-update packet). A few representative excerpts:
Auto Heal — Auto Heal
"Auto heal your player and Sockets at a low HP." Watches health as a percentage, and when it dips to 20% or below, equips and buys a Health Potion (gated by a one-shot flag so it doesn't spam the RPC every tick):
script.js — onEntityUpdate: autohealif (this.scripts.autoheal && me.health > 0 && (me.health / me.maxHealth) * 100 <= 20) {
if (!this.healTimeout) {
this.healTimeout = true;
game.network.sendRpc({ name: "EquipItem", itemName: "HealthPotion", tier: 1 });
game.network.sendRpc({ name: "BuyItem", itemName: "HealthPotion", tier: 1 });
}
} else if (this.healTimeout) {
this.healTimeout = false;
}
Auto Bow / Auto Aim — Auto Bow Auto Aim
"Auto attack for your player and Sockets" / "Aimbot for your player and Sockets." Auto Aim
finds the nearest hostile party-less player via atan2 and continuously aims at them; Auto Bow
fires using whatever the equipped weapon needs — held mouseDown for melee weapons, or a
space press/release pulse for the Bow:
// autoaim: track the nearest enemy player
for (const e of game.world.entities.values()) {
const t = e.targetTick;
if (t.model !== "GamePlayer" || t.partyId === game.ui.playerPartyId || t.dead) continue;
const d = Math.hypot(t.position.x - me().x, t.position.y - me().y);
if (d < nearestDist) { nearestDist = d; nearest = t; }
}
if (this.scripts.autoaim && nearest) {
const yaw = Math.floor((Math.atan2(nearest.position.y - me().y, nearest.position.x - me().x) * 180 / Math.PI + 450) % 360);
game.network.sendInput({ mouseMoved: yaw });
}
// autobow: fire according to the equipped weapon
if (this.scripts.autobow) {
if (game.ui.playerTick.weaponName === "Bow") {
game.network.sendInput({ space: 0 });
game.network.sendInput({ space: 1 }); // pulse — Bow fires on press
} else {
game.network.sendInput({ mouseDown: aimYaw }); // melee weapons fire while held
}
}
Auto Timeout / AITO — Auto Timeout
"Auto buy Timeout for your player and Sockets. It can be used to perform AITO." — AITO stands for
Auto Infinite Timeout: buying the in-game Pause item repeatedly (it costs gold
and pauses the wave clock) lets a base sit indefinitely without taking damage. The check is purely economic —
buy it whenever you're not already paused and can afford the flat 10,000-gold price:
if (this.scripts.autotimeout && !me.isPaused && me.gold >= 10000) {
game.network.sendRpc({ name: "BuyItem", itemName: "Pause", tier: 1 });
}
The Sessions-tab equivalent ("It can be alternated between multiple Sessions to perform AITO") is
exactly the same check, running inside the headless Bot tick loop — see
Session Saver → the bot tick loop.
AHRC — Auto Harvester Resource Collector
"Auto collect from Harvesters." AHRC pairs neatly with the per-tier cadence table shown earlier: every
tick it advances each tier's counter, and on schedule either deposits a fresh batch of gold into matching
harvesters or collects their finished output — entirely state-machine driven, no positional checks needed
because AddDepositToHarvester/CollectHarvester work at any range:
depositAhrc(tickInfo) {
this.harvesters.forEach((h) => {
if (h.tier !== tickInfo.tier) return;
game.network.sendRpc({ name: "AddDepositToHarvester", uid: h.uid, deposit: tickInfo.deposit });
});
}
collectAhrc(tickInfo) {
this.harvesters.forEach((h) => {
if (h.tier !== tickInfo.tier) return;
game.network.sendRpc({ name: "CollectHarvester", uid: h.uid });
});
}
// driven from onEntityUpdate, once per harvester tier:
if (this.scripts.ahrc) {
this.harvesterTicks.forEach((t) => {
t.tick++;
if (t.tick >= t.resetTick) { t.tick = 0; this.depositAhrc(t); }
if (t.tick === 1) this.collectAhrc(t);
});
}
Base automations
These six toggles automate the slowest part of the game loop — building, upgrading, repairing, and tearing
down a base — and they all key off positions stored relative to the Gold Stash
((building.x - stash.x) / 24), so a saved base can be replayed at any new stash location. Saving
happens via !record (see Chat Commands); these toggles consume
what it produces.
| Toggle | What it does | Range gate |
|---|---|---|
| Auto Build | Places any building from the saved base that's currently missing, by replaying MakeBuilding from the inactiveRebuilder map. | ≤ 576px from the target spot |
| Auto Upgrade | Re-issues UpgradeBuilding for anything below its saved tier, from inactiveReupgrader. | ≤ 768px |
| Upgrade All | Upgrades every building (except the Gold Stash) that isn't already tier 8. | ≤ 768px |
| Sell All | Issues DeleteBuilding for everything except the Gold Stash. | ≤ 1152px |
| UTH Upgrade Tower Health | "Auto upgrade buildings at a low HP" — preemptively upgrades a building once its HP drops below 30%, so it gains a bigger health pool before it dies. | ≤ 768px, HP ≤ 30% |
| Tower Heal | "Cast a Heal Spell on buildings at a low HP. This is NOT the glitchy Tower Heal from 2018/2019" — casts HealTowersSpell on damaged towers (≤ 30% HP) within range. A direct callout to the patched Tower Heal exploit covered in zombscontext/01-game.md. | ≤ 1000px, HP ≤ 30% |
if (this.scripts.towerheal && healTowerSet.has(building.type)) {
const hpPct = entity.targetTick.health / entity.targetTick.maxHealth * 100;
if (hpPct <= 30 && distanceTo(building) <= 1000) {
game.network.sendRpc({ name: "CastSpell", spell: "HealTowersSpell", x: building.x, y: building.y, tier: 1 });
}
}
if (this.scripts.uth) {
const hpPct = entity.targetTick.health / entity.targetTick.maxHealth * 100;
if (hpPct <= 30 && building.tier < this.gs.tier && distanceTo(building) <= 768) {
game.network.sendRpc({ name: "UpgradeBuilding", uid: building.uid });
}
}
Auto Refiller
"Auto fill the server with Sockets until it's full at 40 players. During the Night, 1-5 Sockets are sent on
player count change." A neat trick: rather than running a separate scheduler, it just spawns a fresh
Alt straight from the per-tick handler whenever it's nighttime, the player-count tracker ticks
over, and the server isn't already full — turning population management into a side effect of the normal
game loop:
if (this.scripts.autorefiller && !data.isDay && data.tick === 0 && this.players < 40) {
new Alt();
}
Multibox tab
These four toggles are how you actually drive a crowd of in-browser Alts as one unit —
full mechanics on the Multibox page:
- Mouse Move — "Auto move your Sockets to your mouse cursor"; relays the cursor's screen-relative position to every socket as an 8-direction movement vector each tick.
- Position Lock — "Stick your Sockets to a set point with your mouse cursor"; sockets path toward (and hold) a fixed world coordinate rather than following the live cursor.
- WASD — "Copy your player's WASD movement on your Sockets. Mouse Move should be turned off to use this"; mirrors your raw key state onto every socket — explicitly mutually exclusive with Mouse Move.
- Auto Follow — "Auto follow an enemy player by copying their WASD movement"; computes a yaw toward the tracked enemy and drives
mover()with it (see below).
mover(e, uid) {
// e: the tracked target's position; uid: which socket (or "me") to drive
const yaw = Math.floor((Math.atan2(e.y - pos.y, e.x - pos.x) * 180 / Math.PI + 450) % 360);
const rounded = Math.round(yaw / 45) * 45 % 360;
const packet = {
up: [0, 45, 315].includes(rounded) ? 1 : 0,
down: [135, 180, 225].includes(rounded) ? 1 : 0,
right: [45, 90, 135].includes(rounded) ? 1 : 0,
left: [225, 270, 315].includes(rounded) ? 1 : 0,
};
// … sent either as a real input (for the player) or relayed through the
// socket's own connection (for an Alt) …
}
Pets
Banshee always equips PetCARL (the combat pet) for both the player and every socket/session —
consistent with the recommendation in zombscontext/01-game.md that a stationary farming/defense
bot wants C.A.R.L., not Woody, since C.A.R.L. stays put and only fights after you do. The pet automations
layer on top of that:
- Pet Heal — buys/equips
PetHealthPotiononce the pet's HP drops below 70%. - Revive Pets — buys/equips
PetRevivewhenever the pet has been "activated" (i.e. has actually spawned once). - Evolve Pets — token-gated tier progression. The pet's "level" is derived from its raw experience (
experience / 100 + 1), and each tier requires both a level threshold and enough boss tokens:
const petLevel = pet.experience / 100 + 1;
if (petLevel >= 9 && pet.tier < 2 && me.token >= 100) buyTier(2);
else if (petLevel >= 17 && pet.tier < 3 && me.token >= 100) buyTier(3);
else if (petLevel >= 25 && pet.tier < 4 && me.token >= 100) buyTier(4);
else if (petLevel >= 33 && pet.tier < 5 && me.token >= 100) buyTier(5);
else if (petLevel >= 49 && pet.tier < 6 && me.token >= 200) buyTier(6);
else if (petLevel >= 65 && pet.tier < 7 && me.token >= 200) buyTier(7);
else if (petLevel >= 97 && pet.tier < 8 && me.token >= 300) buyTier(8);
This exact ladder reappears, byte for byte, inside the headless Bot.onRpcUpdateHandler's
DayCycle case in zombsSessions.js — further evidence that Sessions are meant to be
a headless mirror of the in-browser automations, not a different feature set.