diff --git a/.vitepress/config.mts b/.vitepress/config.mts index e6c8b85..43fb5a6 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -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, }, diff --git a/src/gameplay/scripts/fundamentals/best_practices.md b/src/gameplay/scripts/fundamentals/best_practices.md new file mode 100644 index 0000000..c05856b --- /dev/null +++ b/src/gameplay/scripts/fundamentals/best_practices.md @@ -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. diff --git a/src/gameplay/scripts/fundamentals/common_features.md b/src/gameplay/scripts/fundamentals/common_features.md new file mode 100644 index 0000000..fb3ddfa --- /dev/null +++ b/src/gameplay/scripts/fundamentals/common_features.md @@ -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 }); +} +``` diff --git a/src/gameplay/scripts/fundamentals/debugging.md b/src/gameplay/scripts/fundamentals/debugging.md new file mode 100644 index 0000000..d69e030 --- /dev/null +++ b/src/gameplay/scripts/fundamentals/debugging.md @@ -0,0 +1,136 @@ +--- +title: Debugging Scripts - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Debugging Scripts - zombs.io Wiki + - - meta + - name: description + content: >- + Techniques for logging, inspecting live state, monitoring network + traffic, and diagnosing failures in zombs.io scripts. + - - meta + - property: 'og:description' + content: >- + Techniques for logging, inspecting live state, monitoring network + traffic, and diagnosing failures in zombs.io scripts. +--- +# Debugging Scripts + +## Inspecting live state in the console + +The console is your first tool. While in-game, you can read any value directly: + +```js +game.ui.playerTick // your full character state +game.ui.playerTick.gold // just your gold +game.ui.inventory // what you're holding +game.world.entities.size // how many entities are loaded +game.ui.getPlayerPartyMembers() // party member list +``` + +To explore an object interactively, just type its path and press Enter — DevTools renders it as a collapsible tree. + +To watch a value continuously without writing a script: + +```js +// run this in the console — logs your gold every second +setInterval(() => console.log("gold:", game.ui.playerTick?.gold), 1000); +``` + +## Logging from a tick handler + +Inside a tick handler, avoid `console.log` on every tick — it will flood the console and slow the page down. Log only on change: + +```js +let lastGold = null; +game.network.addPacketHandler(0, () => { + const gold = game.ui.playerTick?.gold; + if (gold !== lastGold) { + console.log("gold changed:", lastGold, "→", gold); + lastGold = gold; + } +}); +``` + +## Monitoring RPCs + +To see every RPC the server sends you, intercept the handler map: + +```js +const _addRpcHandler = game.network.addRpcHandler.bind(game.network); +game.network.addRpcHandler = (name, fn) => { + _addRpcHandler(name, (data) => { + console.log("[rpc in]", name, data); + fn(data); + }); +}; +``` + +Or more simply, just register a catch-all for a specific RPC you are investigating: + +```js +game.network.addRpcHandler("LocalBuilding", (data) => { + console.log("LocalBuilding", JSON.stringify(data)); +}); +``` + +## Checking entity state + +To look at a specific entity by UID: + +```js +game.world.entities.get(someUid)?.targetTick +``` + +To list all entities of a given model: + +```js +[...game.world.entities.values()] + .map(e => e.targetTick) + .filter(t => t?.model === "Harvester") +``` + +## Diagnosing "my script does nothing" + +Work through this list in order: + +1. **Is `game.ui.playerTick` non-null?** If it is `null`, you are not in-world yet. Your tick handler is firing but all state reads return `null`. + +2. **Did you register the handler before or after entering the world?** Handlers registered before entering the world survive the transition. Handlers that depend on in-world state must still guard on `playerTick`. + +3. **Did the page reload and lose your handler?** `game.network` is rebuilt on every reload. If you stored an "already hooked" flag in `localStorage`, your handler was not re-registered. Store it on the `game` object instead: + ```js + if (game.__myHook) return; + game.__myHook = true; + game.network.addPacketHandler(0, myFn); + ``` + +4. **Is the RPC name spelled correctly?** RPC names are case-sensitive. `"UpgradeBuilding"` works; `"upgradebuilding"` does not. + +5. **Are you getting disconnected silently?** Add a close handler to find out: + ```js + game.network.addCloseHandler(() => console.warn("disconnected!")); + ``` + If this fires right after your RPC, check [Safety and Anti-Disconnect Practices](/gameplay/scripts/fundamentals/dc_triggers). + +6. **Are your coordinates in world space?** Entity positions and `MakeBuilding` use world coordinates. Screen pixel coordinates will place buildings in the wrong location or miss targets entirely. + +## DevTools Sources panel + +For longer scripts, use the **Sources** panel in DevTools: + +- **Snippets** — paste your script, run with `Ctrl+Enter`. Persists across page loads. +- **Breakpoints** — click the line number in a snippet to set a breakpoint. Execution will pause there and let you inspect all local variables. +- **Call stack** — when paused, the call stack shows exactly how the code got to that point. + +## Checking if a building UID is valid + +A stale UID (from a building that has since been destroyed) will cause `UpgradeBuilding` to silently fail: + +```js +const entity = game.world.entities.get(uid); +if (!entity || entity.targetTick?.dead) { + console.warn("building", uid, "is dead or gone"); +} +``` diff --git a/src/gameplay/scripts/fundamentals/entity_world_state.md b/src/gameplay/scripts/fundamentals/entity_world_state.md new file mode 100644 index 0000000..eeeb8ce --- /dev/null +++ b/src/gameplay/scripts/fundamentals/entity_world_state.md @@ -0,0 +1,209 @@ +--- +title: Entity and World State - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Entity and World State - zombs.io Wiki + - - meta + - name: description + content: >- + How to read player data, entity positions, building health, and UIDs + from the live world in a zombs.io script. + - - meta + - property: 'og:description' + content: >- + How to read player data, entity positions, building health, and UIDs + from the live world in a zombs.io script. +--- +# Entity and World State + +State in zombs.io lives in two places: `game.ui` (your character, inventory, party, buildings) and `game.world.entities` (every visible entity on the map). Understanding both is essential for any script beyond a simple one-liner. + +## Reading your own state — `game.ui.playerTick` + +`game.ui.playerTick` is the per-tick snapshot of your own character. It is `null` when you are not in-world. + +```js +const p = game.ui.playerTick; +if (!p) return; // not yet in-world + +p.position // { x, y } — your world coordinates +p.gold +p.wood +p.stone +p.token +p.health +p.maxHealth +p.wave // current wave number +p.dead // true if you are currently dead +``` + +Other useful `game.ui` reads: + +```js +game.ui.getPlayerPartyId() // your numeric party ID +game.ui.getPlayerPartyMembers() // [{ playerUid, canSell, ... }, ...] +game.ui.getPlayerPartyShareKey() // "XXXXXXXXXXXXXXXXXXXX" +game.ui.inventory // { Bow: { tier }, Pickaxe: { tier }, ... } +game.ui.buildings // { [uid]: { uid, type, tier, x, y, dead } } +``` + +## The entity map — `game.world.entities` + +`game.world.entities` is a standard JavaScript `Map` containing every entity the client currently knows about. This includes players, zombies, buildings, resources (trees, stones), and neutral camps. + +```js +game.world.entities // Map +game.world.entities.size // number of entities currently loaded +game.world.entities.get(uid) // look up one entity by UID +game.world.entities.values() // iterate all +``` + +### Entity shape + +Each entry is an entity object with two tick snapshots: + +```js +const entity = game.world.entities.get(uid); +entity.uid // entity's unique ID (number) +entity.targetTick // what the server says the entity is right now +entity.fromTick // the previous tick state (used for interpolation) +``` + +`targetTick` is the one you read in scripts: + +```js +const t = entity.targetTick; +t.uid +t.model // string — see model names below +t.entityClass // "GamePlayer" | "Building" | "Npc" +t.position // { x, y } +t.dead // boolean +t.partyId // which party owns this entity +t.tier // building/zombie tier (1–8 for buildings) +t.health +t.maxHealth +t.wood // on Harvesters: stored wood +t.stone // on Harvesters: stored stone +t.harvestMax // on Harvesters: storage cap +``` + +### Model names + +The `model` field identifies what kind of entity it is: + +| `model` value | Entity | +| :--- | :--- | +| `"GamePlayer"` | Another player | +| `"Tree"` | Tree (resource) | +| `"Stone"` | Stone (resource) | +| `"GoldStash"` | Gold stash building | +| `"GoldMine"` | Gold mine building | +| `"Harvester"` | Harvester building | +| `"Wall"` | Wall | +| `"Door"` | Door | +| `"SlowTrap"` | Slow trap | +| `"ArrowTower"` | Arrow tower | +| `"CannonTower"` | Cannon tower | +| `"BombTower"` | Bomb tower | +| `"MagicTower"` | Mage tower | +| `"MeleeTower"` | Melee tower | +| `"NeutralTier1"` | Neutral camp demon | + +Zombies use their own model strings and have `entityClass === "Npc"`. + +## Common filtering patterns + +### Find the nearest tree and stone + +```js +const me = game.ui.playerTick?.position; +if (!me) return; // bail if not in-world yet + +let nearestTree = null, nearestStone = null; +let treeDist = Infinity, stoneDist = Infinity; + +for (const entity of game.world.entities.values()) { + const t = entity.targetTick; + if (!t?.position) continue; // skip entities without a known position + + const d = Math.hypot(t.position.x - me.x, t.position.y - me.y); + + // track the closest of each type independently + if (t.model === "Tree" && d < treeDist) { treeDist = d; nearestTree = t; } + if (t.model === "Stone" && d < stoneDist) { stoneDist = d; nearestStone = t; } +} +``` + +### Find all enemy players + +```js +const myPartyId = game.ui.getPlayerPartyId(); // your party's numeric ID + +const enemies = []; +for (const entity of game.world.entities.values()) { + const t = entity.targetTick; + if (!t) continue; + // model "GamePlayer" covers all players; partyId check excludes your own party + if (t.model === "GamePlayer" && t.partyId !== myPartyId && !t.dead) + enemies.push(t); +} +``` + +### Find all zombies + +```js +const zombies = []; +for (const entity of game.world.entities.values()) { + const t = entity.targetTick; + // all zombies and neutral demons share entityClass "Npc" + if (t?.entityClass === "Npc" && !t.dead) + zombies.push(t); +} +``` + +### Find your own buildings + +```js +const myPartyId = game.ui.getPlayerPartyId(); + +const myBuildings = []; +for (const entity of game.world.entities.values()) { + const t = entity.targetTick; + // entityClass "Building" covers all placed structures; partyId scopes to yours + if (t?.entityClass === "Building" && t.partyId === myPartyId && !t.dead) + myBuildings.push(t); +} +``` + +### Get your Gold Stash + +```js +const myPartyId = game.ui.getPlayerPartyId(); + +// spread into an array to use .find() — the Map itself doesn't have it +const stash = [...game.world.entities.values()] + .map(e => e.targetTick) + .find(t => t?.model === "GoldStash" && t.partyId === myPartyId); +``` + +## Your own UID + +```js +game.world.myUid // your entity's UID (number) +``` + +Useful for filtering yourself out of entity loops: + +```js +if (t.uid === game.world.myUid) continue; +``` + +## Coordinate conversion + +Entity positions are in **world coordinates**. To get the screen position of an entity (for example, to position a DOM overlay or aim at it): + +```js +const screenPos = game.renderer.worldToScreen(t.position.x, t.position.y); +// screenPos.x and screenPos.y are CSS pixel coordinates +``` diff --git a/src/gameplay/scripts/fundamentals/game_object.md b/src/gameplay/scripts/fundamentals/game_object.md new file mode 100644 index 0000000..b2355ed --- /dev/null +++ b/src/gameplay/scripts/fundamentals/game_object.md @@ -0,0 +1,192 @@ +--- +title: The game Object - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: The game Object - zombs.io Wiki + - - meta + - name: description + content: >- + A practical guide to window.game — the single global that every zombs.io + script reads and drives. + - - meta + - property: 'og:description' + content: >- + A practical guide to window.game — the single global that every zombs.io + script reads and drives. +--- +# The `game` Object + +Every script starts here. The game client exposes a single global called `game` that holds every subsystem. You can access it by name directly in the console or in a script — no `window.` prefix required. + +```js +game // the live Game instance +window.game // same thing +window.Game.currentGame // also the same thing +``` + +Open the browser console while on zombs.io and type `game` to explore the live object tree yourself. + +## Top-level structure + +`game` contains three categories of keys: + +| Category | Pattern | Example | +| :--- | :--- | :--- | +| **Subsystem instances** | plain name | `game.network`, `game.ui`, `game.world` | +| **Subsystem classes** | ends with `Type` | `game.networkType`, `game.uiType` | +| **Misc** | — | `game.options`, `game._events` | + +The `*Type` keys are class references — they let you construct new instances of subsystems, but scripts almost never need them. + +## Subsystems + +### `game.network` + +The WebSocket adapter. This is how scripts send actions to the server and listen for responses. + +Key methods: + +| Method | Use | +| :--- | :--- | +| `sendRpc(data)` | Send an RPC (an action) — `data.name` + arguments | +| `sendInput(data)` | Send a movement / aim / attack input | +| `addRpcHandler(name, fn)` | Listen for a named server event (e.g. `"Dead"`, `"DayCycle"`) | +| `addPacketHandler(opcode, fn)` | Listen by raw opcode — opcode `0` fires every entity-update tick, making it the standard game loop hook | +| `addEntityUpdateHandler(fn)` | Shorthand for the opcode `0` tick | + +See [Network Basics](/gameplay/scripts/fundamentals/network_basics) and the [RPC Reference](/gameplay/scripts/fundamentals/rpc_reference) for full usage. + +### `game.ui` + +All readable game state lives here. This is where scripts get the player's gold, health, inventory, buildings, and party. + +::: warning +`game.ui.playerTick` is `null` until you have entered the world (picked a name and spawned). Always gate on it before reading player state. +::: + +**Commonly used properties:** + +| Property | Type | What it holds | +| :--- | :--- | :--- | +| `playerTick` | object \| null | Per-tick snapshot of your character (position, gold, health, wave, etc.) | +| `buildings` | object | Map of your party's placed buildings by UID | +| `inventory` | object | Your held items (`Bow`, `Pickaxe`, `HealthPotion`, etc.) | +| `parties` | object | All parties the client knows about — both open and non-open | +| `components` | object | Every live UI component by name (e.g. `game.ui.components.Chat`) | + +**Useful getter methods:** + +```js +game.ui.getPlayerTick() // same as game.ui.playerTick +game.ui.getPlayerPartyId() // your numeric party ID +game.ui.getPlayerPartyMembers() // array of { playerUid, canSell, ... } +game.ui.getPlayerPartyShareKey() // your 20-char party share key +game.ui.getPlayerPartyLeader() // UID of the party leader +game.ui.getInventory() // same as game.ui.inventory +game.ui.getBuildings() // same as game.ui.buildings +``` + +**`playerTick` fields** (when in-world): + +```js +const p = game.ui.playerTick; +p.position // { x, y } — world coordinates +p.gold +p.wood +p.stone +p.token +p.health +p.maxHealth +p.wave // current wave number +``` + +### `game.world` + +The live world state — entities, your own UID, and the renderer. + +| Property | What it is | +| :--- | :--- | +| `entities` | `Map` of all visible entities — iterate with `.values()` | +| `myUid` | Your entity's UID (number) | +| `inWorld` | `true` once you have spawned | +| `replicator` | Tick interpolation data (`replicator.msInThisTick`) | +| `localPlayer` | Your local player object | + +**Iterating entities:** + +```js +for (const entity of game.world.entities.values()) { + const t = entity.targetTick; + if (!t || !t.position) continue; + // t.model, t.entityClass, t.position, t.health, t.partyId, t.dead, t.uid ... +} +``` + +See [Entity and World State](/gameplay/scripts/fundamentals/entity_world_state) for the full entity shape and filtering patterns. + +### `game.renderer` + +The PIXI renderer. Handles coordinate conversion between world space and screen space. + +::: info +`game.renderer` and `game.world.renderer` are the **same object**. +::: + +**Coordinate conversion:** + +```js +game.renderer.worldToScreen(x, y) +``` +Converts a world position into CSS pixel coordinates relative to the browser window. Use this when you want to place a DOM element at a world position, or when you need the screen coordinates of an entity to pass to `inputManager.onMouseMoved` for aim scripts. + +```js +game.renderer.screenToWorld(clientX, clientY) +``` +The reverse — converts CSS pixel coordinates (e.g. from a mouse event's `clientX`/`clientY`) into world coordinates. Useful when you want to know what world position the player clicked on. + +```js +game.renderer.worldToUi(x, y) +``` +Similar to `worldToScreen`, but returns coordinates relative to the game's internal UI overlay layer rather than the full browser window. Use this when positioning elements inside the game's own HUD rather than as raw DOM overlays. + +**PIXI layers** for drawing custom overlays: + +```js +game.renderer.ground // ground layer — rendered beneath entities +game.renderer.npcs // NPC / entity layer +game.renderer.players // player layer +``` + +### `game.inputManager` + +Handles raw keyboard and mouse state. The most common scripting use is `onMouseMoved`, which artificially moves the player's aim crosshair to a given screen position — the game responds exactly as if the player physically moved their mouse there: + +```js +// Point the crosshair at a world position +const screenPos = game.renderer.worldToScreen(targetX, targetY); +game.inputManager.onMouseMoved({ clientX: screenPos.x, clientY: screenPos.y }); +``` + +This is how auto-aim scripts work: they find a target in `game.world.entities`, convert its world position to screen coordinates, then call `onMouseMoved` to snap the aim to it every tick. + +### `game.options` + +Server/session metadata, set at load time: + +```js +game.options.stage // environment ("production", etc.) +game.options.servers // list of available servers +game.options.userGroup // user group / account tier +``` + +### Other subsystems + +| Key | Purpose | +| :--- | :--- | +| `game.assetManager` | Loads and caches game assets (images, audio) | +| `game.debug` | Debug utilities — mostly internal | +| `game.metrics` | Performance metrics | +| `game.platform` | Platform detection (browser, OS) | +| `game.inputPacketCreator` | Builds input packets from raw input state | +| `game.inputPacketScheduler` | Rate-limits input packets to the server tick | diff --git a/src/gameplay/scripts/fundamentals/input_ui.md b/src/gameplay/scripts/fundamentals/input_ui.md new file mode 100644 index 0000000..3d70c45 --- /dev/null +++ b/src/gameplay/scripts/fundamentals/input_ui.md @@ -0,0 +1,160 @@ +--- +title: Input and UI Automation - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Input and UI Automation - zombs.io Wiki + - - meta + - name: description + content: >- + How to send movement, aim, and attack inputs in zombs.io scripts, and + how to interact with in-game UI elements. + - - meta + - property: 'og:description' + content: >- + How to send movement, aim, and attack inputs in zombs.io scripts, and + how to interact with in-game UI elements. +--- +# Input and UI Automation + +Player actions — movement, aiming, attacking — are sent via `game.network.sendInput()`. These are distinct from RPCs: inputs represent continuous physical actions, RPCs represent discrete commands. + +## `sendInput` fields + +```js +game.network.sendInput(fields); +``` + +| Field | Type | Meaning | +| :--- | :--- | :--- | +| `up` / `down` / `left` / `right` | `0` or `1` | Movement keys. `1` means the key is held down, `0` means released. Send only the keys that changed — you don't need to include all four every time. | +| `mouseDown` | yaw `0–359` | Starts (and holds) a left-click attack. The value is the angle to swing toward: `0` = up/north, `90` = right/east, `180` = down/south, `270` = left/west. Must include `worldX`, `worldY`, and `distance` to reliably hit a target. | +| `mouseUp` | `1` | Releases the left click, stopping any held attack. | +| `mouseMoved` | yaw `0–359` | Updates the aim direction without clicking. Used to track where the player is looking. | +| `mouseMovedWhileDown` | yaw `0–359` | Same as `mouseMoved` but sent while the mouse button is held — allows tracking aim during a held attack. | +| `worldX`, `worldY` | number | The world coordinates of the cursor target. The server uses these to determine what the attack connects with. | +| `distance` | number | Distance in world units from the player's position to the cursor target. Sent alongside `worldX`/`worldY`. | +| `space` | `0` or `1` | The spacebar. `1` = pressed, `0` = released. Triggers the active ranged weapon (bow shot, bomb throw, spear lunge). | +| `respawn` | `1` | Sends the respawn request. Only meaningful when the player is dead. | + +## Movement + +Send only the directions that are active. Stop all movement by setting all directions to `0`: + +```js +// move right +game.network.sendInput({ right: 1 }); + +// move diagonally up-right +game.network.sendInput({ up: 1, right: 1 }); + +// stop +game.network.sendInput({ up: 0, down: 0, left: 0, right: 0 }); +``` + +**Direction → key mapping:** + +| Degrees | Keys | +| :---: | :--- | +| 0 (up) | `up: 1` | +| 45 | `up: 1, right: 1` | +| 90 (right) | `right: 1` | +| 135 | `down: 1, right: 1` | +| 180 (down) | `down: 1` | +| 225 | `down: 1, left: 1` | +| 270 (left) | `left: 1` | +| 315 | `up: 1, left: 1` | + +## Attacking — `mouseDown` + +`mouseDown` takes a **yaw** (0 = up, increases clockwise). It must also include `worldX`, `worldY`, and `distance` for the hit to register on a target: + +```js +function attackToward(targetX, targetY) { + const me = game.ui.playerTick.position; + const yaw = (Math.atan2(targetY - me.y, targetX - me.x) * 180 / Math.PI + 450) % 360; + const dist = Math.hypot(targetX - me.x, targetY - me.y); + game.network.sendInput({ + mouseDown: Math.round(yaw), + worldX: targetX | 0, + worldY: targetY | 0, + distance: dist | 0, + }); +} + +// release the attack +game.network.sendInput({ mouseUp: 1 }); +``` + +::: tip +A bare `sendInput({ mouseDown: yaw })` without `worldX`/`worldY`/`distance` will animate the swing but may not reliably connect with a target. +::: + +## Aiming without attacking + +To point at a world position without clicking (for example, to drive the game's built-in aim display), use `inputManager.onMouseMoved`: + +```js +const screenPos = game.renderer.worldToScreen(targetX, targetY); +game.inputManager.onMouseMoved({ clientX: screenPos.x, clientY: screenPos.y }); +``` + +## Weapon fire — `space` + +The `space` field triggers the active ranged weapon (bow, bomb, or spear). Toggle it off then on each tick to produce continuous fire: + +```js +game.network.addPacketHandler(0, () => { + game.network.sendInput({ space: 0 }); + game.network.sendInput({ space: 1 }); +}); +``` + +## Respawn + +```js +game.network.addRpcHandler("Dead", () => { + setTimeout(() => game.network.sendInput({ respawn: 1 }), 500); +}); +``` + +Or, if the respawn screen is visible, click the button directly: + +```js +const btn = document.querySelector("#hud-respawn > div > div > div > button:nth-child(3)"); +if (btn) btn.click(); +``` + +## UI element selectors + +Useful DOM selectors for interacting with the game's own UI (used by most scripts): + +| Selector | Element | +| :--- | :--- | +| `#hud-menu-settings` | Settings panel container — where most scripts mount their UI | +| `#hud-respawn` | Respawn screen | +| `.hud-chat-message` | Individual chat message elements | + +## Notifications + +`PopupOverlay` handles all in-game notifications. It supports simple hints, typed toasts, and confirmation dialogs: + +```js +const popup = game.ui.components.PopupOverlay; + +// Simple hint — appears briefly at the top of the screen +popup.showHint("message here"); + +// Typed toast — use "success", "error", "warning", or "info" +popup.showToast("message here", "success"); +popup.showToast("something went wrong", "error"); +popup.showToast("heads up", "warning"); +popup.showToast("just so you know", "info"); + +// Confirmation dialog — presents the player with Yes / No buttons +popup.showConfirm("Are you sure?", () => { + // called when the player clicks Yes +}, () => { + // called when the player clicks No (optional) +}); +``` diff --git a/src/gameplay/scripts/fundamentals/network_basics.md b/src/gameplay/scripts/fundamentals/network_basics.md new file mode 100644 index 0000000..6708358 --- /dev/null +++ b/src/gameplay/scripts/fundamentals/network_basics.md @@ -0,0 +1,119 @@ +--- +title: Network Basics - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Network Basics - zombs.io Wiki + - - meta + - name: description + content: >- + How zombs.io's binary WebSocket protocol works — packets, opcodes, RPCs, + and the split between client-known and server-authoritative state. + - - meta + - property: 'og:description' + content: >- + How zombs.io's binary WebSocket protocol works — packets, opcodes, RPCs, + and the split between client-known and server-authoritative state. +--- +# Network Basics + +zombs.io communicates over a **binary WebSocket**. All traffic goes through `game.network`, which encodes and decodes packets using a compact binary codec (`BinCodec`). As a scripter you rarely touch the codec directly — you call `sendRpc` or `sendInput` and the network layer handles the rest. + +## Packet types + +Each packet has a numeric **opcode** that identifies its type: + +| Opcode | Name | Direction | +| :---: | :--- | :--- | +| `0` | Entity update | Server → client (every tick) | +| `3` | Input | Client → server | +| `4` | Enter world | Both | +| `5` | MBF anti-bot challenge | Both | +| `6` | Enter world (phase 2) | Client → server | +| `7` | Ping | Both | +| `8` | — | **Sending this disconnects you instantly** | +| `9` | RPC | Both | + +Scripts work with two of these: **inputs** (opcode 3) and **RPCs** (opcode 9). + +## RPCs + +An **RPC** is a named action sent to the server. Examples: placing a building, buying an item, sending a chat message. Send one with: + +```js +game.network.sendRpc({ name: "RpcName", ...args }); +``` + +The `name` field selects the action; the remaining fields are its arguments. See the [RPC Reference](/gameplay/scripts/fundamentals/rpc_reference) for every common RPC with its exact argument shapes. + +The server also sends RPCs back to the client to push state changes. Listen for them with: + +```js +game.network.addRpcHandler("Dead", (data) => { + // called when the server tells you that you died +}); + +game.network.addRpcHandler("DayCycle", ({ isDay }) => { + // called when day/night flips +}); +``` + +Common server → client RPCs: + +| Name | When it fires | Key data | +| :--- | :--- | :--- | +| `DayCycle` | Day/night flip | `{ isDay }` | +| `Dead` | You died | — | +| `PartyShareKey` | On join / share key change | `{ partyShareKey }` | +| `LocalBuilding` | Your base changes | `{ response: [{uid, type, x, y, tier, dead}] }` | +| `Leaderboard` | Periodic leaderboard push | scores array | +| `BuildingShopPrices` | On enter world | full price table | +| `ItemShopPrices` | On enter world | full item price table | + +## Inputs + +An **input** represents player movement, aim, or weapon use. Send one with: + +```js +game.network.sendInput({ up: 1 }); +game.network.sendInput({ mouseDown: 90, worldX: 500, worldY: 300, distance: 200 }); +``` + +Inputs are rate-limited internally by `inputPacketScheduler` — only the latest state per tick is sent, so calling `sendInput` repeatedly in one tick doesn't flood the server. + +See [Input and UI Automation](/gameplay/scripts/fundamentals/input_ui) for the full input field reference. + +## The game loop — opcode 0 + +Opcode `0` fires **every entity-update tick** (approximately 20 times per second). Registering a handler for it gives you the closest thing to a game loop: + +```js +game.network.addPacketHandler(0, () => { + // runs ~20x per second, in sync with the server tick + const p = game.ui.playerTick; + if (!p) return; + // read state, send inputs, fire RPCs... +}); +``` + +`addEntityUpdateHandler` is a named shorthand for the same thing: + +```js +game.network.addEntityUpdateHandler(() => { /* same */ }); +``` + +::: tip +Use opcode `0` / `addEntityUpdateHandler` for anything that needs to run continuously — auto-aim, auto-farm, health checks. Use `addRpcHandler` for things that react to specific server events — respawn on death, act on day/night flip. +::: + +## Client vs server authority + +The client knows what the server last told it, interpolated smoothly between ticks. It does **not** simulate game logic — the server decides all outcomes (damage, deaths, resource gain, zombie movement). This means: + +- Reading `game.ui.playerTick.gold` gives you your gold as of the last server tick. +- Sending `UpgradeBuilding` does not immediately change the building's tier in `game.world.entities` — the server confirms it and the next entity-update tick reflects the change. +- You cannot cheat values by writing to client-side state. Scripts can only read what the server sent and send requests; they cannot override server decisions. + +## Packet size limit + +RPCs are encoded before sending. If an encoded RPC exceeds **256 bytes**, the server disconnects you. In practice the most common way to hit this is a chat message that is too long. See [Safety and Anti-Disconnect Practices](/gameplay/scripts/fundamentals/dc_triggers) for the full list of disconnection triggers. diff --git a/src/gameplay/scripts/fundamentals/overview.md b/src/gameplay/scripts/fundamentals/overview.md index e69de29..dfb66de 100644 --- a/src/gameplay/scripts/fundamentals/overview.md +++ b/src/gameplay/scripts/fundamentals/overview.md @@ -0,0 +1,49 @@ +--- +title: Scripting Fundamentals - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Scripting Fundamentals - zombs.io Wiki + - - meta + - name: description + content: >- + A ground-up guide to scripting in zombs.io — from running your first + snippet to writing structured userscripts that read game state and send + actions safely. + - - meta + - property: 'og:description' + content: >- + A ground-up guide to scripting in zombs.io — from running your first + snippet to writing structured userscripts that read game state and send + actions safely. +--- +# Scripting Fundamentals + +zombs.io has no official scripting API. Every script works by reading and driving `game` — a single global object that the game client exposes in the browser. Because the game is fully client-authoritative on its own state (the server is authoritative on outcomes), almost everything a script needs is reachable through that object. + +This section builds the knowledge from the ground up: how to run code in the first place, what the `game` object looks like, how the network layer works, and how to stay connected while doing it. Each article is self-contained but assumes the ones before it. + +## Articles in this section + +| Article | What you'll learn | +| :--- | :--- | +| [Script Setup](/gameplay/scripts/fundamentals/script_setup) | How to run code against the game — browser console, Tampermonkey userscripts, and DevTools snippets | +| [The `game` Object](/gameplay/scripts/fundamentals/game_object) | The structure of `game` and the subsystems every script touches | +| [Network Basics](/gameplay/scripts/fundamentals/network_basics) | Packets, opcodes, RPCs, and the difference between what the client knows and what the server decides | +| [RPC Reference](/gameplay/scripts/fundamentals/rpc_reference) | Every common RPC a script sends — chat, party, items, buildings, spells — with verified argument shapes | +| [Entity and World State](/gameplay/scripts/fundamentals/entity_world_state) | How to read player data, tower positions, health values, and UIDs out of the live world | +| [Timing, Ticks, and Intervals](/gameplay/scripts/fundamentals/timing) | The server tick, scheduling work, debouncing and throttling, and rate limits to respect | +| [Input and UI Automation](/gameplay/scripts/fundamentals/input_ui) | Sending movement, aim, and attack inputs; interacting with in-game menus | +| [Safety and Anti-Disconnect Practices](/gameplay/scripts/fundamentals/dc_triggers) | Actions that disconnect you and how to avoid them | +| [Debugging Scripts](/gameplay/scripts/fundamentals/debugging) | Logging, inspecting live state, monitoring network traffic, and diagnosing failures | +| [Script Structure and Best Practices](/gameplay/scripts/fundamentals/best_practices) | Organizing a script, managing configuration, cleaning up handlers, and keeping things auditable | +| [Common Script Features](/gameplay/scripts/fundamentals/common_features) | Conceptual patterns behind popular script functions — auto-heal, auto-farm, AHRC, base savers, and more | + +## Prerequisites + +These articles assume: +- Basic JavaScript (variables, functions, `setInterval`, Promises). +- A browser with DevTools (F12). Chrome or Firefox both work. +- You can reach [zombs.io](https://zombs.io/) and open the console while in-game. + +No prior zombs.io scripting experience is required. diff --git a/src/gameplay/scripts/fundamentals/rpc_reference.md b/src/gameplay/scripts/fundamentals/rpc_reference.md new file mode 100644 index 0000000..12a9bee --- /dev/null +++ b/src/gameplay/scripts/fundamentals/rpc_reference.md @@ -0,0 +1,172 @@ +--- +title: RPC Reference - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: RPC Reference - zombs.io Wiki + - - meta + - name: description + content: >- + Every common RPC a script sends — chat, party, items, buildings, and + spells — with verified argument shapes. + - - meta + - property: 'og:description' + content: >- + Every common RPC a script sends — chat, party, items, buildings, and + spells — with verified argument shapes. +--- +# RPC Reference + +All RPCs are sent with: + +```js +game.network.sendRpc({ name: "RpcName", ...args }); +``` + +Field names matter — use the exact names listed below. + +## Chat + +### `SendChatMessage` +```js +game.network.sendRpc({ + name: "SendChatMessage", + channel: "Local", + message: "hello" +}); +``` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `channel` | `"Local"` | Must be `"Local"` — other values are rejected by the codec | +| `message` | string | **Must be under 250 bytes** (not characters). Exceeding this disconnects you. | + +## Items + +### `BuyItem` +```js +game.network.sendRpc({ name: "BuyItem", itemName: "Bow", tier: 1 }); +``` + +### `EquipItem` +```js +game.network.sendRpc({ name: "EquipItem", itemName: "Bow", tier: 1 }); +``` + +| `itemName` values | | +| :--- | :--- | +| `"Pickaxe"` | Harvesting tool | +| `"Bow"` | Ranged weapon | +| `"Spear"` | Melee weapon | +| `"Bomb"` | AoE weapon | +| `"Shield"` | Extra HP | +| `"HealthPotion"` | Consumable heal | +| `"PetCARL"` | Combat pet | +| `"PetMiner"` | Harvester pet | +| `"HatHorns"` | Hat | + +::: warning +Do not send `EquipItem` for a pet immediately after `BuyItem` for the next tier. The server is still processing the evolution — sending equip while it is mid-evolve will disconnect you. Wait for the evolution to resolve first. +::: + +## Buildings + +### `MakeBuilding` +Places a new tier-1 building. +```js +game.network.sendRpc({ + name: "MakeBuilding", + type: "Wall", + x: 1000, + y: 1000, + yaw: 0 +}); +``` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `type` | string | Building type (see table below) | +| `x`, `y` | number | World coordinates | +| `yaw` | number | Rotation in degrees — only relevant for Melee Tower | + +| `type` values | | +| :--- | :--- | +| `"GoldStash"` | Core building | +| `"GoldMine"` | Passive gold | +| `"Harvester"` | Converts gold to wood/stone | +| `"Wall"` | Barrier | +| `"Door"` | Party-walkable barrier | +| `"SlowTrap"` | Slows enemies, always walkable | +| `"ArrowTower"` | Single-target ranged tower | +| `"CannonTower"` | AoE ranged tower | +| `"BombTower"` | Large AoE tower | +| `"MagicTower"` | Multi-projectile tower | +| `"MeleeTower"` | Directional close-range tower | + +### `UpgradeBuilding` +Upgrades a building one tier. You must be within range of the building. +```js +game.network.sendRpc({ name: "UpgradeBuilding", uid: buildingUid }); +``` + +The building's UID comes from `game.world.entities` or `game.ui.buildings`. Maximum tier is 8. + +## Harvesters + +### `AddDepositToHarvester` +Feeds gold into a Harvester so it starts producing wood/stone. +```js +game.network.sendRpc({ name: "AddDepositToHarvester", uid: harvesterUid, deposit: 1 }); +``` + +### `CollectHarvester` +Collects the produced wood/stone from a Harvester. +```js +game.network.sendRpc({ name: "CollectHarvester", uid: harvesterUid }); +``` + +## Party + +### `JoinPartyByShareKey` +Joins a party using its 20-character share key. +```js +game.network.sendRpc({ name: "JoinPartyByShareKey", partyShareKey: "XXXXXXXXXXXXXXXXXXXX" }); +``` + +Pass an empty string to leave your current party and start a solo one. + +### `SetPartyMemberCanSell` +Grants or revokes a party member's permission to sell buildings. +```js +game.network.sendRpc({ name: "SetPartyMemberCanSell", uid: memberUid, canSell: 1 }); +``` + +| `canSell` | Effect | +| :---: | :--- | +| `1` | Grant sell permission | +| `0` | Revoke sell permission | + +The member UID comes from `game.ui.getPlayerPartyMembers()` — each member object has a `playerUid` field. + +## Spells + +### `CastSpell` +```js +game.network.sendRpc({ name: "CastSpell", spell: "HealTowersSpell", tier: 1, x: 0, y: 0 }); +``` + +| Field | Notes | +| :--- | :--- | +| `spell` | Spell name (e.g. `"HealTowersSpell"`, `"TimeoutSpell"`) | +| `tier` | Must be `1` — higher tiers disconnect you | +| `x`, `y` | World position for targeted spells | + +## Respawn + +Respawn is **not** an RPC — it is an input packet: + +```js +game.network.sendInput({ respawn: 1 }); +``` + +Trigger it from an `addRpcHandler("Dead", ...)` callback. See [Input and UI Automation](/gameplay/scripts/fundamentals/input_ui). diff --git a/src/gameplay/scripts/fundamentals/script_setup.md b/src/gameplay/scripts/fundamentals/script_setup.md new file mode 100644 index 0000000..6c80476 --- /dev/null +++ b/src/gameplay/scripts/fundamentals/script_setup.md @@ -0,0 +1,89 @@ +--- +title: Script Setup - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Script Setup - zombs.io Wiki + - - meta + - name: description + content: >- + How to run JavaScript against zombs.io — browser console, Tampermonkey + userscripts, and DevTools snippets. + - - meta + - property: 'og:description' + content: >- + How to run JavaScript against zombs.io — browser console, Tampermonkey + userscripts, and DevTools snippets. +--- +# Script Setup + +There are three main ways to run JavaScript against zombs.io. Each has different trade-offs around persistence, convenience, and structure. + +## Browser Console + +The simplest method. Open DevTools with `F12`, go to the **Console** tab, and type directly. + +```js +game.ui.playerTick.wood // read your current wood +``` + +::: tip +Type `game` and press Enter to inspect the full object tree in the console. This is the fastest way to explore what's available. +::: + +The console is good for one-off commands and quick experiments, but nothing you type is saved — it's gone on reload. Use it to test snippets before putting them somewhere permanent. + +## DevTools Snippets + +Snippets are small scripts saved inside DevTools itself. They persist across reloads and are a good middle ground before committing to a full userscript. + +1. Open DevTools (`F12`) +2. Go to **Sources** → **Snippets** (you may need to click `>>` to find it) +3. Click **+ New snippet**, give it a name +4. Paste your code and press `Ctrl+Enter` to run it + +Snippets run once when you execute them — they don't auto-run on page load. If you need something to run automatically every time the page loads, use a userscript instead. + +## Userscripts (Tampermonkey / Violentmonkey) + +Userscripts are the standard method for anything persistent. They inject into the page automatically on load, so your script runs every time you open zombs.io without any manual steps. + +**Install a userscript manager:** +- [Tampermonkey](https://www.tampermonkey.net/) (Chrome, Firefox, Edge) +- [Violentmonkey](https://violentmonkey.github.io/) (Chrome, Firefox) + +Either works. Create a new script and use this as the base: + +```js +// ==UserScript== +// @name My zombs.io script +// @match *://zombs.io/* +// @match *://*.zombs.io/* +// @run-at document-idle +// @grant none +// ==/UserScript== + +(function () { + "use strict"; + + const ready = setInterval(() => { + if (!game?.network || !game?.ui?.playerTick) return; + clearInterval(ready); + main(); + }, 250); + + function main() { + // game is live and you are in-world — put your code here + } +})(); +``` + +### Why the `ready` loop? + +The `ready` loop is only necessary if your script might run before the player enters the game — which is exactly what happens with userscripts, since they inject at page load. `game` exists shortly after load, but `game.ui.playerTick` only exists once you have actually entered the world (picked a name and spawned). If you access it before then, it's `null`. + +The `setInterval` polls every 250ms and clears itself once both are present. Everything after that runs against a live, in-world game state. If you are running code manually from the console or a snippet while already in-game, you can skip the loop entirely and just call your function directly. + +::: warning +Do not store an "already hooked" flag in `localStorage` to skip re-registering handlers. `game.network` is rebuilt on every page reload, so a persisted flag will silently skip re-registration and your script will do nothing after reload. Store any such flags on the `game` object itself (e.g. `game.__myScript = true`) so they reset with the game. +::: diff --git a/src/gameplay/scripts/fundamentals/timing.md b/src/gameplay/scripts/fundamentals/timing.md new file mode 100644 index 0000000..3af79bf --- /dev/null +++ b/src/gameplay/scripts/fundamentals/timing.md @@ -0,0 +1,132 @@ +--- +title: Timing, Ticks, and Intervals - zombs.io Wiki +head: + - - meta + - property: 'og:title' + content: Timing, Ticks, and Intervals - zombs.io Wiki + - - meta + - name: description + content: >- + The server tick, scheduling script work, debouncing, throttling, and + rate limits to respect in zombs.io scripts. + - - meta + - property: 'og:description' + content: >- + The server tick, scheduling script work, debouncing, throttling, and + rate limits to respect in zombs.io scripts. +--- +# Timing, Ticks, and Intervals + +## The server tick + +The server updates the world approximately **20 times per second** (every ~50ms). Each update pushes a new entity-update packet (opcode `0`) to all connected clients. This is the heartbeat that scripts sync to. + +```js +// runs every server tick (~20x per second) +game.network.addPacketHandler(0, () => { + // your game loop logic here +}); +``` + +Input packets follow the same cadence — `inputPacketScheduler` only sends the latest input state once per tick, so calling `sendInput` multiple times in one tick is safe and won't flood the server. + +## Scheduling work + +### On every tick — the game loop + +Use `addPacketHandler(0, fn)` or `addEntityUpdateHandler(fn)` for anything that needs to run continuously: + +```js +game.network.addPacketHandler(0, () => { + // runs every tick — keep this fast +}); +``` + +### On a timer — `setInterval` / `setTimeout` + +Use these for work that does not need to be tick-synchronised: + +```js +// run something every 2 seconds +const timer = setInterval(() => { + game.network.sendRpc({ name: "CollectHarvester", uid: someUid }); +}, 2000); + +// cancel it later +clearInterval(timer); +``` + +`setInterval` is appropriate for periodic non-critical tasks like collecting harvesters or checking conditions infrequently. For anything that reacts to world state (health, enemies, position), the tick handler is more reliable. + +### On a server event — `addRpcHandler` + +Use this to react to specific server-sent events: + +```js +game.network.addRpcHandler("Dead", () => { + setTimeout(() => game.network.sendInput({ respawn: 1 }), 500); +}); +``` + +## Debouncing and throttling + +Scripts that send RPCs in a tick handler can accidentally spam the server. The two patterns to know: + +### Throttle with a timestamp + +Allow an action at most once every N milliseconds: + +```js +let lastAt = 0; +game.network.addPacketHandler(0, () => { + const now = Date.now(); + if (now - lastAt < 400) return; + lastAt = now; + // do the thing +}); +``` + +Use this for actions that should repeat but not on every single tick — healing, feeding harvesters, attacking. + +### Debounce with a flag + +Allow an action only once per trigger event: + +```js +let triggered = false; +game.network.addRpcHandler("Dead", () => { + if (triggered) return; + triggered = true; + setTimeout(() => { + game.network.sendInput({ respawn: 1 }); + triggered = false; + }, 500); +}); +``` + +## Rate limits to respect + +| Action | Safe rate | +| :--- | :--- | +| `sendInput` | Any — the scheduler merges per tick automatically | +| `sendRpc` (general) | ~1 per tick per RPC type | +| `SendChatMessage` | ~1 per second (server enforces ~1050ms) | +| `SetPartyMemberCanSell` | Stagger calls — the server processes ~1 per network flush | + +Sending the same RPC type faster than the server processes them does not help and may cause dropped packets or disconnection. + +## Day/night timing + +The server pushes a `DayCycle` RPC when the cycle flips: + +```js +game.network.addRpcHandler("DayCycle", ({ isDay }) => { + if (isDay) { + // daytime — safe to travel, farm, build + } else { + // nighttime — zombies are attacking + } +}); +``` + +One full day+night cycle is approximately **120 seconds**.