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):

script.js — class Script constructor (excerpt)
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:

script.js — per-tier harvester cadence
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:

script.js — class Scripts (defaults, excerpt)
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:

script.js — setScriptToggle (excerpt)
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 …
    }
}

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):

script.js — createScriptMenu tab schema (excerpt)
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:

script.js — render action shape & persistence
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: autoheal
if (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:

script.js — onEntityUpdate: autoaim + autobow (condensed)
// 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:

script.js — onEntityUpdate: autotimeout
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:

script.js — depositAhrc / collectAhrc + driver
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.

ToggleWhat it doesRange gate
Auto BuildPlaces any building from the saved base that's currently missing, by replaying MakeBuilding from the inactiveRebuilder map.≤ 576px from the target spot
Auto UpgradeRe-issues UpgradeBuilding for anything below its saved tier, from inactiveReupgrader.≤ 768px
Upgrade AllUpgrades every building (except the Gold Stash) that isn't already tier 8.≤ 768px
Sell AllIssues 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%
script.js — onEntityUpdate: Tower Heal & UTH (excerpt)
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:

script.js — onEntityUpdate: autorefiller
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).
script.js — mover(): the shared auto-follow movement helper (condensed)
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 PetHealthPotion once the pet's HP drops below 70%.
  • Revive Pets — buys/equips PetRevive whenever 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:
script.js — autoevolvepets thresholds
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.