Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -724,9 +724,53 @@ export default defineConfig({
text: "Fundamentals",
items: [
{
text: "Disconnection Triggers",
text: "Overview",
link: "/gameplay/scripts/fundamentals/overview",
},
{
text: "Script Setup",
link: "/gameplay/scripts/fundamentals/script_setup",
},
{
text: "The game Object",
link: "/gameplay/scripts/fundamentals/game_object",
},
{
text: "Network Basics",
link: "/gameplay/scripts/fundamentals/network_basics",
},
{
text: "RPC Reference",
link: "/gameplay/scripts/fundamentals/rpc_reference",
},
{
text: "Entity and World State",
link: "/gameplay/scripts/fundamentals/entity_world_state",
},
{
text: "Timing, Ticks, and Intervals",
link: "/gameplay/scripts/fundamentals/timing",
},
{
text: "Input and UI Automation",
link: "/gameplay/scripts/fundamentals/input_ui",
},
{
text: "Safety and Anti-Disconnect",
link: "/gameplay/scripts/fundamentals/dc_triggers",
},
{
text: "Debugging Scripts",
link: "/gameplay/scripts/fundamentals/debugging",
},
{
text: "Script Structure and Best Practices",
link: "/gameplay/scripts/fundamentals/best_practices",
},
{
text: "Common Script Features",
link: "/gameplay/scripts/fundamentals/common_features",
},
],
collapsed: true,
},
Expand Down
160 changes: 160 additions & 0 deletions src/gameplay/scripts/fundamentals/best_practices.md

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quality of the writing in this article is not up to standard. Many details are obvious to the average scripter, few are actually related to zombs. I suggest a complete rewrite, maybe keep a section about built-in game handlers.

Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
---
title: Script Structure and Best Practices - zombs.io Wiki
head:
- - meta
- property: 'og:title'
content: Script Structure and Best Practices - zombs.io Wiki
- - meta
- name: description
content: >-
How to organise a zombs.io userscript, manage configuration, clean up
handlers, and keep your code auditable.
- - meta
- property: 'og:description'
content: >-
How to organise a zombs.io userscript, manage configuration, clean up
handlers, and keep your code auditable.
---
# Script Structure and Best Practices

## The standard userscript skeleton

```js
// ==UserScript==
// @name My Script
// @match *://zombs.io/*
// @match *://*.zombs.io/*
// @run-at document-idle
// @grant none
// ==/UserScript==

(function () {
"use strict";

// Wait until the game is live and we are in-world
const ready = setInterval(() => {
if (!game?.network || !game?.ui?.playerTick) return;
clearInterval(ready);
boot();
}, 250);

function boot() {
registerHandlers();
}

function registerHandlers() {
// Guard against double-registration across hot reloads
if (game.__myScript) return;
game.__myScript = true;

game.network.addPacketHandler(0, onTick);
game.network.addRpcHandler("Dead", onDead);
}

function onTick() {
const p = game.ui.playerTick;
if (!p) return;
// continuous logic here
}

function onDead() {
setTimeout(() => game.network.sendInput({ respawn: 1 }), 500);
}
})();
```

## Guarding against double-registration

`game.network` is **rebuilt on every page reload**. If you track "already registered" in `localStorage` or any persistent store, your handlers will silently not be registered after a reload. Track the flag on the `game` object itself — it resets with the game:

```js
// Wrong — persists across reloads
if (localStorage.getItem("myScript.hooked")) return;
localStorage.setItem("myScript.hooked", "1");

// Correct — resets when game reloads
if (game.__myScript) return;
game.__myScript = true;
```

## Separating state from behaviour

Keep mutable state (on/off flags, last-action timestamps, target UIDs) separate from the logic that uses it. This makes toggling features clean:

```js
const state = {
autoHeal: false,
lastHealAt: 0,
};

game.network.addPacketHandler(0, () => {
if (!state.autoHeal) return;
const now = Date.now();
if (now - state.lastHealAt < 400) return;
const p = game.ui.playerTick;
if (!p || p.health / p.maxHealth > 0.3) return;
game.network.sendRpc({ name: "EquipItem", itemName: "HealthPotion", tier: 1 });
state.lastHealAt = now;
});

// Toggle from elsewhere (UI button, keyboard shortcut, etc.)
state.autoHeal = true;
```

## One tick handler, not many

Each call to `addPacketHandler(0, fn)` registers an additional handler — they all fire every tick. Multiple small handlers are fine, but if your script has many features, consolidating them into one handler reduces overhead and makes execution order explicit:

```js
game.network.addPacketHandler(0, () => {
tickAutoHeal();
tickAutoUpgrade();
tickAHRC();
});
```

## Persisting settings across reloads

For settings that should survive a page reload, use `localStorage`:

```js
// Save
localStorage.setItem("myScript.autoHeal", "true");

// Load (with a default)
const autoHeal = localStorage.getItem("myScript.autoHeal") === "true";
```

Use a namespaced key (`scriptName.settingName`) to avoid colliding with the game's own storage.

## Cleaning up

If your script supports being turned off at runtime without a page reload, clean up your intervals and remove handlers where possible:

```js
// Intervals are easy to clean up
const timer = setInterval(doThing, 2000);
// later:
clearInterval(timer);
```

Packet and RPC handlers registered with `addPacketHandler` / `addRpcHandler` have no built-in remove mechanism — gate them with a flag inside the handler instead:

```js
let active = true;
game.network.addPacketHandler(0, () => {
if (!active) return;
// ...
});
// To "remove" it:
active = false;
```

## Keeping things auditable

Scripts that run automatically against a live game can cause real consequences (spending gold, selling buildings, sending chat). A few habits help:

- **Log significant actions.** `console.log` before sending an upgrade or sell RPC, at least during development.
- **Add dry-run modes.** A `dryRun` flag that skips `sendRpc` calls while still logging what would happen is useful for testing.
- **Throttle destructive actions.** Selling or upgrading buildings should have a rate limit even if the server would accept them faster — it makes mistakes easier to catch and interrupt.
- **Test on a fresh base.** Automate on a disposable game before running on a high-wave session.
138 changes: 138 additions & 0 deletions src/gameplay/scripts/fundamentals/common_features.md

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea behind this article is good, but the way to approach the idea is wrong. This page should serve as a guide to what features of the game (e.g. what RPC does what, what entity update provides, etc) each script rely on, not just a demonstration of what each script does.

Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: Common Script Features - zombs.io Wiki
head:
- - meta
- property: 'og:title'
content: Common Script Features - zombs.io Wiki
- - meta
- name: description
content: >-
Conceptual patterns behind the most common zombs.io script functions —
auto-heal, AHRC, auto-upgrade, auto-aim, and more.
- - meta
- property: 'og:description'
content: >-
Conceptual patterns behind the most common zombs.io script functions —
auto-heal, AHRC, auto-upgrade, auto-aim, and more.
---
# Common Script Features

These are the patterns behind the functions you see in most zombs.io scripts. Each one is chosen to demonstrate a different part of the game object — reading `playerTick`, iterating entities, using the renderer, listening for server events.

## Auto Heal

Demonstrates reading `playerTick` for health state and `inventory` to check item stock.

```js
let lastHealAt = 0;

game.network.addPacketHandler(0, () => {
const p = game.ui.playerTick;
if (!p) return;

// buy a potion if we don't have one and can afford it
if (!game.ui.inventory?.HealthPotion && p.gold >= 100)
game.network.sendRpc({ name: "BuyItem", itemName: "HealthPotion", tier: 1 });

// drink when below 30% health — throttle to once per 400ms to avoid burning the stack
const now = Date.now();
if (p.health / p.maxHealth <= 0.3 && now - lastHealAt > 400) {
game.network.sendRpc({ name: "EquipItem", itemName: "HealthPotion", tier: 1 });
lastHealAt = now;
}
});
```

## Auto Respawn

Demonstrates `addRpcHandler` — reacting to a named server event rather than polling every tick.

```js
game.network.addRpcHandler("Dead", () => {
// wait briefly for the respawn screen to appear before sending the input
setTimeout(() => game.network.sendInput({ respawn: 1 }), 500);
});
```

## AHRC (Auto Harvester Refuel & Collect)

Demonstrates entity iteration filtered by `model` and `partyId`, combined with multiple RPCs per entity.

```js
game.network.addPacketHandler(0, () => {
const myPartyId = game.ui.getPlayerPartyId();
const gold = game.ui.playerTick?.gold ?? 0;

for (const e of game.world.entities.values()) {
const t = e.targetTick;
// only act on your own live Harvesters
if (!t || t.model !== "Harvester" || t.partyId !== myPartyId || t.dead) continue;

// feed gold in to keep the harvester running
if (gold > 1)
game.network.sendRpc({ name: "AddDepositToHarvester", uid: t.uid, deposit: 1 });

// collect whatever it has produced
game.network.sendRpc({ name: "CollectHarvester", uid: t.uid });
}
});
```

## Auto Upgrade

Demonstrates using `entityClass` and `tier` from entity state to drive upgrade decisions.

```js
const TARGET_TIER = 8; // upgrade everything to max

game.network.addPacketHandler(0, () => {
const myPartyId = game.ui.getPlayerPartyId();

for (const e of game.world.entities.values()) {
const t = e.targetTick;
if (!t || t.entityClass !== "Building") continue;
if (t.partyId !== myPartyId || t.dead) continue; // only your own buildings
if ((t.tier ?? 1) >= TARGET_TIER) continue; // already at or above target
game.network.sendRpc({ name: "UpgradeBuilding", uid: t.uid });
}
});
```

## Auto Aim

Demonstrates `game.world.entities` for target selection, `game.renderer.worldToScreen` for coordinate conversion, and `game.inputManager.onMouseMoved` to move the aim.

```js
game.network.addPacketHandler(0, () => {
const me = game.ui.playerTick?.position;
if (!me) return;
const myPartyId = game.ui.getPlayerPartyId();

// find the nearest enemy player
let best = null, bestDist = Infinity;
for (const e of game.world.entities.values()) {
const t = e.targetTick;
if (!t?.position || t.dead) continue;
if (t.model !== "GamePlayer" || t.partyId === myPartyId) continue;
const d = Math.hypot(t.position.x - me.x, t.position.y - me.y);
if (d < bestDist) { bestDist = d; best = t; }
}

if (!best) return;

// convert the target's world position to screen coordinates and move the crosshair there
const screenPos = game.renderer.worldToScreen(best.position.x, best.position.y);
game.inputManager.onMouseMoved({ clientX: screenPos.x, clientY: screenPos.y });
});
```

## Grant Sell Permissions

Demonstrates `getPlayerPartyMembers()` — reading party state and acting on each member.

```js
for (const member of game.ui.getPlayerPartyMembers()) {
if (!member?.playerUid) continue;
game.network.sendRpc({ name: "SetPartyMemberCanSell", uid: member.playerUid, canSell: 1 });
}
```
Loading