-
Notifications
You must be signed in to change notification settings - Fork 5
add scripting fundamentals section (issue #26) #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
TropicalBanana2
wants to merge
1
commit into
AyuBloom:main
Choose a base branch
from
TropicalBanana2:claude/lucid-goodall-c4d502
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); | ||
| } | ||
| ``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.