From a0c523ad7303039ef89ea0eaefe676c51dd8fc87 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Tue, 13 Oct 2026 20:20:44 -0400 Subject: [PATCH 1/9] docs: add a TypeScript guide Held until a release ships the Mod's typed native events (Mod branch typed_events: 09445f75, 8cf15663, f794b9a0): its event-handler samples only type-check against that contract. Before opening the PR, bump the @hogwartsmp/types@1.8.2 install lines to that release. --- guides/Getting started/first-resource.md | 2 +- guides/Getting started/local-server.md | 1 + guides/Getting started/typescript.md | 222 +++++++++++++++++++++++ 3 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 guides/Getting started/typescript.md diff --git a/guides/Getting started/first-resource.md b/guides/Getting started/first-resource.md index 3bc0fc2..73e048d 100644 --- a/guides/Getting started/first-resource.md +++ b/guides/Getting started/first-resource.md @@ -178,7 +178,7 @@ That includes player inventories: a joining player starts with an empty inventor ## Editor autocomplete -The same TypeScript declarations that generate this reference are published on npm as `@hogwartsmp/types`, versioned with the release. Install the version your server runs as a dev dependency and name your side in a `jsconfig.json` or `tsconfig.json`, `"types": ["@hogwartsmp/types/server"]` or `"types": ["@hogwartsmp/types/client"]`, and your editor autocompletes and type-checks the whole API. +The same TypeScript declarations that generate this reference are published on npm as `@hogwartsmp/types`, versioned with the release. Install the version your server runs as a dev dependency and name your side in a `jsconfig.json` or `tsconfig.json`, `"types": ["@hogwartsmp/types/server"]` or `"types": ["@hogwartsmp/types/client"]`, and your editor autocompletes and type-checks the whole API. [Use TypeScript](/guides/getting-started/typescript/) walks through both setups. > Load the server and client declarations **separately**. A global shown in one environment is not automatically available in the other: `Hud`, `Camera`, and `LocalPlayer` are client-only; `PlayerManager`, `NPCManager`, and `InventoryCatalog` are server-only; `World` and `Pvp` exist on both sides with different members. diff --git a/guides/Getting started/local-server.md b/guides/Getting started/local-server.md index bb99ca1..83c864a 100644 --- a/guides/Getting started/local-server.md +++ b/guides/Getting started/local-server.md @@ -77,4 +77,5 @@ A server built from source in the Debug configuration also watches its running r ## Where to go next - [Your first resource](/guides/getting-started/first-resource/) for the manifest, load order and sharing code between resources. +- [Use TypeScript](/guides/getting-started/typescript/) for autocomplete and type checking. - [Debugging](/guides/getting-started/debugging/) for where output and errors go, on both sides. diff --git a/guides/Getting started/typescript.md b/guides/Getting started/typescript.md new file mode 100644 index 0000000..6fd67f8 --- /dev/null +++ b/guides/Getting started/typescript.md @@ -0,0 +1,222 @@ +--- +title: Use TypeScript +description: Get autocomplete and type checking for the whole API, either in plain JavaScript or by compiling TypeScript. +sidebar: + order: 12 +--- + +The runtime runs JavaScript, but every release also publishes the API's TypeScript declarations on npm as [`@hogwartsmp/types`](https://www.npmjs.com/package/@hogwartsmp/types). Install them in a resource and your editor knows every global, its members and their types. It autocompletes them and flags a misspelled method, or a client-only global used on the server, before you start the server. + +There are two ways to use them. Keep writing JavaScript and let the editor check it, or write TypeScript and compile it to the JavaScript your manifest runs. + +## Install the declarations + +The package version is the release version. From the resource's directory, install the one your server runs: + +```sh +npm install --save-dev --save-exact @hogwartsmp/types@1.8.2 +``` + +A resource's `package.json` is also an ordinary npm manifest, so the dependency is recorded beside the `mafiahub` block. As long as the manifest declares `files`, only what it lists [reaches players](/guides/getting-started/first-resource/#what-reaches-the-player); `node_modules/` stays on your machine and the server's disk. + +## One program per side + +Server and client scripts see different globals, so each side is its own TypeScript program and names its half of the package in `types`: `@hogwartsmp/types/server` or `@hogwartsmp/types/client`. Set `lib` to an ECMAScript version and leave out `DOM`. The runtime declares its own `console` and timers, and the DOM's would clash with them. + +## Check plain JavaScript + +Add a `jsconfig.json` to each side's folder. Editors such as VS Code apply it to the scripts beneath it: + +```text +resources/my-gamemode/ +├── package.json +├── server/ +│ ├── jsconfig.json +│ └── main.js +└── client/ + ├── jsconfig.json + └── main.js +``` + +```jsonc +// server/jsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "node16", + "types": ["@hogwartsmp/types/server"], + "checkJs": true + } +} +``` + +The client's is the same with `@hogwartsmp/types/client`. `checkJs` turns on error reporting; leave it out to get autocomplete alone. To check every script from the command line, install the compiler and run it once per side without writing any output: + +```sh +npm install --save-dev --save-exact typescript@5.9.3 +npx tsc -p server/jsconfig.json --noEmit +npx tsc -p client/jsconfig.json --noEmit +``` + +## Compile TypeScript + +Keep the sources in `src/` and compile each side into `dist/`, which is what the manifest runs: + +```text +resources/my-gamemode/ +├── package.json +├── tsconfig.json # the server program +├── src/ +│ ├── server/ +│ │ └── main.ts +│ └── client/ +│ ├── tsconfig.json # the client program +│ └── main.ts +└── dist/ # compiled JavaScript +``` + +Install the compiler next to the declarations, then point the manifest at the compiled files and add a build script: + +```sh +npm install --save-dev --save-exact typescript@5.9.3 +``` + +```json +{ + "name": "my-gamemode", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc -p tsconfig.json && tsc -p src/client/tsconfig.json" + }, + "devDependencies": { + "@hogwartsmp/types": "1.8.2", + "typescript": "5.9.3" + }, + "mafiahub": { + "serverScripts": ["dist/server/main.js"], + "clientScripts": ["dist/client/main.js"], + "files": ["dist/client/**/*.js"] + } +} +``` + +`files` ships every compiled client module, so an `import` between your client files still resolves on the player's machine. + +The server program sits at the resource root: + +```jsonc +// tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "node16", + "types": ["@hogwartsmp/types/server"], + "strict": true, + "noEmitOnError": true, + "rootDir": "src/server", + "outDir": "dist/server" + }, + "include": ["src/server"] +} +``` + +The client program sits beside its sources: + +```jsonc +// src/client/tsconfig.json +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "node16", + "types": ["@hogwartsmp/types/client"], + "strict": true, + "noEmitOnError": true, + "rootDir": ".", + "outDir": "../../dist/client" + }, + "include": ["."] +} +``` + +| Option | Why | +| --- | --- | +| `"module": "node16"` | Both runtimes load scripts as CommonJS modules. Without `"type": "module"` in `package.json`, the compiler turns each `import` into the `require()` they understand. | +| `"noEmitOnError": true` | The compiler writes JavaScript even when type checking fails. Without this, a broken build replaces the last good one in `dist/`. | +| `rootDir`, `outDir` | Each side compiles into its own folder, the one the manifest names. | + +A server entry with a typed handler: + +```ts +// src/server/main.ts +const banned = new Set(["76561190000000000"]); + +Events.on("playerConnecting", (connection) => { + if (banned.has(connection.steamId)) connection.reject("You are banned from this server."); +}); +``` + +A client entry. A custom event's payload is `unknown`, so narrow it before using it: + +```ts +// src/client/main.ts +Events.on("my-gamemode:announce", (payload) => { + if (typeof payload === "string") Game.notify(payload); +}); +``` + +Build, then restart the resource to load the new `dist/`: + +```sh +npm run build +``` + +The client side runs in a sandbox whose `require()` loads only your own files by relative path, so a client program can import its own modules but not npm packages. To use a package on the client, bundle the client program into one file with a bundler such as esbuild instead. + +## Typing event handlers + +Every native event is declared with the arguments its handlers receive, so a handler's parameters are typed without annotations, in TypeScript and in checked JavaScript alike: + +```ts +// src/server/main.ts +Events.on("chatCommand", (player, message, command, args) => { + if (command === "wave") Chat.sendToAll(`${player.nickname} waves at ${args.join(" ") || "everyone"}.`); +}); + +Events.on("npcDied", (npcId, enemyId, killerId) => { + if (killerId !== null) new Player(killerId).sendChat(`You defeated ${enemyId}.`); +}); +``` + +The compiler also holds you to what an event can carry: `killerId` above is `number | null`, so using it without the check is an error. Events your own resources emit are not declared, so their payloads stay `unknown` until you narrow them. + +## Node.js modules on the server + +Server scripts run in Node.js and may `require` its modules. To type them, install `@types/node` and add `"node"` to the server program's `types`. Node's declarations bring their own `console`, so also set `skipLibCheck`: + +```jsonc +// tsconfig.json (excerpt) +{ + "compilerOptions": { + "types": ["node", "@hogwartsmp/types/server"], + "skipLibCheck": true + } +} +``` + +## Upgrading the server + +When the server moves to a new release, install the matching declarations and rebuild. Anything the release renamed or removed shows up as a type error: + +```sh +npm install --save-dev --save-exact @hogwartsmp/types@ +npm run build +``` + +## Where to go next + +- [Your first resource](/guides/getting-started/first-resource/) for the manifest, load order and sharing code between resources. +- [Events](/guides/core-concepts/events/) for the event catalog and the client/server bridge. From 27cb3283df1e918570c841be749a5af61a1486a0 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Tue, 13 Oct 2026 23:57:27 -0400 Subject: [PATCH 2/9] docs: number sections by hundreds to make room for 1.9's Every guide's sidebar order is multiplied by ten, so each section owns a hundred and the sections 1.9 adds can sit next to the ones they extend, such as PvE after NPCs and creatures. --- README.md | 2 +- guides/Camera and sound/audio.md | 2 +- guides/Camera and sound/camera.md | 2 +- guides/Camera and sound/voice-chat.md | 2 +- guides/Core concepts/authority.md | 2 +- guides/Core concepts/events.md | 2 +- guides/Core concepts/state-bags.md | 2 +- guides/Core concepts/virtual-worlds.md | 2 +- guides/Getting started/debugging.md | 2 +- guides/Getting started/first-resource.md | 2 +- guides/Getting started/local-server.md | 2 +- guides/Getting started/typescript.md | 2 +- guides/Hosting a server/configuration.md | 2 +- guides/Hosting a server/docker.md | 2 +- guides/Hosting a server/run-a-server.md | 2 +- guides/Mounts and races/brooms-and-mounts.md | 2 +- guides/Mounts and races/quidditch.md | 2 +- guides/Mounts and races/races.md | 2 +- guides/NPCs and creatures/beasts-and-characters.md | 2 +- guides/NPCs and creatures/npcs.md | 2 +- guides/Players/appearance.md | 2 +- guides/Players/chat-and-commands.md | 2 +- guides/Players/inventory.md | 2 +- guides/Players/join-and-spawn.md | 2 +- guides/User interface/hud-and-nametags.md | 2 +- guides/User interface/keys-and-menus.md | 2 +- guides/User interface/map-markers.md | 2 +- guides/User interface/web-views.md | 2 +- guides/World/doors.md | 2 +- guides/World/loot.md | 2 +- guides/World/objects-and-effects.md | 2 +- guides/World/time-and-weather.md | 2 +- 32 files changed, 32 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d231dff..d49b59d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 10–19, Core concepts 20–29, Players 30–39, NPCs and creatures 40–49, World 50–59, User interface 60–69, Camera and sound 70–79, Mounts and races 80–89, Hosting a server 90–99. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, World 500–549, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Camera and sound/audio.md b/guides/Camera and sound/audio.md index d0fbe63..033a8b1 100644 --- a/guides/Camera and sound/audio.md +++ b/guides/Camera and sound/audio.md @@ -2,7 +2,7 @@ title: Audio description: Play the game's sounds at a place, on a player or for one player, stop sounds that keep playing, load soundbanks and change the music, from the server or a client. sidebar: - order: 71 + order: 710 --- Every sound in the game is a Wwise event, named like `cabbage_explosion` or `accio_cast`. The server's `Audio` global plays events on players' clients, and a client's `Audio` plays them for its own player. Event names ignore case, and positions and ranges are world centimetres. diff --git a/guides/Camera and sound/camera.md b/guides/Camera and sound/camera.md index 7d7e25e..6640a1b 100644 --- a/guides/Camera and sound/camera.md +++ b/guides/Camera and sound/camera.md @@ -2,7 +2,7 @@ title: Camera description: Take over the player's view with a camera of your own, move it in shots, paths and orbits, fade the screen, and hand the view back. sidebar: - order: 70 + order: 700 --- Each client has one custom camera that client scripts drive with the `Camera` global. While it is active, the player sees through it instead of the game's own camera, and `Camera.restore()` hands the view back. Positions are world centimetres, rotations are degrees and durations are seconds. diff --git a/guides/Camera and sound/voice-chat.md b/guides/Camera and sound/voice-chat.md index 32fea71..90b16ab 100644 --- a/guides/Camera and sound/voice-chat.md +++ b/guides/Camera and sound/voice-chat.md @@ -2,7 +2,7 @@ title: Voice chat description: Who hears whom in proximity voice chat, how far voices carry, whispering and shouting, muting players, and the voice settings each player controls. sidebar: - order: 72 + order: 720 --- Players talk to the people near them, holding push-to-talk, which is V by default. The server's `Voice` global decides how far voices carry and who may talk or listen, and each client's `Voice` controls that player's own microphone and volume. Ranges are world centimetres. diff --git a/guides/Core concepts/authority.md b/guides/Core concepts/authority.md index 9120b7e..96f8a92 100644 --- a/guides/Core concepts/authority.md +++ b/guides/Core concepts/authority.md @@ -2,7 +2,7 @@ title: Authority description: What the server decides, what players' clients run and report, and how to handle what a client sends. sidebar: - order: 20 + order: 200 --- A HogwartsMP server and its players' clients split the work. The server owns the state that matters, and each client runs the game for its player and reports what happens in it. Knowing which side owns what tells you which input to trust. diff --git a/guides/Core concepts/events.md b/guides/Core concepts/events.md index 2e7731c..aa39af7 100644 --- a/guides/Core concepts/events.md +++ b/guides/Core concepts/events.md @@ -2,7 +2,7 @@ title: Events description: Listen and emit across resources and the client/server boundary, the runtime event catalog, and what to trust. sidebar: - order: 21 + order: 210 --- `Events` is the asynchronous, named event bus available to both server and client resources. Resources use it to react to runtime activity, communicate with other resources in the same environment, and exchange messages across the client/server boundary. diff --git a/guides/Core concepts/state-bags.md b/guides/Core concepts/state-bags.md index 5279a1f..881ac5b 100644 --- a/guides/Core concepts/state-bags.md +++ b/guides/Core concepts/state-bags.md @@ -2,7 +2,7 @@ title: State bags description: Attach your own keys and values to players and other entities, choose which clients receive each key, and react when one changes. sidebar: - order: 22 + order: 220 --- Every replicated entity carries a state bag, `entity.state`: a small key/value store that the server writes and that travels with the entity to the clients that can see it. Players, NPCs, beasts, world objects, effects and characters all have one, and `World.state` is a bag for state that belongs to no entity. diff --git a/guides/Core concepts/virtual-worlds.md b/guides/Core concepts/virtual-worlds.md index a4a3229..6c13b2f 100644 --- a/guides/Core concepts/virtual-worlds.md +++ b/guides/Core concepts/virtual-worlds.md @@ -2,7 +2,7 @@ title: Virtual worlds description: Split players and entities into separate copies of the world, how a player's world combines with the map they are in, and what ignores worlds. sidebar: - order: 23 + order: 230 --- A virtual world is a number that partitions replication. A player sees the players and entities in their own world and nothing in any other, so several groups can share the same place without meeting: a duel arena, a private event or a tutorial. Players in different worlds do not hear each other on voice chat either. diff --git a/guides/Getting started/debugging.md b/guides/Getting started/debugging.md index 7ceb8c3..f8ad6d2 100644 --- a/guides/Getting started/debugging.md +++ b/guides/Getting started/debugging.md @@ -2,7 +2,7 @@ title: Debugging description: Where console output and errors go on the server and the client, what each kind of error does to a resource, and what to attach to a bug report. sidebar: - order: 13 + order: 130 --- The release server and client have no debugger to attach to resource scripts, so you debug with what your scripts log and what the runtime logs about them. This page covers where that output goes on each side and how to read it. diff --git a/guides/Getting started/first-resource.md b/guides/Getting started/first-resource.md index 73e048d..15ff65b 100644 --- a/guides/Getting started/first-resource.md +++ b/guides/Getting started/first-resource.md @@ -2,7 +2,7 @@ title: Your first resource description: Build a minimal resource with a manifest and server and client scripts, then learn load order, sharing code and cleaning up. sidebar: - order: 11 + order: 110 --- Everything a game mode does in HogwartsMP lives in a **resource**: a directory containing a `package.json` manifest and one or more JavaScript files. The server discovers every resource directory under `resources/` at startup, runs its server scripts in Node.js, and packages its client scripts and assets for each connecting player, where they run in a sandboxed V8 context. diff --git a/guides/Getting started/local-server.md b/guides/Getting started/local-server.md index 83c864a..bbc6945 100644 --- a/guides/Getting started/local-server.md +++ b/guides/Getting started/local-server.md @@ -2,7 +2,7 @@ title: Run a local server description: Run a bare server on your own machine, join it, and reload a resource after every change without restarting. sidebar: - order: 10 + order: 100 --- To develop a resource you need a server you control and a client to join it with. Both come in the release ZIP, and a server for local work needs nothing else: no Foundations, no database and no open ports. diff --git a/guides/Getting started/typescript.md b/guides/Getting started/typescript.md index 6fd67f8..ed1309f 100644 --- a/guides/Getting started/typescript.md +++ b/guides/Getting started/typescript.md @@ -2,7 +2,7 @@ title: Use TypeScript description: Get autocomplete and type checking for the whole API, either in plain JavaScript or by compiling TypeScript. sidebar: - order: 12 + order: 120 --- The runtime runs JavaScript, but every release also publishes the API's TypeScript declarations on npm as [`@hogwartsmp/types`](https://www.npmjs.com/package/@hogwartsmp/types). Install them in a resource and your editor knows every global, its members and their types. It autocompletes them and flags a misspelled method, or a client-only global used on the server, before you start the server. diff --git a/guides/Hosting a server/configuration.md b/guides/Hosting a server/configuration.md index b52d2ba..3d4f810 100644 --- a/guides/Hosting a server/configuration.md +++ b/guides/Hosting a server/configuration.md @@ -2,7 +2,7 @@ title: Server configuration description: Every server.json key and command-line flag, passwords, required CurseForge mods, respawning, and the information API. sidebar: - order: 91 + order: 910 --- The server takes its settings from `server.json` in its working directory, and command-line flags can override some of them. This page lists every key and flag. To install a server first, see [Hosting a server](/guides/hosting-a-server/run-a-server/). diff --git a/guides/Hosting a server/docker.md b/guides/Hosting a server/docker.md index 8f64ba7..41eaba7 100644 --- a/guides/Hosting a server/docker.md +++ b/guides/Hosting a server/docker.md @@ -2,7 +2,7 @@ title: Docker and game panels description: Run the public server image with Docker Compose, or on Pterodactyl and Pelican with the official egg. sidebar: - order: 92 + order: 920 --- Every release publishes the Linux dedicated server as a public container image: diff --git a/guides/Hosting a server/run-a-server.md b/guides/Hosting a server/run-a-server.md index 3c9934a..2f3797d 100644 --- a/guides/Hosting a server/run-a-server.md +++ b/guides/Hosting a server/run-a-server.md @@ -3,7 +3,7 @@ title: Hosting a server description: Take a fresh Windows or Linux machine to a public server running HMP Foundations, then keep it running and updated. sidebar: label: Run a server - order: 90 + order: 900 --- A HogwartsMP server is one executable plus a `server.json` configuration file and a `resources/` directory. The release ships the runtime only, with no game mode. Public servers run **HMP Foundations**, a complete resource pack that adds characters, inventory, banking, jobs, shops, administration and other common systems on top of a MySQL or MariaDB database. diff --git a/guides/Mounts and races/brooms-and-mounts.md b/guides/Mounts and races/brooms-and-mounts.md index 77880bf..33dade1 100644 --- a/guides/Mounts and races/brooms-and-mounts.md +++ b/guides/Mounts and races/brooms-and-mounts.md @@ -2,7 +2,7 @@ title: Brooms and mounts description: Give players brooms and creatures to ride, put them on and take them off from a client script, change how a broom flies, and keep riders out of an area. sidebar: - order: 80 + order: 800 --- Players fly brooms and ride the game's creatures, and everyone else sees them do it without any script. Client scripts control the local player's riding with `Broom`, `FlyingMount` and `GroundMount`. The server decides what each player owns, through their [inventory](/guides/players/inventory/), and has no call of its own for riding. diff --git a/guides/Mounts and races/quidditch.md b/guides/Mounts and races/quidditch.md index 16881ba..e1f066d 100644 --- a/guides/Mounts and races/quidditch.md +++ b/guides/Mounts and races/quidditch.md @@ -2,7 +2,7 @@ title: Quidditch description: Spawn a ball, hoops, bludgers, the snitch and house banners on players' clients, and run the match itself on the server. sidebar: - order: 82 + order: 820 --- The `Quidditch` global spawns the pieces of a match on a client: a ball, hoops, bludgers, the game's snitch and house banners. It is a box of props, not a game. Nothing in it notices a goal, a catch or a hit, and each client spawns its own copy, so the server runs the match: it decides where everything is, tells clients, and works out goals and catches from positions. diff --git a/guides/Mounts and races/races.md b/guides/Mounts and races/races.md index 6b53a82..7ed8f60 100644 --- a/guides/Mounts and races/races.md +++ b/guides/Mounts and races/races.md @@ -2,7 +2,7 @@ title: Races description: Run the game's broom races on tracks of your own, for several players at once, with the server keeping the time. sidebar: - order: 81 + order: 810 --- The game has broom races of its own, with rings to fly through, turbo bubbles and a pace ghost. Client scripts can take one of these races, give it a track of your own and start it, with the `Races` global. `Races` is experimental, and it works on the local player only: each racer's client runs its own copy of the race, and the server ties them together. diff --git a/guides/NPCs and creatures/beasts-and-characters.md b/guides/NPCs and creatures/beasts-and-characters.md index 24e5066..9b3c80d 100644 --- a/guides/NPCs and creatures/beasts-and-characters.md +++ b/guides/NPCs and creatures/beasts-and-characters.md @@ -2,7 +2,7 @@ title: Beasts and characters description: Spawn tame beasts players can pet and feed, decide who may care for them, and place stationary townspeople and vendors with nameplates. sidebar: - order: 42 + order: 420 --- Besides enemies, the server can place two kinds of living thing: tame beasts, which wander and can be cared for, and characters, which stand still and never fight. diff --git a/guides/NPCs and creatures/npcs.md b/guides/NPCs and creatures/npcs.md index b64ec60..3ac33ed 100644 --- a/guides/NPCs and creatures/npcs.md +++ b/guides/NPCs and creatures/npcs.md @@ -2,7 +2,7 @@ title: NPCs description: Spawn the game's enemies as replicated NPCs, control their health, targets and size, and react when they are hurt or die. sidebar: - order: 40 + order: 400 --- `NPC.create` spawns one of the game's enemies as a replicated NPC that every nearby player sees and fights. The server decides that it exists and holds what you set on it; its AI runs on one nearby player's client. diff --git a/guides/Players/appearance.md b/guides/Players/appearance.md index 5dc8db3..d65e74a 100644 --- a/guides/Players/appearance.md +++ b/guides/Players/appearance.md @@ -2,7 +2,7 @@ title: Appearance description: Save and restore how players look, open the character creator, and change their clothes, house, size, disguise and what they hold. sidebar: - order: 33 + order: 330 --- A player's look is their body, face, hair, voice and name. Their own game builds it and sends it to the server, which checks it and shows it to everyone else. The server can read a look, keep it and hand it back, and it controls a player's house, size, disguise and the prop in their hand. Clothes are gear in the player's [inventory](/guides/players/inventory/). diff --git a/guides/Players/chat-and-commands.md b/guides/Players/chat-and-commands.md index 80dcd26..f40d311 100644 --- a/guides/Players/chat-and-commands.md +++ b/guides/Players/chat-and-commands.md @@ -2,7 +2,7 @@ title: Chat and commands description: Send chat to everyone or one player, take over relaying players' chat, handle slash commands, and replace the chat on the client. sidebar: - order: 31 + order: 310 --- The game's chat overlay carries both chat and slash commands. The server sees every line a player sends and decides what everyone else reads. diff --git a/guides/Players/inventory.md b/guides/Players/inventory.md index c0bb182..25125bc 100644 --- a/guides/Players/inventory.md +++ b/guides/Players/inventory.md @@ -2,7 +2,7 @@ title: Inventory description: Give, take and move items in a server-owned inventory, change several things at once safely, save inventories between visits, and adopt items the game hands out. sidebar: - order: 32 + order: 320 --- Every player's inventory belongs to the server. `player.inventory` is the server's model of it: the player's game shows what the server sends, its reads never ask the client, and nothing the client reports adds an item on its own. diff --git a/guides/Players/join-and-spawn.md b/guides/Players/join-and-spawn.md index de9e614..136d108 100644 --- a/guides/Players/join-and-spawn.md +++ b/guides/Players/join-and-spawn.md @@ -2,7 +2,7 @@ title: Join and spawn description: Admit or turn away joining players, the events a join raises, putting players where you want them, deaths and respawns, and leaving. sidebar: - order: 30 + order: 300 --- A player's visit raises a fixed series of events on the server. This page follows it from the request to join to the moment they leave. diff --git a/guides/User interface/hud-and-nametags.md b/guides/User interface/hud-and-nametags.md index ab7736d..c433098 100644 --- a/guides/User interface/hud-and-nametags.md +++ b/guides/User interface/hud-and-nametags.md @@ -2,7 +2,7 @@ title: HUD, prompts and nametags description: Hide parts of the game's HUD, float an interaction prompt over the world, and set the names, colours and lines over players' heads. sidebar: - order: 61 + order: 610 --- Some of what players see on screen is the game's own HUD, which client scripts can hide in parts and add a prompt to. The names over players' heads are set from both sides: the server decides what everyone sees, and each client can add lines and hide nametags for its own player. diff --git a/guides/User interface/keys-and-menus.md b/guides/User interface/keys-and-menus.md index 60cea48..31f52ae 100644 --- a/guides/User interface/keys-and-menus.md +++ b/guides/User interface/keys-and-menus.md @@ -2,7 +2,7 @@ title: Keys and menus description: Bind keyboard and mouse keys, lock the game's controls while players use your interface, and keep the game's own menus closed so your resource can replace them. sidebar: - order: 63 + order: 630 --- Client scripts can listen for keys of their own, take the game's controls away while players use an interface, and keep the game's own menus from opening so the keys behind them can open yours. diff --git a/guides/User interface/map-markers.md b/guides/User interface/map-markers.md index 5f18ba8..e7185ee 100644 --- a/guides/User interface/map-markers.md +++ b/guides/User interface/map-markers.md @@ -2,7 +2,7 @@ title: Map markers description: Put markers, search circles and gold routes on the map and minimap, choose which players get a blip and in what colour, and read what the minimap shows. sidebar: - order: 62 + order: 620 --- The game's map and minimap draw a blip for every other player, and client scripts can add markers and circles of their own and draw the game's gold route to one. All of this lives on each client, in the `Blips` global, so the server decides what each player should see and tells their client. diff --git a/guides/User interface/web-views.md b/guides/User interface/web-views.md index 131e603..acbf1df 100644 --- a/guides/User interface/web-views.md +++ b/guides/User interface/web-views.md @@ -2,7 +2,7 @@ title: Web views description: Draw your own interfaces in HTML over the game, ship their pages with your resource, and pass messages between a page and your client script. sidebar: - order: 60 + order: 600 --- A web view is a browser page drawn over the game. Client scripts create and control views with the `Web` global, and a page talks back to its script through a `callEvent` function the view gives it. Menus, shops and scoreboards are usually web views. diff --git a/guides/World/doors.md b/guides/World/doors.md index 51d6266..5d25deb 100644 --- a/guides/World/doors.md +++ b/guides/World/doors.md @@ -2,7 +2,7 @@ title: Doors description: Hold the game's doors locked, unlocked, open or closed for every player, find a door's name, and decide when a locked door opens for someone. sidebar: - order: 52 + order: 520 --- Hogwarts and Overland are full of the game's own doors and locks, and each player's game normally keeps their state in that player's save. The server can take over any of them, so that a door is locked or open for everyone. diff --git a/guides/World/loot.md b/guides/World/loot.md index bc5d1c1..c52ae0e 100644 --- a/guides/World/loot.md +++ b/guides/World/loot.md @@ -2,7 +2,7 @@ title: Loot description: Place chests, moonstone deposits and foragables the server owns, decide what each player finds in them, refill them, and switch off the game's own loot. sidebar: - order: 53 + order: 530 --- Players loot the world the game's way: they open chests with F, break moonstone deposits with a spell and pick up foragables with F. The server can own any of these, so that what they hold, who can take it and when it refills are your resource's to decide. diff --git a/guides/World/objects-and-effects.md b/guides/World/objects-and-effects.md index f927be4..035ab83 100644 --- a/guides/World/objects-and-effects.md +++ b/guides/World/objects-and-effects.md @@ -2,7 +2,7 @@ title: Objects and effects description: Place props from the game's meshes and visual effects from its effect systems, keyed so you can replace or remove them later. sidebar: - order: 51 + order: 510 --- The server can place two kinds of decoration that every nearby player sees: world objects, built from the game's static meshes, and effects, built from its particle systems. Both are created under a key of your choosing: creating again with the same key replaces the old one, and the key removes it later. diff --git a/guides/World/time-and-weather.md b/guides/World/time-and-weather.md index d630c84..abd0fc0 100644 --- a/guides/World/time-and-weather.md +++ b/guides/World/time-and-weather.md @@ -2,7 +2,7 @@ title: Time and weather description: Set the shared clock, calendar, season and weather every player sees, change how fast time passes, and pin holiday decor and the Headmaster's office. sidebar: - order: 50 + order: 500 --- The server owns one world state that every client follows: the clock, the calendar, the season, the weather, holiday decor and the Headmaster's office. Each player's game shows what the server sets, so everyone sees the same sky. From 9b0c603ec2631166d6dc10b1079a4e1a721e56bc Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:04:45 -0400 Subject: [PATCH 3/9] docs: add Room of Requirement guides New Room of Requirement section (orders 550-599) for 1.9: rooms as layouts under script-chosen ids, editors, live edits and saving on sanctuaryChanged; destinations, Sanctuary.enter, visitors, the shared castle door and its modes and access, occupancy events, and each room being its own virtual world. Virtual worlds links to it. --- README.md | 2 +- guides/Core concepts/virtual-worlds.md | 2 +- .../Room of Requirement/doors-and-visits.md | 89 +++++++++++++++++++ guides/Room of Requirement/rooms.md | 84 +++++++++++++++++ guides/overview.md | 5 ++ 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 guides/Room of Requirement/doors-and-visits.md create mode 100644 guides/Room of Requirement/rooms.md diff --git a/README.md b/README.md index d49b59d..7bcff14 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, World 500–549, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Core concepts/virtual-worlds.md b/guides/Core concepts/virtual-worlds.md index 6c13b2f..80b36f0 100644 --- a/guides/Core concepts/virtual-worlds.md +++ b/guides/Core concepts/virtual-worlds.md @@ -35,7 +35,7 @@ The value reads back at once and takes effect on the next server tick. A player' The world you set is combined with the map the player is in. In Overland, the game's main map, world N is world N. Every other map, such as a dungeon, has worlds of its own: players there share a world with the others in the same map who have the same world number, and never meet anyone in another map. -During a loading screen between maps, each player is alone in a world of their own. +During a loading screen between maps, each player is alone in a world of their own. In the Room of Requirement, each room is a world of its own, combined with the world number: see [Who sees whom](/guides/room-of-requirement/doors-and-visits/#who-sees-whom). `player.virtualWorld` returns the number you set, not this combined world. diff --git a/guides/Room of Requirement/doors-and-visits.md b/guides/Room of Requirement/doors-and-visits.md new file mode 100644 index 0000000..a82201c --- /dev/null +++ b/guides/Room of Requirement/doors-and-visits.md @@ -0,0 +1,89 @@ +--- +title: Doors and visits +description: Decide where each player's door to the Room of Requirement leads, take players into rooms, invite visitors, and know who is in which room. +sidebar: + order: 560 +--- + +Players reach the Room of Requirement through its door in the castle, or by fast travel. Where they arrive is up to the server: each player has a destination, the room their door leads to, and the server can also take a player into any room at once. + +## Destinations + +`Sanctuary.setDestination(player, id)` sets the room a player's door and fast travel lead to, and `null` leads nowhere. `Sanctuary.destination(player)` reads it back. + +A destination can be set before its room is loaded. The door stays shut for that player until the room loads, so set it first and load the room from your store afterwards. Changing a player's destination while they are in a room takes effect once they leave. + +## Taking a player in + +`Sanctuary.enter(player, id)` takes a player into a room now, behind the game's loading screen, from anywhere in Overland or from another room, which it swaps in place. It returns `false` when no room is loaded under that id, and leaves the player's destination as it was. + +```js +// server/main.js +function bringInto(player, id) { + if (!Sanctuary.enter(player, id)) player.sendChat("That room is not open."); +} +``` + +There is no call to take a player out. They leave by the room's own exit, or you move them with `player.fastTravel`. + +## Visitors + +There are no invitations as such: a visitor is a player whose destination is someone else's room, or whom you take there. This keeps a list of who each player has invited, and lets invited players set their door to a friend's room: + +```js +// server/main.js +const invited = new Map(); // room id -> Set of player ids + +Events.on("chatCommand", (player, message, command, args) => { + if (command === "invite" && args[0]) { + const guest = PlayerManager.getAll().find((p) => p.nickname === args[0]); + if (!guest) return player.sendChat(`Nobody called ${args[0]} is here.`); + const room = roomOf(player); + if (!invited.has(room)) invited.set(room, new Set()); + invited.get(room).add(guest.id); + guest.sendChat(`${player.nickname} invited you to their room. Type /visit ${player.nickname}.`); + } + + if (command === "visit" && args[0]) { + const host = PlayerManager.getAll().find((p) => p.nickname === args[0]); + if (!host || !invited.get(roomOf(host))?.has(player.id)) return player.sendChat("You have not been invited."); + Sanctuary.setDestination(player, roomOf(host)); + player.sendChat(`The door now leads to ${host.nickname}'s room.`); + } + + if (command === "home") Sanctuary.setDestination(player, roomOf(player)); +}); +``` + +`roomOf` is the room id from [Rooms](/guides/room-of-requirement/rooms/#loading-a-room). Destinations, edit rights and door access belong to the player's connection, and are gone when they reconnect, so set them again on `playerConnect`. + +## The door + +The door in the castle is one door that everyone sees. `Sanctuary.setDoor(mode)` sets how it behaves for everyone: + +| Mode | The door | +| --- | --- | +| `"proximity"` | Appears while a player who may enter stands at it, and everyone watching sees it appear. The default. | +| `"open"` | Always shows | +| `"closed"` | Never shows, so nobody walks in | + +`Sanctuary.setDoorAccess(player, false)` keeps the door shut for one player. They still see it appear for others, but cannot go through, and it never appears for them. `Sanctuary.doorAccess(player)` is true when a player may go through: they have access, and their destination is loaded. + +The door is not a lock. Fast travel to the Room of Requirement ignores door access, and the door itself is drawn by each player's game. What keeps a player out of a room is that the server only sends a room to players whose destination it is, who are entering it or who are in it. To bar a player, clear their destination as well. + +## Who is where + +`Sanctuary.inside(player)` returns the room a player is in, or `null`, and `Sanctuary.occupants(id)` lists the players in a room. `playerSanctuaryEnter` and `playerSanctuaryLeave` fire with the player and the room's id. Swapping rooms raises a leave for the old room and then an enter for the new one, and a player who disconnects inside a room leaves it before `playerDisconnect` fires. + +The server only knows a player is inside once their game has finished loading the room, so `inside` stays `null` through the loading screen. + +## Who sees whom + +Each room is its own [virtual world](/guides/core-concepts/virtual-worlds/), combined with the player's world number. Players in the same room with the same world number see and hear each other, and nobody in another room. The global world `4294967295` still sees into every room. + +Entities the server creates are not tied to a map, so you cannot place an NPC or an object in one particular room. + +## Where to go next + +- [Rooms](/guides/room-of-requirement/rooms/) for loading, decorating and saving rooms. +- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for how worlds and maps combine. diff --git a/guides/Room of Requirement/rooms.md b/guides/Room of Requirement/rooms.md new file mode 100644 index 0000000..e56f42f --- /dev/null +++ b/guides/Room of Requirement/rooms.md @@ -0,0 +1,84 @@ +--- +title: Rooms +description: Load rooms of the Room of Requirement under ids of your own, let players decorate them, and save what they build. +sidebar: + order: 550 +--- + +The Room of Requirement holds as many rooms as the server loads. A room is a layout, the objects players have conjured and changed there, stored under an id your script chooses. The `Sanctuary` global on the server loads rooms, decides who may decorate them and who goes into which. + +Nobody owns a room unless your script says so. "Every player has a room of their own" is a few lines of script: load a room for each player as they join, make it where their door leads, and let them edit it. + +## Loading a room + +`Sanctuary.load(id, layout)` creates a room under an id, or replaces the one there. An id is any string of 1 to 128 characters. An empty room is a layout with nothing in it: + +```js +// server/main.js +const EMPTY_ROOM = { version: 1, objects: [], locks: {}, lighting: "" }; + +function roomOf(player) { + return `room:${player.id}`; +} + +Events.on("playerConnect", (player) => { + const id = roomOf(player); + if (Sanctuary.revision(id) === null) Sanctuary.load(id, EMPTY_ROOM); + Sanctuary.setDestination(player, id); + Sanctuary.setEditor(id, player, true); +}); +``` + +`Sanctuary.ids()` lists every loaded room, and `Sanctuary.revision(id)` returns a room's revision, or `null` when no room is loaded under that id. `Sanctuary.unload(id)` drops a room, and refuses while anyone is inside it, entering it or has it as their destination. `load` throws for a malformed layout. + +Players see each other's rooms only through your script, so choose ids that say nothing private. Every player taken into a room can read its id. + +## Decorating + +`Sanctuary.setEditor(id, player, true)` lets a player change a room with the game's own spells: conjuring, moving, resizing, recolouring and vanishing pieces, unlocking parts of the room and choosing its lighting. They need the spells and the moonstones to cast them, as in the game. `Sanctuary.editors(id)` lists who may edit a room. + +Edit rights belong to the player's connection: they last until the player disconnects, whether or not the room is loaded, so grant them again when the player comes back. + +Moving, conjuring, recolouring and vanishing reach everyone in the room within a second or so. Unlocked parts and lighting appear for everyone else the next time they enter. Players without edit rights can still cast in a room, but nothing they change is kept, and it is gone the next time they enter. + +The server checks that an edit is well formed, not that it makes sense, so a player you let edit can put anything anywhere in their room. + +## Saving rooms + +Rooms live in the server's memory. They survive your resource restarting, and are gone when the server stops. To keep them, save each room as it changes and load it again when you need it. + +`sanctuaryChanged` fires with the room's id, its new revision and the player who edited it, after every accepted edit. An editor's game reports changes twice a second while they work, so wait for a pause before saving. `Sanctuary.get(id)` returns the room's layout, ready to store as JSON: + +```js +// server/main.js +const pendingSaves = new Map(); // room id -> timer + +Events.on("sanctuaryChanged", (id) => { + clearTimeout(pendingSaves.get(id)); + pendingSaves.set(id, setTimeout(() => { + pendingSaves.delete(id); + const layout = Sanctuary.get(id); + if (layout) saveRoom(id, JSON.stringify(layout)); + }, 2000)); +}); +``` + +`saveRoom` stands for your own store. Load a saved room before you need it, and only when it is not loaded already: loading over a room that is still in memory replaces it with the saved copy, which can be a few seconds behind. `load` itself does not raise `sanctuaryChanged`. + +## What a layout holds + +| Field | Holds | +| --- | --- | +| `version` | 1 | +| `objects` | Every conjured or changed piece: its type, position, rotation, scale and colour. A piece of the room's own that was vanished has the type `"None"`. | +| `locks` | Which parts of the room are unlocked: the expansions, the biomes, the initial dressing and lighting | +| `lighting` | The lighting style: `"Default"`, `"Potnr"`, `"Hbgst"`, `"Mzoo"` or `"Dwiz"` (the potioneer's, herbologist's, magizoologist's and dark wizard's) | + +A room holds at most 8192 objects. A layout does not hold what goes on at the room's stations, its beasts, who may edit it or where anyone's door leads. + +Give every layout its `locks` and `lighting`. A layout that leaves them empty keeps whatever the player's game had from the last room they were in. + +## Where to go next + +- [Doors and visits](/guides/room-of-requirement/doors-and-visits/) for getting players in and out of rooms. +- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for who sees whom inside a room. diff --git a/guides/overview.md b/guides/overview.md index 57bad45..4dccbc5 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -46,6 +46,11 @@ Use the navigation to browse the globals available to the selected environment. - [Doors](/guides/world/doors/) — holding the game's doors locked or open for everyone, and deciding who gets in. - [Loot](/guides/world/loot/) — server-owned chests, moonstones and foragables, per-player contents, and switching off the game's own loot. +**Room of Requirement:** + +- [Rooms](/guides/room-of-requirement/rooms/) — loading rooms under ids of your own, letting players decorate them, and saving what they build. +- [Doors and visits](/guides/room-of-requirement/doors-and-visits/) — where each player's door leads, taking players in, visitors, and who is where. + **User interface:** - [Web views](/guides/user-interface/web-views/) — HTML interfaces in the game, and messages between the page and your scripts. From 02c382ec59c2fb3c3c380c0302617693c8ce2530 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:10:45 -0400 Subject: [PATCH 4/9] docs: add encounters and PvP guides Encounters section (orders 450-469) for 1.9: the server-wide encounter policy, server-run camps (activation, real-time respawn, events, reward policies, harvested camps), world events (rolls, in-game cooldowns, start and reset, events) and zones (shapes, tracking, enter and exit, a safe-zone sample). PvP section (orders 470-499): rules, relations, pairs, flags and teams; the hit pipeline, built-in prices, multipliers, the policy hook, kneel and death defeats and health recovery; duels on pairs and the wand link. --- README.md | 2 +- guides/Encounters/camps.md | 74 +++++++++++++++++++ guides/Encounters/encounter-policy.md | 44 +++++++++++ guides/Encounters/world-events.md | 93 +++++++++++++++++++++++ guides/Encounters/zones.md | 91 +++++++++++++++++++++++ guides/PvP/duels-and-wand-links.md | 93 +++++++++++++++++++++++ guides/PvP/hits-and-defeat.md | 83 +++++++++++++++++++++ guides/PvP/rules-and-relations.md | 102 ++++++++++++++++++++++++++ guides/overview.md | 13 ++++ 9 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 guides/Encounters/camps.md create mode 100644 guides/Encounters/encounter-policy.md create mode 100644 guides/Encounters/world-events.md create mode 100644 guides/Encounters/zones.md create mode 100644 guides/PvP/duels-and-wand-links.md create mode 100644 guides/PvP/hits-and-defeat.md create mode 100644 guides/PvP/rules-and-relations.md diff --git a/README.md b/README.md index 7bcff14..3f31808 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, Encounters 450–469, PvP 470–499, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Encounters/camps.md b/guides/Encounters/camps.md new file mode 100644 index 0000000..c75ddea --- /dev/null +++ b/guides/Encounters/camps.md @@ -0,0 +1,74 @@ +--- +title: Camps +description: How the server spawns Overland's enemy camps when players come near, brings their dead back, and tells you when one is cleared. +sidebar: + order: 455 +--- + +Under the [`"server"` encounter policy](/guides/encounters/encounter-policy/), the server runs the game's enemy camps: more than 300 of them across Overland. The `Camps` global reads them and tunes how they come back. + +## How a camp runs + +A camp spawns its enemies when a player in world 0 comes within its trigger radius. The nearest player's game runs them. The first time, the camp rolls its roster, as the game does; after that it keeps the same roster. + +Some camps are only active at certain hours of the server's [clock](/guides/world/time-and-weather/): ten of them only at night, from 21:00 to 05:00. A camp only starts during its hours, and one already running finishes normally. + +A camp despawns 10 seconds after nobody is inside its cull radius and nobody is within 30 m of one of its enemies. Its dead stay dead when it despawns, so a camp left half cleared comes back half cleared. They return `Camps.respawnSeconds` after the last of them died, once nobody is near: 900 seconds by default, in real time, whatever the clock does. + +`Camps.setRespawnSeconds(seconds)` sets that, from 0 to a week. With 0, the dead return as soon as everyone has left. + +## Reading camps + +`Camps.getAll()` lists every camp, and `Camps.get(id)` returns one or `null`: + +| Field | Meaning | +| --- | --- | +| `id` | Its id, such as `":"` | +| `source` | `"catalog"` for the game's own camps, `"harvested"` for camps learned from modded levels | +| `x`, `y`, `z` | Where it is | +| `triggerRadius`, `cullRadius` | How near a player must come for it to spawn, and how far they must go for it to despawn, in centimetres | +| `activeFrom`, `activeTo` | The hours it may start, wrapping past midnight; 0 to 24 is all day | +| `active` | Whether its enemies are out | +| `rolled`, `dead` | How many enemies its roster has, 0 until a player first comes near, and how many of them are dead | +| `npcs` | Its live enemies, as [NPCs](/guides/npcs-and-creatures/npcs/) | + +`Camps.revive(id)` despawns a camp and forgets its roster, so the next player to arrive meets a fresh roll. + +## Camp events + +`campActivated` fires with the camp's id every time its enemies spawn, after each despawn too. `campCleared` fires when every enemy of an active camp is dead. Nothing fires when a camp despawns because everyone left. + +```js +// server/main.js +Events.on("campCleared", (campId) => { + const camp = Camps.get(campId); + if (!camp) return; + const minutes = Math.round(Camps.respawnSeconds / 60); + for (const player of PlayerManager.getAll()) { + const pos = player.position; + if (player.virtualWorld === 0 && Math.hypot(pos.x - camp.x, pos.y - camp.y) <= camp.cullRadius) { + player.sendChat(`Camp cleared. It returns ${minutes} minutes after everyone leaves.`); + } + } +}); +``` + +An enemy counts as dead once its NPC is gone, so destroying one from a script counts towards clearing its camp. + +## What camps pay + +Camp enemies pay out like any NPC. `Camps.setRewardPolicy(policy)` sets what every camp's enemies pay, and `Camps.setRewardPolicy(policy, campId)` what one camp's do. A camp's own policy wins, then the one for every camp, then `NPC.setDefaultRewardPolicy`. `Camps.getRewardPolicy(campId)` reads back what applies. + +```js +// server/main.js +Camps.setRewardPolicy({ recipients: "damagers", experience: 50 }); +``` + +## Camps in modded levels + +A camp a player's game places beyond the game's own, in a modded level, is learned from that player while the policy is `"server"`. It is listed with `source: "harvested"` and kept until the server stops. Its enemies must be ones the game knows: enemies a mod adds cannot be spawned. + +## Where to go next + +- [World events](/guides/encounters/world-events/) for the roaming encounters. +- [NPCs](/guides/npcs-and-creatures/npcs/) for what you can do with a camp's enemies. diff --git a/guides/Encounters/encounter-policy.md b/guides/Encounters/encounter-policy.md new file mode 100644 index 0000000..fca831a --- /dev/null +++ b/guides/Encounters/encounter-policy.md @@ -0,0 +1,44 @@ +--- +title: Encounter policy +description: Decide whether Overland's enemy camps and roaming encounters are switched off, spawned by each player's game, or run by the server for everyone. +sidebar: + order: 450 +--- + +Overland is full of enemy camps, roaming encounters and wild creatures. In single player the game spawns them as the player explores. In multiplayer, the encounter policy decides who spawns them. Enemies that belong to a mission always come from each player's own game, whatever the policy. + +## Choosing a policy + +`World.setEncounterPolicy(policy)` on the server sets the policy for every player, and `World.encounterPolicy` reads it back: + +| Policy | Camps and roaming encounters | Wild creatures | +| --- | --- | --- | +| `"suppressed"`, the default | None | None | +| `"native"` | Each player's game spawns its own: players see different enemies in the same place | Each player's game spawns its own | +| `"server"` | The server spawns [camps](/guides/encounters/camps/) and [world events](/guides/encounters/world-events/) as NPCs that every player meets | None | + +```js +// server/main.js +World.setEncounterPolicy("server"); +``` + +`setEncounterPolicy` returns `false` for any other value. + +The policy belongs to the server, not to your resource: it stays when your resource stops, and any resource can change it. On a client, `World.encounterPolicy()` returns `true` while the player's game is not spawning its own encounters, under `"suppressed"` and `"server"` alike. + +## Running encounters on the server + +Under `"server"`, the camps and roaming encounters are the game's own, at the game's places, with the game's enemies. They spawn as ordinary hostile [NPCs](/guides/npcs-and-creatures/npcs/), so every NPC call works on them and `npcDied` fires when one dies. + +Some of what the game does in single player is not reproduced. Camp patrols, campfire activities and reinforcement waves are missing, and enemies wait where they spawned until they notice someone. An enemy's level is whatever the player's game running it gives it. + +Leaving `"server"` removes every camp and world-event enemy within half a second. It also forgets which enemies each camp rolled, which of them died and every world event's cooldown, so switching back starts everything afresh. + +## Encounters and virtual worlds + +The game's camps and world events are in [virtual world](/guides/core-concepts/virtual-worlds/) 0, in Overland. Only players in world 0 in Overland bring them to life. Players in other worlds, in a dungeon, in the Room of Requirement or on a loading screen never trigger them. Players in the global world `4294967295` see them, but do not trigger them either. + +## Where to go next + +- [Camps](/guides/encounters/camps/) for the enemy camps and how they come back. +- [World events](/guides/encounters/world-events/) for the roaming encounters. diff --git a/guides/Encounters/world-events.md b/guides/Encounters/world-events.md new file mode 100644 index 0000000..ece446e --- /dev/null +++ b/guides/Encounters/world-events.md @@ -0,0 +1,93 @@ +--- +title: World events +description: How the server runs Overland's roaming encounters when players come near, cools each spot down, and lets you start or reset one yourself. +sidebar: + order: 460 +--- + +World events are the game's roaming encounters: a pack of wolves, a band of dark wizards, a troll. Under the [`"server"` encounter policy](/guides/encounters/encounter-policy/), the server runs them once, at more than 500 spots across Overland, so every player meets the same one. The `WorldEvents` global reads and controls them. + +## How a spot runs + +A spot rolls when a player in world 0 comes within 100 m. Each kind of encounter the spot can run has a 60% chance, and at most three events of one kind run at once across the whole server. When the roll succeeds the enemies spawn around the spot; when it fails, the spot waits until everyone has gone before it can roll again. + +An event ends once every player is more than 140 m away. The spot then cools down for `WorldEvents.cooldownHours` of in-game time, 24 by default: at the default time scale, 24 real minutes. Moving the clock forward with `World.setTime` counts towards a cooldown, moving it back does not, and a time scale of 0 freezes every cooldown. + +Some spots only roll by day or by night on the server's clock. + +## Reading spots + +`WorldEvents.getAll()` lists every spot, and `WorldEvents.get(id)` returns one or `null`: + +| Field | Meaning | +| --- | --- | +| `id`, `x`, `y`, `z` | The spot and where it is | +| `kinds` | The encounters it can run, such as `"WE_ENEMY_ENCOUNTER_TROLL"`; kinds ending in `_EVE` set two sides against each other | +| `activeFrom`, `activeTo` | The hours it may roll | +| `streamOutRadius` | How far away players must be for its event to end, in centimetres | +| `state` | `"waiting"`, `"failed"`, `"active"` or `"cooldown"` | +| `kind` | The running or last kind, or `null` | +| `cleared` | Whether its last event was beaten | +| `cooldownSeconds` | In-game seconds of cooldown left | +| `npcs` | Its live enemies, as [NPCs](/guides/npcs-and-creatures/npcs/) | + +## Controlling world events + +| Call | Does | +| --- | --- | +| `WorldEvents.start(id, kind)` | Starts an event at a spot now, whatever its roll, cooldown, hours or the limit of three; leave out `kind` for a random one of the spot's kinds | +| `WorldEvents.reset(id)` | Ends a spot's event and clears its cooldown | +| `WorldEvents.setCooldownHours(hours)` | Sets the cooldown, from 0 to 720 in-game hours, for events that end from now on | +| `WorldEvents.setEnabled(false)` | Ends every running event and turns world events off; camps carry on | + +`start` returns `false` for an unknown spot or kind, a spot that is already running, when the policy is not `"server"` or world events are off, and for half a second after either is turned on. An event started with nobody within 140 m ends straight away, so start events near players: + +```js +// server/main.js +Events.on("chatCommand", (player, message, command) => { + if (command !== "troll") return; + const kind = "WE_ENEMY_ENCOUNTER_TROLL"; + const here = player.position; + const distance = (spot) => Math.hypot(spot.x - here.x, spot.y - here.y); + const spot = WorldEvents.getAll() + .filter((s) => s.kinds.includes(kind) && s.state !== "active" && distance(s) < s.streamOutRadius) + .sort((a, b) => distance(a) - distance(b))[0]; + if (!spot) return player.sendChat("No troll can appear near here."); + player.sendChat(WorldEvents.start(spot.id, kind) ? "A troll approaches." : "The troll did not come."); +}); +``` + +## World event events + +`worldEventStarted` fires with the spot's id and the kind, `worldEventCleared` with the spot's id when every enemy of a running event is dead, and `worldEventEnded` with the spot's id when its event ends. `worldEventEnded` also fires for every running event when you reset its spot, turn world events off or leave the `"server"` policy. + +Some spots roll an event with no enemies. It still raises `worldEventStarted`, but never `worldEventCleared`, so check its `npcs` first: + +```js +// server/main.js +Events.on("worldEventStarted", (spotId, kind) => { + const spot = WorldEvents.get(spotId); + if (!spot || spot.npcs.length === 0) return; + const foe = kind.replace("WE_ENEMY_ENCOUNTER_", "").replace(/_EVE$/, "").toLowerCase(); + for (const player of PlayerManager.getAll()) { + const pos = player.position; + if (player.virtualWorld === 0 && Math.hypot(pos.x - spot.x, pos.y - spot.y) < 30000) player.sendChat(`Something stirs nearby: ${foe}.`); + } +}); +``` + +## What world events pay + +`WorldEvents.setRewardPolicy(policy)` sets what every world event's enemies pay, falling back to `NPC.setDefaultRewardPolicy`, and `WorldEvents.getRewardPolicy()` reads it back. There is one policy for all of them. + +```js +// server/main.js +WorldEvents.setRewardPolicy({ recipients: "nearby", radius: 5000 }); +``` + +These settings belong to the server, not your resource, and stay when it stops. + +## Where to go next + +- [Camps](/guides/encounters/camps/) for the enemy camps. +- [Zones](/guides/encounters/zones/) for areas of your own that notice who comes and goes. diff --git a/guides/Encounters/zones.md b/guides/Encounters/zones.md new file mode 100644 index 0000000..a338b74 --- /dev/null +++ b/guides/Encounters/zones.md @@ -0,0 +1,91 @@ +--- +title: Zones +description: Mark out areas of the world on the server and get an event when players or other entities come in or go out. +sidebar: + order: 465 +--- + +A zone is an area of the world that the server watches. It raises an event when a player, or another kind of entity you ask for, comes in or goes out. A zone does nothing else by itself: a safe zone, an arena or a shop counter is your handlers reacting to those events. + +## Creating a zone + +`Zone.create(key, shape, options)` creates a zone under a key, or replaces the one there, and returns it: + +```js +// server/main.js +const square = Zone.create("market:square", { + type: "cylinder", + center: { x: 12000, y: -4500, z: 200 }, + radius: 4000, + height: 1500, +}); +``` + +Shapes are in world centimetres: + +| `type` | Fields | +| --- | --- | +| `"sphere"` | `center`, `radius` | +| `"box"` | `center`, `size` (full lengths along each axis), `yaw` in degrees | +| `"cylinder"` | `center` (the middle of its base), `radius`, `height` | +| `"polygon"` | `points`, 3 to 64 corners as `{ x, y }` whose edges do not cross, and `minZ`, `maxZ` | + +The options are: + +- `virtualWorld`: the [world](/guides/core-concepts/virtual-worlds/) it watches, 0 by default. A zone matches entities in exactly that world. Players match only in Overland, since anywhere else their world is combined with their map. +- `track`: which entities it notices, from `"player"`, `"npc"`, `"beast"`, `"worldObject"` and `"character"`, by default only players. + +A malformed shape or option throws. The server holds at most 4096 zones. + +Keys are shared by every resource, and creating a zone under a key in use replaces that zone, so start yours with your resource's name. `Zone.get(key)` returns a zone or `undefined`, `Zone.getAll()` lists them, and `Zone.destroy(key)` or `zone.destroy()` removes one. + +## Coming and going + +`zoneEnter` fires with the zone and the entity that came in. `zoneExit` fires with the zone, the entity, a reason and the entity's id: the reason is `"left"` when it moved out, or out of the zone's world, and `"gone"` when it no longer exists, such as a player who disconnected, and then the entity is `null`. Every resource receives every zone's events, so check the key: + +```js +// server/main.js +Events.on("zoneEnter", (zone, entity) => { + if (zone.key === "market:square" && entity instanceof Player) entity.sendChat("Welcome to the market."); +}); + +Events.on("zoneExit", (zone, entity, reason) => { + if (zone.key === "market:square" && reason === "left" && entity instanceof Player) entity.sendChat("Come again."); +}); +``` + +Zones are checked ten times a second. An entity that moves through a zone between two checks still enters and leaves it, but one that moves more than 50 m between checks, such as a teleport, only counts where it lands. + +`zone.getPlayers()` and `zone.getEntities()` list what was inside at the last check, and `zone.contains(point)` tests a point, ignoring worlds. + +## A safe zone + +A zone that turns PvP off inside it pairs the zone's events with a [PvP policy](/guides/pvp/hits-and-defeat/#the-policy-hook): + +```js +// server/main.js +const SAFE = "market:safe"; +const inSafeZone = new Set(); + +Zone.create(SAFE, { type: "sphere", center: { x: 12000, y: -4500, z: 200 }, radius: 3000 }); + +Events.on("zoneEnter", (zone, entity) => { + if (zone.key === SAFE && entity) inSafeZone.add(entity.id); +}); +Events.on("zoneExit", (zone, entity, reason, entityId) => { + if (zone.key === SAFE) inSafeZone.delete(entityId); +}); + +Pvp.setPolicy((hit) => (inSafeZone.has(hit.casterId) || inSafeZone.has(hit.victimId) ? false : undefined)); +``` + +The positions a zone tests are those players' games report, so treat what a zone tells you about a player like any other position they send. + +## Lifetime + +A resource's zones are destroyed when it stops, without `zoneExit`. Zone ids are never reused while the server runs. + +## Where to go next + +- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for which world a zone sees. +- [PvP rules and relations](/guides/pvp/rules-and-relations/) for deciding who may fight where. diff --git a/guides/PvP/duels-and-wand-links.md b/guides/PvP/duels-and-wand-links.md new file mode 100644 index 0000000..d2b8ad6 --- /dev/null +++ b/guides/PvP/duels-and-wand-links.md @@ -0,0 +1,93 @@ +--- +title: Duels and wand links +description: Run one-on-one duels with pairs and kneeling defeats, and stage the game's wand link, where two duellists' beams lock and they push for the upper hand. +sidebar: + order: 480 +--- + +A duel is two players made hostile to each other for a while, whatever the rest of the server is doing. A wand link is the game's own beam contest, which you can stage between any two players, inside a duel or not. + +## A duel + +A [pair](/guides/pvp/rules-and-relations/#pairs) makes two players hostile without touching anyone else, and a kneeling defeat ends the duel without killing anybody: + +```js +// server/main.js +const duels = new Map(); // player id -> opponent + +function startDuel(first, second) { + if (!Pvp.setPair(first, second, { hostile: true, aim: "lock", defeat: "kneel" })) return false; + for (const player of [first, second]) player.setHealthRecovery(false); + duels.set(first.id, second); + duels.set(second.id, first); + first.sendChat(`Duel with ${second.nickname}: begin!`); + second.sendChat(`Duel with ${first.nickname}: begin!`); + return true; +} + +function endDuel(player) { + const opponent = duels.get(player.id); + if (!opponent) return; + duels.delete(player.id); + duels.delete(opponent.id); + Pvp.setPair(player, opponent, null); + for (const p of [player, opponent]) if (p.connected) p.setHealthRecovery(true); +} + +Events.on("pvpDefeat", (victim, winner) => { + if (duels.get(victim.id)?.id !== winner.id) return; + Chat.sendToAll(`${winner.nickname} won the duel against ${victim.nickname}.`); + endDuel(victim); +}); + +Events.on("playerDisconnect", (player) => endDuel(player)); +``` + +Turning health recovery off keeps a duellist from healing between spells. A pair ends by itself when either player leaves, but their opponent's recovery is yours to turn back on. The server holds at most 32 pairs, so check what `setPair` returns. + +## Wand links + +In a wand link, two players' beams meet in the middle and each pushes to drive the clash towards the other. `WandLink.start(a, b, options)` stages one and returns it: + +```js +// server/main.js +function startClash(first, second) { + try { + return WandLink.start(first, second, { maxDistance: 1500 }); + } catch (error) { + first.sendChat("You cannot lock wands with them right now."); + return null; + } +} +``` + +`start` throws when the two cannot link: either is not connected, dead, riding or already linked, they are in different virtual worlds, or they are closer than 1.5 m or farther than `maxDistance` centimetres (4000 by default). `ratio` sets where the clash starts, 0.5 by default. + +The beams fire about three seconds in, and `link.connected` turns true once they meet. Linked players cannot cast, though your key bindings still fire. + +`link.ratio` is where the clash stands, from 0 at the first player's end to 1 at the second's. You can set it, and `link.push(player, amount)` pushes it towards the other player and returns the new ratio. The link does not decide a winner: your script does, and calls `link.end(winner)`, which drives the clash home. `link.end()` without a winner calls it off. + +```js +// server/main.js +Events.onClient("clash:push", (player) => { + const link = WandLink.of(player); + if (!link || !link.connected) return; + const ratio = link.push(player, 0.04); + if (ratio === 1) link.end(link.playerA); + else if (ratio === 0) link.end(link.playerB); +}); +``` + +```js +// client/main.js +Key.bind("e", () => Events.emitServer("clash:push")); +``` + +`wandLinkEnded` fires with the link, the winner or `null`, and why it ended: `"ended"`, `"cancelled"`, `"playerLeft"` or `"playerDied"`. The link ends by itself only when a player leaves or dies, not when they move apart. + +A wand link does no damage and does not need the players to be hostile. To make the loser pay, change their health in `wandLinkEnded`, or start a duel there. `WandLink.of(player)` returns a player's link, and `WandLinkManager.getAll()` lists them. A link outlasts the resource that started it. + +## Where to go next + +- [Hits and defeat](/guides/pvp/hits-and-defeat/) for kneeling and health recovery. +- [Keys and menus](/guides/user-interface/keys-and-menus/#binding-keys) for the push key. diff --git a/guides/PvP/hits-and-defeat.md b/guides/PvP/hits-and-defeat.md new file mode 100644 index 0000000..a972c67 --- /dev/null +++ b/guides/PvP/hits-and-defeat.md @@ -0,0 +1,83 @@ +--- +title: Hits and defeat +description: How a spell hit between players lands, what the server checks, how much it costs, how to change hits with a policy, and what losing a fight does. +sidebar: + order: 475 +--- + +A spell that hits another player passes through the server on its way. The attacker's game reports the hit, the server checks it and prices it, and the victim's game applies the damage. + +## How a hit lands + +1. The attacker's spell has to strike the victim's body, which only blocks spells from players it is hostile to. +2. The attacker's game reports the hit to the server. +3. The server drops it if the two are not [hostile](/guides/pvp/rules-and-relations/#how-a-relation-is-decided), the victim is invulnerable or has spawn protection, the spell is a curse while `curses` is off, or the attacker reports more than 12 hits a second. +4. The server prices the hit, runs the [policy](#the-policy-hook), and sends the hit to the victim's game. +5. The victim's game applies the damage and the spell's effect, and handles a [defeat](#defeat). + +`pvpHit` fires on the server with the attacker's id, the victim's id, the spell and the damage, once the hit is sent. It may still not land, for example on a kneeling player, so count defeats with `pvpDefeat` rather than hits. + +The server does not check how far apart the two are, whether the attacker could see the victim, or whether they even have the spell. A changed game can report any hit on a player it is hostile to, so cap what a hit can do with the policy. + +## Damage + +The server sets the damage of every hit itself, and ignores the attacker's game's own number. Each spell has a price from the game's own spell data: + +| Spell | Damage | +| --- | --- | +| Stupefy | 3, or 23 heavy | +| Stinging | 5 | +| Descendo | 11 | +| Expelliarmus | 20 | +| Confringo, Fiendfyre | 25 | +| Expulso | 35 | +| Diffindo | 45 | +| Incendio | 50 | +| Crucio | 4 a second for 20 seconds | +| Avada Kedavra | Lethal | + +Every other spell does no damage but still has its effect. `Pvp.setSpellDamage(spell, damage)` replaces a spell's price and `null` restores it. The price is then multiplied by the `damageScale` rule, the attacker's `player.damageGivenMultiplier` and the victim's `player.damageTakenMultiplier`, set with `setDamageGivenMultiplier` and `setDamageTakenMultiplier`. The Killing Curse stays lethal unless you set its price yourself. + +## The policy hook + +`Pvp.setPolicy(fn)` runs a function of yours for every hit the rules allow, before it is sent. It receives the hit: the attacker's and victim's ids, the spell, its `damage` and, when known, the victim's health as `victimHp` and `victimMax`. It returns: + +- `false` to drop the hit; +- a number to replace the damage; +- `{ allow, damage, duration, minHp }` to change several things, where `minHp` is a health the hit cannot take the victim below; +- nothing to let the hit through as it is. + +```js +// server/main.js +Pvp.setPolicy((hit) => { + const attacker = PlayerManager.getAll().find((p) => p.id === hit.casterId); + const victim = PlayerManager.getAll().find((p) => p.id === hit.victimId); + if (!attacker || !victim) return false; + const a = attacker.position; + const v = victim.position; + if (Math.hypot(a.x - v.x, a.y - v.y, a.z - v.z) > 6000) return false; // farther than 60 m + return Math.min(hit.damage, 60); +}); +``` + +A policy that throws drops the hit. There is one policy for the whole server and the last one set wins, so a resource that sets one should own it and clear it with `Pvp.setPolicy(null)` as it stops. + +## Defeat + +A hit that would take a player's health to 0 defeats them, the way the relation's `defeat` says: + +- `"death"`: the game's own death. `playerDied` fires, and the player respawns as after any [death](/guides/players/join-and-spawn/#deaths-and-respawns). Once they are back up, they have spawn protection for `spawnProtectionSeconds`. +- `"kneel"`: the player drops to their knees for about four seconds, held at `kneelHp`, ignoring hits, and then gets up by themselves. `playerDowned` fires as they kneel and `playerDownedEnd` as they stand. Under a kneel relation, even the Killing Curse kneels. + +`pvpDefeat` fires with the victim, the player who defeated them and `"death"` or `"kneel"`. It credits a player whose hit the server accepted in the last few seconds, so a defeat nobody caused raises nothing. A policy's `minHp` under a death relation keeps the victim alive without a defeat. + +On the client, `localDowned` fires as the local player kneels and `localDownedEnd` as they get up. + +## Health recovery + +Players get their health back by themselves, and every PvP hit pauses that for 10 seconds. `player.setHealthRecovery(false)` stops it until you set it back to `true`. A respawn or any trip to another map turns recovery back on, so set it again afterwards. + +## Where to go next + +- [Rules and relations](/guides/pvp/rules-and-relations/) for who may fight whom. +- [Duels and wand links](/guides/pvp/duels-and-wand-links/) for one-on-one fights. diff --git a/guides/PvP/rules-and-relations.md b/guides/PvP/rules-and-relations.md new file mode 100644 index 0000000..1f852b2 --- /dev/null +++ b/guides/PvP/rules-and-relations.md @@ -0,0 +1,102 @@ +--- +title: Rules and relations +description: Decide which players may fight each other with server-wide rules, pairs, PvP flags and teams, and read the relation between any two players. +sidebar: + order: 470 +--- + +The server decides which players may hurt each other. It combines four things: the rules for the whole server, pairs of players you set, each player's PvP flag and each player's team. The answer for two players is their relation, and every player's game applies it: spells pass through players who are not hostile, and only hostile players can be locked on to. + +Out of the box nobody can hurt anybody. PvP starts when a script sets rules or pairs. + +## The rules + +`Pvp.configure(patch)` changes the rules you name and returns all of them; `Pvp.getRules()` reads them: + +| Rule | Default | Meaning | +| --- | --- | --- | +| `enabled` | `true` | The master switch. Off, nobody is hostile, pairs included. | +| `who` | `"none"` | Who is hostile without a pair: `"none"`, `"flagged"` (both flagged), `"teams"` (both on a team) or `"everyone"` | +| `friendlyFire` | `false` | Whether players on the same team may hurt each other | +| `aim` | `"lock"` | `"lock"` lets players lock on to hostile players; `"skillshot"` does not, though spells still hit | +| `defeat` | `"death"` | What losing does: the game's own `"death"`, or a `"kneel"` that the player gets up from | +| `curses` | `"on"` | `"off"` stops the Killing, Cruciatus and Imperius curses doing anything to players | +| `kneelHp` | 1 | The health at which a kneel defeat starts, and the health a kneeling player is held at | +| `flagCombatLockSeconds` | 15 | How long after a hit, given or taken, a player who drops their flag stays flagged | +| `spawnProtectionSeconds` | 5 | How long a player killed in PvP cannot be hurt once they are back up; their own hit ends it | +| `damageScale` | 1 | Multiplies every spell's [damage](/guides/pvp/hits-and-defeat/#damage) | + +```js +// server/main.js +Pvp.configure({ who: "everyone", aim: "skillshot", curses: "off" }); +``` + +An unknown rule or a value of the wrong kind throws, and so does a negative number; nothing changes then. `pvpRulesChanged` fires with the rules whenever they change. + +## How a relation is decided + +For two players, in this order: + +1. With `enabled` off, they are not hostile. +2. A pair that says whether they are hostile decides it. +3. Otherwise `who` decides: nobody, both flagged, both on a team, or everyone. +4. Two players on the same team are not hostile unless `friendlyFire` is on. + +A pair's `aim` and `defeat`, when it sets them, replace the rules' for those two players. + +`Pvp.relation(a, b)` returns the relation between two players, as `{ hostile, aim, defeat, reason }`. `reason` says what decided it, such as `"pair"`, `"sameTeam"`, `"notFlagged"` or `"rule"`. On a client, `Pvp.relation(playerId)` returns the local player's relation to another, and `pvpRelationChanged` fires as it changes. + +Virtual worlds already keep players apart: players in different worlds cannot see each other, so they cannot hit each other. + +## Pairs + +`Pvp.setPair(a, b, options)` sets how two players relate, whatever the rules, with `hostile`, `aim` and `defeat`. Leave out a field to keep the rules' answer for it, and pass `null` to clear the pair: + +```js +// server/main.js +function challenge(first, second) { + if (!Pvp.setPair(first, second, { hostile: true, defeat: "kneel" })) { + first.sendChat("No duel can start right now."); + } +} +``` + +`setPair` returns `false` for a player who is not connected, a player paired with themselves, or a new pair when 32 already exist across the server. A pair lasts through deaths and ends when either player disconnects. + +## Flags + +With `who: "flagged"`, players who have turned PvP on can fight each other. `player.setPvpFlag(true)` flags a player and `player.getPvpFlag()` reads it. Dropping the flag within `flagCombatLockSeconds` of a hit is put off until that time is up: `setPvpFlag(false)` then returns how long is left as `pendingMs`, and `pvpFlagChanged` fires with the player and their flag when it changes. + +```js +// server/main.js +Pvp.configure({ who: "flagged" }); + +Events.on("chatCommand", (player, message, command) => { + if (command !== "pvp") return; + const result = player.setPvpFlag(!player.getPvpFlag()); + if (result.pendingMs) player.sendChat(`You are still in combat. PvP turns off in ${Math.ceil(result.pendingMs / 1000)} s.`); + else player.sendChat(result.flagged ? "PvP is on." : "PvP is off."); +}); +``` + +## Teams + +`player.setPvpTeam(name)` puts a player on a team, named by any string of up to 32 bytes, and `null` takes them off it. `player.getPvpTeam()` reads it. With `who: "teams"`, players on different teams are hostile and players without a team are hostile to nobody, which suits spectators. + +```js +// server/main.js +function startTeamMatch(red, blue) { + Pvp.configure({ who: "teams", friendlyFire: false }); + for (const player of red) player.setPvpTeam("red"); + for (const player of blue) player.setPvpTeam("blue"); +} +``` + +## What lasts + +The rules, the [policy](/guides/pvp/hits-and-defeat/#the-policy-hook) and spell damage belong to the server: they stay when your resource stops, and any resource can change them, so let one resource own them and set them back as it stops. Pairs end with either player's connection. Flags and teams belong to a player's connection, so a player who reconnects comes back unflagged and without a team. + +## Where to go next + +- [Hits and defeat](/guides/pvp/hits-and-defeat/) for how a hit lands, what it costs and what losing does. +- [Duels and wand links](/guides/pvp/duels-and-wand-links/) for one-on-one fights. diff --git a/guides/overview.md b/guides/overview.md index 4dccbc5..de54921 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -39,6 +39,19 @@ Use the navigation to browse the globals available to the selected environment. - [NPCs](/guides/npcs-and-creatures/npcs/) — spawning enemies, controlling their health, targets and size, and their damage and death events. - [Beasts and characters](/guides/npcs-and-creatures/beasts-and-characters/) — tame creatures players care for, and stationary townspeople. +**Encounters:** + +- [Encounter policy](/guides/encounters/encounter-policy/) — whether Overland's camps and roaming encounters are off, spawned by each player's game, or run by the server. +- [Camps](/guides/encounters/camps/) — server-run enemy camps, how they come back, and when they are cleared. +- [World events](/guides/encounters/world-events/) — the roaming encounters, their cooldowns, and starting or resetting one yourself. +- [Zones](/guides/encounters/zones/) — areas the server watches for players and other entities coming and going. + +**PvP:** + +- [Rules and relations](/guides/pvp/rules-and-relations/) — which players may fight: the rules, pairs, flags and teams. +- [Hits and defeat](/guides/pvp/hits-and-defeat/) — how a hit lands, what it costs, the policy hook, kneeling and death, and health recovery. +- [Duels and wand links](/guides/pvp/duels-and-wand-links/) — one-on-one duels and the game's beam contest. + **World:** - [Time and weather](/guides/world/time-and-weather/) — the shared clock, calendar, season and weather, holiday decor and the Headmaster's office. From a230dbf299e782d2821466c613f5e9473bdea798 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:13:12 -0400 Subject: [PATCH 5/9] docs: add physics objects and rebuild Quidditch on server objects Physics objects (World, order 515) for 1.9: creating one, the nearest-client simulator election, settings, pushes and setVelocity, carrying with attachTo, and polling for what happens. Objects and effects gains skeletal-mesh and Blueprint models, moving objects by position, Blueprint objects, and Fx attachTo/detach/setColor. 1.9 removes the client Quidditch global, so the old Mounts and races page goes and a Quidditch section (orders 850-899) builds a match from ring hoops, a physics quaffle, server-moved bludgers and the simulated snitch steered through its state. --- README.md | 2 +- guides/Mounts and races/quidditch.md | 52 ------------ guides/Mounts and races/races.md | 2 +- guides/Quidditch/running-a-match.md | 117 +++++++++++++++++++++++++++ guides/World/objects-and-effects.md | 48 ++++++++++- guides/World/physics-objects.md | 115 ++++++++++++++++++++++++++ guides/overview.md | 8 +- 7 files changed, 284 insertions(+), 60 deletions(-) delete mode 100644 guides/Mounts and races/quidditch.md create mode 100644 guides/Quidditch/running-a-match.md create mode 100644 guides/World/physics-objects.md diff --git a/README.md b/README.md index 3f31808..69ec154 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, Encounters 450–469, PvP 470–499, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, Encounters 450–469, PvP 470–499, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Quidditch 850–899, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Mounts and races/quidditch.md b/guides/Mounts and races/quidditch.md deleted file mode 100644 index e1f066d..0000000 --- a/guides/Mounts and races/quidditch.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Quidditch -description: Spawn a ball, hoops, bludgers, the snitch and house banners on players' clients, and run the match itself on the server. -sidebar: - order: 820 ---- - -The `Quidditch` global spawns the pieces of a match on a client: a ball, hoops, bludgers, the game's snitch and house banners. It is a box of props, not a game. Nothing in it notices a goal, a catch or a hit, and each client spawns its own copy, so the server runs the match: it decides where everything is, tells clients, and works out goals and catches from positions. - -## The pieces - -| Piece | Spawn | Then | -| --- | --- | --- | -| Ball | `spawnBall(meshPath, x, y, z, massKg, probeRadius)` returns a handle, or 0 | `setBallPosition`, `throwBall` (velocity in cm/s), `setBallSimulating`, `attachBall`, `detachBall`, `ballPosition`, `destroyBall` | -| Hoop | `spawnHoop(x, y, z, yaw)` | `clearHoops()` removes them all | -| Bludger | `spawnBludger(x, y, z)` returns an index, or -1 | `moveBludger(index, x, y, z)`, `setBludgerScale(scale)`, `clearBludgers()` | -| Snitch | `spawnSnitch(x, y, z, scale, trailScale)` | `steerSnitch`, `setSnitchRoam`, `setSnitchSpeed`, `snitchPosition`, `despawnSnitch` | -| Banner | `spawnBanner(mesh, x, y, z, yaw, scale)` | `clearBanners()` removes them all | - -- **The ball** is a physics object built from a static mesh you name. It never pushes players about. While the server carries it, turn its physics off with `setBallSimulating(id, false)` and move it with `setBallPosition`; when it is loose, turn physics on and `throwBall` it. `attachBall(id)` puts it in the local player's hand. `setBallPosition` jumps rather than travels, so test a goal against the line between two positions, not against each point. -- **A hoop** is one of the game's race rings. Leave about 11 m of clear air under it, since it stands on hidden pillars. -- **A bludger** is a puppet: `moveBludger` sets where it heads, and it glides there over the next frames. `setBludgerScale` makes them bigger, since they are small to dodge at speed. -- **The snitch** flies itself, at random, inside a box you move with `steerSnitch(x, y, z)`. `setSnitchRoam(extent)` sets how far the box reaches, in centimetres, and `setSnitchSpeed(rate)` how fast it flies, 3 by default. Each client's snitch flies its own way, so the server keeps the true position and checks catches against where clients report theirs. Call `despawnSnitch()` before spawning another. - -```js -// client/main.js -Events.on("match:setup", (pitch) => { - Quidditch.clearAll(); - for (const hoop of pitch.hoops) Quidditch.spawnHoop(hoop.x, hoop.y, hoop.z, hoop.yaw); - Quidditch.spawnSnitch(pitch.centre.x, pitch.centre.y, pitch.centre.z + 2000, 4); -}); - -Events.on("match:snitch", (target) => Quidditch.steerSnitch(target.x, target.y, target.z)); -``` - -## Cleaning up - -The pieces belong to the client, not to your resource, and survive your resource restarting. `Quidditch.clearAll()` destroys everything `Quidditch` spawned and returns how many, so call it before setting up a match and as your resource stops: - -```js -// client/main.js -Events.on("resourceStop", (resourceName) => { - if (resourceName === "quidditch-match") Quidditch.clearAll(); -}); -``` - -Players are not put on brooms by any of this: [Brooms and mounts](/guides/mounts-and-races/brooms-and-mounts/#riding-a-broom) covers giving them one and getting them in the air. - -## Where to go next - -- [Brooms and mounts](/guides/mounts-and-races/brooms-and-mounts/) for the players' side of the match. -- [Map markers](/guides/user-interface/map-markers/) for showing players where the pitch is. diff --git a/guides/Mounts and races/races.md b/guides/Mounts and races/races.md index 7ed8f60..7b2e7e4 100644 --- a/guides/Mounts and races/races.md +++ b/guides/Mounts and races/races.md @@ -96,4 +96,4 @@ Events.on("resourceStop", (resourceName) => { ## Where to go next - [Brooms and mounts](/guides/mounts-and-races/brooms-and-mounts/) for giving players brooms and controlling how they fly. -- [Quidditch](/guides/mounts-and-races/quidditch/) for balls, hoops and a snitch. +- [Quidditch](/guides/quidditch/running-a-match/) for a match with a quaffle, hoops, bludgers and the snitch. diff --git a/guides/Quidditch/running-a-match.md b/guides/Quidditch/running-a-match.md new file mode 100644 index 0000000..14f41a8 --- /dev/null +++ b/guides/Quidditch/running-a-match.md @@ -0,0 +1,117 @@ +--- +title: Running a match +description: Build a Quidditch match from server objects, with race rings for hoops, a physics quaffle players carry and throw, bludgers the server moves, and the game's snitch flown for everyone. +sidebar: + order: 850 +--- + +There is no Quidditch game mode built in. A match is server code built from [world objects](/guides/world/objects-and-effects/): race rings for hoops, a [physics object](/guides/world/physics-objects/) for the quaffle, objects the server moves for the bludgers, and the game's own snitch. The server keeps the score and decides goals, catches and hits from where everything is. Players fly with their own [brooms](/guides/mounts-and-races/brooms-and-mounts/). + +Run the match on a timer on the server, around 20 times a second, and keep every decision there. + +## The pitch + +Hoops are the game's race ring, placed as a Blueprint: + +```js +// server/main.js +const RING = "/Game/Gameplay/BroomRaces/Blueprints/BP_RaceRing.BP_RaceRing_C"; + +function placeHoops(hoops) { + return hoops.map((hoop, i) => WorldObject.create(`match:hoop:${i}`, RING, { x: hoop.x, y: hoop.y, z: hoop.z, yaw: hoop.yaw })); +} +``` + +A ring stands on hidden pillars about 11 m below its opening, so keep hoops at least 13 m above the ground. The ring itself notices only players, never the ball, so goals are yours to decide. + +House banners and other dressing are plain world objects with `collision: false`. + +## The quaffle + +The quaffle is a physics object, created from the game's toy quaffle: + +```js +// server/main.js +const QUAFFLE = "/Game/Gameplay/Nurturing/Creatures/Meshes/Toys/SM_Sanc_BeastToy_QuidditchQuaffle.SM_Sanc_BeastToy_QuidditchQuaffle"; +const quaffle = WorldObject.create("match:quaffle", QUAFFLE, { x: 0, y: 0, z: 2000, physics: true, massKg: 2 }); +``` + +- **Picking it up:** a player's client asks with an event; the server checks they are close enough and allowed, then calls `quaffle.attachTo(player, "WandSocket")`. +- **Throwing:** the client sends only where the player is aiming, from `LocalPlayer.getControlRotation()`. The server works out the speed itself, within limits, and calls `quaffle.detach()` and `quaffle.setVelocity(...)`. +- **Dropping it** when the carrier is hit or leaves: `quaffle.detach()`. +- **Resetting** after a goal: `detach()`, set `position`, then `setVelocity(0, 0, 0)`. + +[Carrying it](/guides/world/physics-objects/#carrying-it) shows the pick-up and throw handlers. + +## Goals + +A loose quaffle's position comes from the player simulating it, and it can move several metres between two checks. Test whether the line from its last position to its new one passes through a hoop's opening, rather than whether a single position is inside it: + +```js +// server/main.js +function crossedHoop(hoop, from, to, radius) { + const yaw = (hoop.yaw * Math.PI) / 180; + const nx = Math.cos(yaw); + const ny = Math.sin(yaw); + const side = (p) => (p.x - hoop.x) * nx + (p.y - hoop.y) * ny; + const a = side(from); + const b = side(to); + if (a === b || Math.sign(a) === Math.sign(b)) return false; + const t = a / (a - b); + const x = from.x + (to.x - from.x) * t; + const y = from.y + (to.y - from.y) * t; + const z = from.z + (to.z - from.z) * t; + return Math.hypot(x - hoop.x, y - hoop.y, z - hoop.z) <= radius; +} +``` + +While a player carries the quaffle, use their position instead, and decide whether flying it through a hoop counts. + +## Bludgers + +Bludgers are world objects the server moves itself, not physics objects: each tick, work out where each one heads and set its `position`, and every player sees it glide there. A hit is the server finding a bludger close enough to a player. + +```js +// server/main.js +const BLUDGER = "/Game/RiggedObjects/Props/Quidditch/Bludger/SK_Quid_Bludger.SK_Quid_Bludger"; +const bludger = WorldObject.create("match:bludger:0", BLUDGER, { x: 0, y: 0, z: 2500, scale: 4, collision: false }); +``` + +## The snitch + +The snitch is the game's own loose snitch, created as a simulated Blueprint so that one player's game flies it and everyone else sees the same flight: + +```js +// server/main.js +const SNITCH = "/Game/Levels/WorldEvents/Blueprints/LooseSnitch/BP_Loose_Snitch.BP_Loose_Snitch_C"; +const snitch = WorldObject.create("match:snitch", SNITCH, { x: 0, y: 0, z: 3000, scale: 4, simulated: true }); +``` + +It flies itself, in arcs, inside a box. Steer it a few times a second by writing where the box is, how big it is and how fast the snitch flies to its [state](/guides/core-concepts/state-bags/): + +```js +// server/main.js +function steerSnitch(target) { + if (!snitch) return; + snitch.state.set("snitch.x", target.x); + snitch.state.set("snitch.y", target.y); + snitch.state.set("snitch.z", target.z); + snitch.state.set("snitch.roam", 3000); // the box's size, in centimetres + snitch.state.set("snitch.speed", 3); // how fast it flies, 3 by default +} +``` + +A catch is the server finding a seeker within a few metres of `snitch.position` for long enough, say two seconds. That position comes from the player flying the snitch. + +## Trust + +The quaffle's and snitch's positions are reported by whichever player's game simulates them, and the server takes their word. Keep every decision on the server, check distances there, and treat what clients send, such as aim, as requests. + +## Cleaning up + +The objects belong to the resource that created them and are destroyed when it stops. Let go of the quaffle when its carrier disconnects, or nobody simulates it. + +## Where to go next + +- [Physics objects](/guides/world/physics-objects/) for simulation, pushes and carrying. +- [Brooms and mounts](/guides/mounts-and-races/brooms-and-mounts/) for getting the players in the air. diff --git a/guides/World/objects-and-effects.md b/guides/World/objects-and-effects.md index 035ab83..c38cb35 100644 --- a/guides/World/objects-and-effects.md +++ b/guides/World/objects-and-effects.md @@ -1,11 +1,11 @@ --- title: Objects and effects -description: Place props from the game's meshes and visual effects from its effect systems, keyed so you can replace or remove them later. +description: Place props from the game's meshes and Blueprints, move them, and add visual effects that stay put or follow an entity, keyed so you can replace or remove them later. sidebar: order: 510 --- -The server can place two kinds of decoration that every nearby player sees: world objects, built from the game's static meshes, and effects, built from its particle systems. Both are created under a key of your choosing: creating again with the same key replaces the old one, and the key removes it later. +The server can place two kinds of thing that every nearby player sees: world objects, built from the game's meshes and Blueprints, and effects, built from its particle systems. Both are created under a key of your choosing: creating again with the same key replaces the old one, and the key removes it later. Keys are shared by every resource, so start yours with your resource's name. ## World objects @@ -18,7 +18,7 @@ WorldObject.create("market-barrel-1", "/Game/Environment/BanditCamps/Interactabl }); ``` -`WorldObject.create(key, model, options)` takes the model as a static-mesh asset path, `/Game//.`, or as a numeric id from the Mod's catalog of world-object meshes, and throws for anything else. It returns the object, or `undefined` if it could not be created. +`WorldObject.create(key, model, options)` takes the model as a mesh's asset path, `/Game//.`, a Blueprint's class path, `/Game//BP_Name.BP_Name_C`, or a numeric id from the Mod's catalog of world-object meshes, and throws for anything else. It returns the object, or `undefined` if it could not be created. | Option | Meaning | | --- | --- | @@ -29,8 +29,29 @@ WorldObject.create("market-barrel-1", "/Game/Environment/BanditCamps/Interactabl | `promptText` | A floating line, seen only within `promptRange`, usually inviting players to interact | | `promptHeight` | Where the prompt floats, in centimetres above the object's origin: 150 by default | | `promptRange` | How close a player must be to see the prompt, in centimetres: 250 by default | +| `fx`, `fxSocket` | An effect's path to attach to the object as it spawns, and the socket on its mesh to attach it to | +| `physics`, `massKg` | Makes a mesh object a [physics object](/guides/world/physics-objects/) that falls, rolls and can be thrown, and its mass | +| `simulated` | For a Blueprint, lets one player's game run it for everyone; see [Blueprint objects](#blueprint-objects) | -The prompt is only text; handling the interaction it invites is up to your resource. `WorldObject.destroy(key)` removes an object by its key, `object.destroy()` removes one through its handle, and `WorldObjectManager.getAll()` lists every live object. `object.modelPath` reads back the mesh it shows, whichever form you created it with. +The prompt is only text; handling the interaction it invites is up to your resource. `WorldObject.destroy(key)` removes an object by its key, `object.destroy()` removes one through its handle, and `WorldObjectManager.getAll()` lists every live object. `object.modelPath` reads back the model it shows, whichever form you created it with. + +### Moving objects + +Set `object.position` to move an object. It glides to the new place on every player's screen: + +```js +// server/main.js +const lift = WorldObject.create("tower-lift", "/Game/Environment/BanditCamps/Interactables/SM_Int_BCProps_Barrel_001_W.SM_Int_BCProps_Barrel_001_W", { x: 0, y: 0, z: 0 }); +let up = false; +setInterval(() => { + up = !up; + if (lift) lift.position = new Vector3(0, 0, up ? 800 : 0); +}, 5000); +``` + +### Blueprint objects + +A Blueprint object runs the game's own logic for that Blueprint. Placed normally, every player's game runs its own copy, so anything that moves by itself moves differently for each player. With `simulated: true`, the game of the nearest player runs it and everyone else follows. The game's race ring, `/Game/Gameplay/BroomRaces/Blueprints/BP_RaceRing.BP_RaceRing_C`, and loose snitch, `/Game/Levels/WorldEvents/Blueprints/LooseSnitch/BP_Loose_Snitch.BP_Loose_Snitch_C`, are known to work; [Quidditch](/guides/quidditch/running-a-match/) builds a match from them. Other Blueprints spawn, but may not behave. ## Effects @@ -46,6 +67,25 @@ Fx.create("spawn-flash", flashPath, { x, y, z, scale: 2, ttl: 5 }); `Fx.catalog()` lists effects known to work, each with a category such as fire, explosions, smoke, weather, water, ambient or magic, a name and its path. It is a starting point rather than a limit: any well-formed effect path spawns. `FxManager.getAll()` lists every live effect. +### Attaching effects + +`effect.attachTo(entity, offset)` makes an effect follow a player, NPC, beast, world object or character, turning with it, at an `offset` in centimetres from its centre. It is destroyed with the entity, for example when a player leaves. `effect.detach()` leaves it where it is, and `effect.attachedTo` is the entity's id, or 0. + +```js +// server/main.js +function lightTorch(player) { + const torch = Fx.create(`torch:${player.id}`, campfire, { scale: 0.3 }); + if (!torch) return undefined; + torch.setVirtualWorld(player.virtualWorld); + torch.attachTo(player, { x: 30, y: 25, z: 70 }); + return torch; +} +``` + +An attached effect does not take its entity's virtual world, so set it to match. + +`effect.setColor(name, color)` sets one of a Niagara effect's colour parameters, by the parameter's name, for everyone. Components run from 0 to 1, and values above 1 glow. An effect can carry up to four colours, and Cascade effects ignore them. + ## Lifetime Objects and effects your resource creates are destroyed when it stops, along with its other entities. They are not saved: a resource that keeps placed props across restarts stores their keys, models and positions itself and creates them again when it starts. diff --git a/guides/World/physics-objects.md b/guides/World/physics-objects.md new file mode 100644 index 0000000..8a558f8 --- /dev/null +++ b/guides/World/physics-objects.md @@ -0,0 +1,115 @@ +--- +title: Physics objects +description: Make world objects that fall, roll and fly, push and throw them from the server, and let players carry them. +sidebar: + order: 515 +--- + +A physics object is a [world object](/guides/world/objects-and-effects/#world-objects) that falls, rolls and bounces. The server runs no physics itself: the game of the nearest player simulates the object and reports where it goes, and everyone else follows. The server pushes, freezes and hands out objects, and decides what happens to them. + +## Creating one + +Pass `physics: true` when creating a world object from a mesh: + +```js +// server/main.js +const QUAFFLE = "/Game/Gameplay/Nurturing/Creatures/Meshes/Toys/SM_Sanc_BeastToy_QuidditchQuaffle.SM_Sanc_BeastToy_QuidditchQuaffle"; + +const ball = WorldObject.create("park:ball", QUAFFLE, { x, y, z: z + 100, physics: true, massKg: 0.5 }); +``` + +`massKg` sets its mass; without it, the mesh's own. A Blueprint cannot be a physics object, and `create` throws for one. The mesh needs collision of its own for the object to land on anything. + +Players walk through physics objects and never push them, so anything that moves one is your script. + +## Who simulates it + +The nearest player within 150 m, measured along the ground, in the object's virtual world, simulates it. They keep it until they are more than 250 m away or someone else is clearly nearer. `object.simulatorId` is that player's id. With nobody near, the object rests where it was last reported until someone comes. + +The simulating player's game reports where the object is, and the server takes its word. Anything you decide from a physics object's position, such as a goal, trusts that player. + +## Settings + +| Setting | Default | Does | +| --- | --- | --- | +| `physics` | `true` | `false` stops simulating; the object stays where it is | +| `frozen` | `false` | Holds the object still in mid-air and ignores pushes, without giving up its simulator | +| `gravity` | `true` | Whether it falls | +| `mass` | The mesh's | In kilograms; 0 or less uses the mesh's own | +| `linearDamping`, `angularDamping` | The mesh's | How quickly it slows down and stops spinning | + +Use `frozen` to hold an object until it should fall, rather than turning `physics` off and on: + +```js +// server/main.js +const crate = WorldObject.create("dock:crate", "/Game/Environment/BanditCamps/Interactables/SM_Int_BCProps_Barrel_001_W.SM_Int_BCProps_Barrel_001_W", { + x, y, z: z + 600, + physics: true, + massKg: 40, + promptText: "[F] Cut the rope", +}); +if (crate) crate.frozen = true; + +Events.onClient("dock:cut", (player) => { + if (crate) crate.frozen = false; +}); +``` + +## Pushing it + +| Call | Does | +| --- | --- | +| `object.setVelocity(x, y, z)` | Sets its velocity in cm/s. It is kept on the object, so it applies even while nobody simulates it | +| `object.applyImpulse(x, y, z, options)` | A sudden push | +| `object.applyForce(x, y, z, options)` | A push for one physics step | +| `object.setLinearVelocity`, `object.setAngularVelocity` | Set its speed or spin | +| `object.applyTorque`, `object.applyAngularImpulse` | Spin it | + +All but `setVelocity` go to the player simulating the object, and return `false` when nobody does; `true` means it was sent. `options` can set `velocityChange: true` to ignore mass, `space: "local"` to push along the object's own axes, and `at` to push at a point rather than its centre. + +`object.getLinearVelocity()` and `object.getAngularVelocity()` read what the simulating player last reported. + +Moving an object by setting `object.position` keeps its speed, so follow it with `setVelocity(0, 0, 0)` to put it down at rest. Physics is unreliable above about 5000 cm/s, so keep throws below that. + +## Carrying it + +`object.attachTo(player, socket)` puts a physics object in a player's hand, at a socket such as `WandSocket`, and that player's game takes it over. `object.detach()` lets go, and `setVelocity` straight after throws it. `object.ownerId` is the carrier, or the simulating player. + +```js +// server/main.js +let carrierId = null; + +Events.onClient("ball:grab", (player) => { + if (carrierId !== null || !ball) return; + const p = player.position; + const b = ball.position; + if (Math.hypot(p.x - b.x, p.y - b.y, p.z - b.z) > 300) return; + carrierId = player.id; + ball.attachTo(player, "WandSocket"); +}); + +Events.onClient("ball:throw", (player, aim) => { + if (carrierId !== player.id || !ball || !aim) return; + carrierId = null; + const length = Math.hypot(aim.x, aim.y, aim.z) || 1; + ball.detach(); + ball.setVelocity((aim.x / length) * 2500, (aim.y / length) * 2500, (aim.z / length) * 2500); +}); + +Events.on("playerDisconnect", (player) => { + if (carrierId !== player.id || !ball) return; + carrierId = null; + ball.detach(); +}); +``` + +Let go of an object when its carrier disconnects: until you do, nobody simulates it. + +## Noticing what happens + +Physics objects raise no events of their own: nothing fires when one lands, hits something or is picked up. Check their positions on a timer, or put a [zone](/guides/encounters/zones/) that tracks `"worldObject"` where something should happen. A zone is checked ten times a second, so for fast objects test the line between two positions instead. + +## Where to go next + +- [Objects and effects](/guides/world/objects-and-effects/) for placing objects and moving them by hand. +- [Quidditch](/guides/quidditch/running-a-match/) for a match built from physics objects. diff --git a/guides/overview.md b/guides/overview.md index de54921..fc5dd9d 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -55,7 +55,8 @@ Use the navigation to browse the globals available to the selected environment. **World:** - [Time and weather](/guides/world/time-and-weather/) — the shared clock, calendar, season and weather, holiday decor and the Headmaster's office. -- [Objects and effects](/guides/world/objects-and-effects/) — placing props from the game's meshes and visual effects. +- [Objects and effects](/guides/world/objects-and-effects/) — placing and moving props from the game's meshes and Blueprints, and visual effects that can follow an entity. +- [Physics objects](/guides/world/physics-objects/) — objects that fall, roll and fly, pushing and throwing them, and letting players carry them. - [Doors](/guides/world/doors/) — holding the game's doors locked or open for everyone, and deciding who gets in. - [Loot](/guides/world/loot/) — server-owned chests, moonstones and foragables, per-player contents, and switching off the game's own loot. @@ -81,7 +82,10 @@ Use the navigation to browse the globals available to the selected environment. - [Brooms and mounts](/guides/mounts-and-races/brooms-and-mounts/) — giving players brooms and creatures, mounting and dismounting, flight overrides, and keeping riders out. - [Races](/guides/mounts-and-races/races/) — the game's broom races on tracks of your own, for several players, timed by the server. -- [Quidditch](/guides/mounts-and-races/quidditch/) — the ball, hoops, bludgers and snitch, with the match run on the server. + +**Quidditch:** + +- [Running a match](/guides/quidditch/running-a-match/) — hoops, a quaffle players carry and throw, bludgers and the snitch, all run on the server. **Running a server:** From 000f050122f22aea34690d6abfb1fafb03a73e61 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:15:26 -0400 Subject: [PATCH 6/9] docs: add PvE guides and update NPCs for 1.9 PvE section (orders 430-449): how several players fight one NPC (owner AI, relayed attacks recomputed on the victim, guest damage applied by the owner, hand-overs that keep the fight), npcDamaged and the 1.9 npcDied with killer and damagers, damage multipliers for NPCs and players, no regeneration and the 12% finish, boss phases; reward policies, their precedence across NPC, camp, world event and default, and drop or inventory delivery. NPCs gains ownership ranges, the 1.9 forceTarget and launch, and the five behaviour switches. --- README.md | 2 +- guides/Encounters/camps.md | 2 +- guides/Encounters/world-events.md | 2 +- guides/NPCs and creatures/npcs.md | 75 +++++++++++++++-------------- guides/PvE/fighting-npcs.md | 79 +++++++++++++++++++++++++++++++ guides/PvE/rewards-and-loot.md | 76 +++++++++++++++++++++++++++++ guides/overview.md | 7 ++- 7 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 guides/PvE/fighting-npcs.md create mode 100644 guides/PvE/rewards-and-loot.md diff --git a/README.md b/README.md index 69ec154..a8e64a8 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The closed-source Mod remains authoritative for generated client and server API ## Structure -- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–449, Encounters 450–469, PvP 470–499, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Quidditch 850–899, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. +- `guides/` contains scripting concepts and tutorials. Each subdirectory is a sidebar section named after the directory (`guides/Getting started/first-resource.md` is served at `/guides/getting-started/first-resource/`). Sections are ordered by the lowest `sidebar.order` among their pages, so each section owns a range: Getting started 100–199, Core concepts 200–299, Players 300–399, NPCs and creatures 400–429, PvE 430–449, Encounters 450–469, PvP 470–499, World 500–549, Room of Requirement 550–599, User interface 600–699, Camera and sound 700–799, Mounts and races 800–849, Quidditch 850–899, Hosting a server 900–999. A section added between two takes the free part of a hundred, such as 450–499. - Image directories live beside the Markdown document that references them. - `docs.config.json` owns the generator pin, branding, links, navigation, and community-content mapping. - `scripts/sync_contract.mjs` downloads and verifies the public scripting contract. diff --git a/guides/Encounters/camps.md b/guides/Encounters/camps.md index c75ddea..9d2380d 100644 --- a/guides/Encounters/camps.md +++ b/guides/Encounters/camps.md @@ -57,7 +57,7 @@ An enemy counts as dead once its NPC is gone, so destroying one from a script co ## What camps pay -Camp enemies pay out like any NPC. `Camps.setRewardPolicy(policy)` sets what every camp's enemies pay, and `Camps.setRewardPolicy(policy, campId)` what one camp's do. A camp's own policy wins, then the one for every camp, then `NPC.setDefaultRewardPolicy`. `Camps.getRewardPolicy(campId)` reads back what applies. +Camp enemies pay out like any NPC, by the [reward policies](/guides/pve/rewards-and-loot/) that apply to them. `Camps.setRewardPolicy(policy)` sets what every camp's enemies pay, and `Camps.setRewardPolicy(policy, campId)` what one camp's do. A camp's own policy wins, then the one for every camp, then `NPC.setDefaultRewardPolicy`. `Camps.getRewardPolicy(campId)` reads back what applies. ```js // server/main.js diff --git a/guides/Encounters/world-events.md b/guides/Encounters/world-events.md index ece446e..e929e4f 100644 --- a/guides/Encounters/world-events.md +++ b/guides/Encounters/world-events.md @@ -78,7 +78,7 @@ Events.on("worldEventStarted", (spotId, kind) => { ## What world events pay -`WorldEvents.setRewardPolicy(policy)` sets what every world event's enemies pay, falling back to `NPC.setDefaultRewardPolicy`, and `WorldEvents.getRewardPolicy()` reads it back. There is one policy for all of them. +`WorldEvents.setRewardPolicy(policy)` sets the [reward policy](/guides/pve/rewards-and-loot/) for every world event's enemies, falling back to `NPC.setDefaultRewardPolicy`, and `WorldEvents.getRewardPolicy()` reads it back. There is one policy for all of them. ```js // server/main.js diff --git a/guides/NPCs and creatures/npcs.md b/guides/NPCs and creatures/npcs.md index 3ac33ed..81dcce7 100644 --- a/guides/NPCs and creatures/npcs.md +++ b/guides/NPCs and creatures/npcs.md @@ -1,11 +1,11 @@ --- title: NPCs -description: Spawn the game's enemies as replicated NPCs, control their health, targets and size, and react when they are hurt or die. +description: Spawn the game's enemies as replicated NPCs, control their health, targets, size and behaviour, and read what they are doing. sidebar: order: 400 --- -`NPC.create` spawns one of the game's enemies as a replicated NPC that every nearby player sees and fights. The server decides that it exists and holds what you set on it; its AI runs on one nearby player's client. +`NPC.create` spawns one of the game's enemies as a replicated NPC that every nearby player sees and fights. The server decides that it exists and holds what you set on it; its AI runs on one nearby player's client. [Fighting NPCs](/guides/pve/fighting-npcs/) covers what happens when players fight it, and [Rewards and loot](/guides/pve/rewards-and-loot/) what its death pays. ## Spawning @@ -20,73 +20,72 @@ if (grunt) { `NPC.create(enemyId, x, y, z, disposition, preferredOwnerId)` takes: -- `enemyId`: one of the game's enemy ids, such as `Goblin_Grunt`, or an enemy's class path. The server does not check it for you, so check its spelling. +- `enemyId`: one of the game's enemy ids, one of the Mod's short names such as `Goblin_Grunt`, or an enemy's class path. The server does not check it for you: a misspelled id returns an NPC that never appears. - A position in world centimetres. - `disposition`: `"hostile"` (the default) or `"friendly"`. - `preferredOwnerId`: the player whose client should run its AI. It is used only if that player can see world 0 when the NPC spawns; otherwise the server picks. It returns the NPC, or `undefined` if it could not be created. `setScale` takes effect best right after `create`, before the NPC first reaches anyone. Pass `"visual"` as its second argument to resize only the body and keep native collision. -NPCs start in [virtual world](/guides/core-concepts/virtual-worlds/#placing-entities) 0, and the NPCs a resource creates are destroyed when that resource stops. +NPCs start in [virtual world](/guides/core-concepts/virtual-worlds/#placing-entities) 0. The NPCs a resource creates are destroyed when that resource stops; [camp](/guides/encounters/camps/) and [world event](/guides/encounters/world-events/) enemies, and enemies an NPC summons, belong to no resource. ## Who runs the AI -An NPC's AI runs on the client of one nearby player, its owner: `npc.ownerId` names them, and is 0 while the NPC is dormant because no player can see it. The server hands the NPC to another player as people come and go, and what you set on the server carries over. +An NPC's AI runs on the client of one nearby player, its owner: `npc.ownerId` names them. The server picks the nearest player within about 300 m and lets them go once they are more than 400 m away; with nobody in range the NPC is dormant and `ownerId` is 0. An NPC in a fight keeps its owner for up to 20 seconds even when someone else comes nearer. -Some readings come back from the owner's client. `npc.health` and `npc.maxHealth` are what the owner last reported, and read 0 until a client has embodied the NPC. `npc.targetPlayerId` and `npc.targetType` (0 none, 1 attack, 2 flee) report what the NPC is actually doing. +When the owner changes, the new owner takes over the NPC where it stands: its health, its fight, a raised shield and effects that are running all carry over, and so does everything you set on the server. + +Some readings come back from the owner's client, about four times a second. `npc.health` and `npc.maxHealth` are what the owner last reported, and read 0 until a client has loaded the NPC. `npc.targetPlayerId` and `npc.targetType` (0 none, 1 attack, 2 flee) report what the NPC is actually doing. ## Controlling an NPC | Call | Does | | --- | --- | -| `setMaxHealth(amount)` | Holds the NPC's max health, across owner hand-offs and level travel. 0 or less restores its own. | +| `setMaxHealth(amount)` | Holds the NPC's max health, across owner hand-offs and level travel. | | `setHealth(amount)` | Sets current health once. Does nothing while the NPC is dormant. | | `setDisposition("hostile" \| "friendly")` | Turns the NPC against players, or out of combat. | | `setScale(scale, mode)` | Resizes it on every client; `mode` is `"full"` (the default) or `"visual"`. | -| `forceTarget(playerId, holdSeconds)` | Makes it target one player for `holdSeconds`: 3 by default, at most 60. `0` releases it. | +| `forceTarget(playerId, holdSeconds)` | Makes it fight one player, the way the game's taunt does, until you pass `0` or that player dies. | +| `launch(x, y, z, options)` | Throws it through the air, as a velocity in cm/s added to its own. | | `kill()` | Plays its death on every client, then despawns it. | | `destroy()` | Removes it at once, with no death. | -```js -// server/main.js -function startBossFight(boss, challenger) { - boss.setMaxHealth(5000); - boss.forceTarget(challenger.id, 30); -} -``` +An NPC never gets its health back by itself, and its owner finishes off any NPC at or below 12% of its max health. A `setHealth` that low kills it, and that death counts as the last hit's. -The owner's client applies the force, so read `targetPlayerId` back to see whether it held. +`forceTarget` does nothing on a dormant or dead NPC, and waits while the NPC's AI is paused. When the forced player is more than 55 m from the owner's player, the NPC moves to the forced player's client until the force ends. A force is not cleared when its player disconnects, so release it yourself in `playerDisconnect`. Read `targetPlayerId` to see whether it held. -## Damage and death events +`launch` returns `false` for a dormant NPC; `true` means it was sent. Pass `{ overrideXY: true }` or `{ overrideZ: true }` to replace the NPC's own speed instead of adding to it, and `{ space: "local" }` to launch along its own facing. -- `npcDamaged` gives the NPC's id, the attacking player's id and the amount, clamped and rate-capped by the server. The attacker's client reports it, so treat it as a claim. -- `npcDied` gives the NPC's id and its enemy id. +## Switching behaviour off -To credit the players who fought an NPC, add up its damage as it arrives and settle when it dies: +Five properties switch parts of an NPC's behaviour off. They are held on the server, can be set before the NPC has an owner, and carry over hand-offs: + +| Setting | Effect | +| --- | --- | +| `npc.aiPaused = true` | It stops pathing and starts no new attack or dodge, and idles in place, but still reacts to hits and blocks. | +| `npc.combatEnabled = false` | It keeps chasing its target but never starts an attack, though it can still counter after a block or dodge. | +| `npc.perceptionEnabled = false` | It stops noticing players, drops its targets and idles. A forced target overrides this. | +| `npc.movementFrozen = true` | It cannot move, and `launch` does nothing. | +| `npc.animationFrozen = true` | It freezes in place on every client, pose and all. | ```js // server/main.js -const damageByNpc = new Map(); // npc id -> Map(player id -> damage) - -Events.on("npcDamaged", (npcId, attackerId, amount) => { - const dealt = damageByNpc.get(npcId) ?? new Map(); - dealt.set(attackerId, (dealt.get(attackerId) ?? 0) + amount); - damageByNpc.set(npcId, dealt); -}); - -Events.on("npcDied", (npcId, enemyId) => { - const dealt = damageByNpc.get(npcId); - damageByNpc.delete(npcId); - if (!dealt) return; - for (const player of PlayerManager.getAll()) { - if (dealt.has(player.id)) player.sendChat(`You helped defeat ${enemyId}.`); - } -}); +function trainingDummy(x, y, z) { + const dummy = NPC.create("Goblin_Grunt", x, y, z, "hostile"); + if (!dummy) return undefined; + dummy.aiPaused = true; + dummy.combatEnabled = false; + dummy.movementFrozen = true; + dummy.setMaxHealth(100000); + return dummy; +} ``` -The player ids are network ids, and a player may have left by the time you read one, so look them up among the online players rather than constructing a `Player` from the id. +## Events + +`npcDamaged` fires as players hurt an NPC, and `npcDied` when it dies, with who killed it and who fought it. [Fighting NPCs](/guides/pve/fighting-npcs/#damage-and-death) describes both. ## Where to go next +- [Fighting NPCs](/guides/pve/fighting-npcs/) for what happens when several players fight one. - [Beasts and characters](/guides/npcs-and-creatures/beasts-and-characters/) for tame creatures and stationary townspeople. -- [Authority](/guides/core-concepts/authority/) for which client runs an NPC's AI and what that means. diff --git a/guides/PvE/fighting-npcs.md b/guides/PvE/fighting-npcs.md new file mode 100644 index 0000000..6bf17b4 --- /dev/null +++ b/guides/PvE/fighting-npcs.md @@ -0,0 +1,79 @@ +--- +title: Fighting NPCs +description: How several players fight one NPC together, what the server decides and what clients report, damage and death events, damage multipliers, and bosses. +sidebar: + order: 430 +--- + +Any number of players can fight the same [NPC](/guides/npcs-and-creatures/npcs/). Each sees it attack them and can hurt it, the NPC keeps one health bar, and its death credits the players who fought it. + +## How a fight works + +One player's game, the NPC's owner, runs the NPC's AI and the game's own combat code. Every other player sees a copy that follows the owner's. + +- **The NPC attacking another player:** its swing, spell or thrown weapon is passed through the server to the player it targets. That player's own game works out the hit from its copy of the NPC, so damage depends on the victim's level, gear and multipliers. No damage number travels between players. +- **Another player hurting the NPC:** their game measures the damage it did to its copy and reports it to the server, which passes it to the owner to apply. Shield breaks, killing blows, status effects and hit reactions travel the same way, and everyone sees the NPC's real health on its health bar. +- **The owner changing mid-fight:** the new owner takes over where the old one left off. The NPC keeps its health, its fight, a raised shield and running effects. + +The server checks that relayed attacks come from the NPC's owner and caps how many arrive. It drops NPC hits on players who are invulnerable. + +## Damage and death + +`npcDamaged` fires with the NPC's id, the attacking player's id and the amount, for every hit any player lands, the owner's player included. `npcDied` fires with the NPC's id, its enemy id, the id of the player who killed it, or `null`, and the ids of every player who damaged it: + +```js +// server/main.js +function onlinePlayer(id) { + return PlayerManager.getAll().find((player) => player.id === id); +} + +Events.on("npcDied", (npcId, enemyId, killerId, damagerIds) => { + const killer = killerId === null ? undefined : onlinePlayer(killerId); + if (killer) killer.sendChat(`You defeated ${enemyId}.`); + for (const id of damagerIds) { + if (id !== killerId) onlinePlayer(id)?.sendChat(`You helped defeat ${enemyId}.`); + } +}); +``` + +- The killer is the last player to damage the NPC within 15 seconds of its death. +- `npcDied` arrives half a second after the death; the NPC's handle still works in the handler, with `alive` false. `npc.kill()` raises it with no killer, and `npc.destroy()` raises nothing. +- The ids are network ids. A player may have left by the time you read one, so look them up among the online players. + +These are reports from players' games. `npcDamaged` amounts are what the attacker's game measured, capped at 10,000 a hit and 20,000 a second per player, and can be more than the NPC actually lost. Health and death come from the owner's game. A changed game can claim damage it did not do, so be careful what you hang on them. + +## Damage multipliers + +Both sides of a fight between a player and a server NPC have multipliers, set on the server: + +| Call | Scales | +| --- | --- | +| `npc.setDamageGivenMultiplier(n)` | The damage the NPC deals to players | +| `npc.setDamageTakenMultiplier(n)` | The damage players deal to the NPC | +| `player.setDamageGivenMultiplier(n)` | The damage the player deals, to NPCs and in [PvP](/guides/pvp/hits-and-defeat/#damage) | +| `player.setDamageTakenMultiplier(n)` | The damage the player takes from NPCs and in PvP | + +The final damage is the attacker's given multiplier times the victim's taken multiplier, on top of the game's own gear, potions and difficulty. `null` restores 1, and a negative value throws. `damageGivenMultiplier` and `damageTakenMultiplier` read them back. + +An NPC's multipliers carry over hand-offs and travel. A player's last until they reconnect. They do not touch fights between NPCs, falls, or enemies a player's own game spawns under the [`"native"` encounter policy](/guides/encounters/encounter-policy/). + +```js +// server/main.js +// An escort nobody can hurt, whose quest fails if it dies some other way. +const escort = NPC.create("Goblin_Grunt", x, y, z, "friendly"); +escort?.setDamageTakenMultiplier(0); +escort?.setMaxHealth(4000); +``` + +## Health + +Server NPCs never get their health back by themselves, and the owner finishes off any NPC at or below 12% of its max health. Players do get theirs back; [Health recovery](/guides/pvp/hits-and-defeat/#health-recovery) covers turning that off for a hardcore mode. + +## Bosses + +Bosses keep the game's health phases. A troll such as `Troll_Crate` stops taking damage at two thirds and one third of its health for a moment, as in single player, before the fight goes on. `npc.kill()` always lands, phases or not. + +## Where to go next + +- [Rewards and loot](/guides/pve/rewards-and-loot/) for what an NPC's death pays. +- [NPCs](/guides/npcs-and-creatures/npcs/) for spawning NPCs and switching their behaviour off. diff --git a/guides/PvE/rewards-and-loot.md b/guides/PvE/rewards-and-loot.md new file mode 100644 index 0000000..af4899c --- /dev/null +++ b/guides/PvE/rewards-and-loot.md @@ -0,0 +1,76 @@ +--- +title: Rewards and loot +description: Decide what an NPC's death pays and to whom, with reward policies for one NPC, for camps, for world events and for every NPC. +sidebar: + order: 435 +--- + +The server decides what a server NPC's death pays. By default, the player who kills it earns the game's own kill reward: Field Guide knowledge, experience and kill-challenge progress, and the game's own loot bag. A reward policy changes what is paid and to whom. + +## Reward policies + +```js +// server/main.js +NPC.setDefaultRewardPolicy({ recipients: "damagers", experience: 50 }); + +grunt?.setRewardPolicy({ + loot: [{ itemId: "LacewingFlies", count: [1, 3], chance: 0.5 }], + lootDelivery: "inventory", +}); +``` + +| Field | Default | Meaning | +| --- | --- | --- | +| `recipients` | `"killer"` | `"killer"`, `"damagers"` (everyone who hit it) or `"nearby"` (everyone who hit it, plus every player within `radius` of the body who can see it) | +| `radius` | 3000 | For `"nearby"`, in centimetres, from 100 to 50000 | +| `gameReward` | `true` | Whether to pay the game's own kill reward | +| `experience` | 0 | Flat experience on top, a whole number up to 1,000,000 | +| `loot` | `null` | Items to give, up to 32: each an `itemId`, a `count` or `[min, max]` range, and a `chance` from 0 to 1 | +| `lootMode` | `"each"` | `"each"`: every recipient gets a roll of their own. `"shared"`: one roll for the group. | +| `lootDelivery` | `"drop"` | `"drop"` leaves a loot bag at the body; `"inventory"` puts the loot straight into the recipient's [inventory](/guides/players/inventory/) | + +`loot: null` leaves the game's own random loot bag, which each player's game drops for itself; what a player takes from it arrives as an [item the game hands out](/guides/players/inventory/#items-the-game-hands-out). `loot: []` drops nothing at all. + +A malformed policy throws, and so does an item id the game does not know. + +## Which policy applies + +An NPC's own policy wins. Then, for a camp's enemies, that camp's policy and the policy for every camp; for a world event's, the world events' policy. Last comes `NPC.setDefaultRewardPolicy`, and the fields none of them set keep their defaults. + +| Call | Sets | +| --- | --- | +| `npc.setRewardPolicy(policy)` | One NPC's policy; `npc.getRewardPolicy()` reads it back | +| `Camps.setRewardPolicy(policy, campId)` | One [camp](/guides/encounters/camps/#what-camps-pay)'s, or every camp's without `campId` | +| `WorldEvents.setRewardPolicy(policy)` | Every [world event](/guides/encounters/world-events/#what-world-events-pay)'s | +| `NPC.setDefaultRewardPolicy(policy)` | Every NPC's | + +`setRewardPolicy` replaces a policy rather than merging into it: setting `{ experience: 5 }` on an NPC drops loot you set on it before. `null` clears one. A camp policy is written into each of its NPCs, so setting a camp's policy afterwards replaces one you set on its NPCs directly. + +The policies for camps, world events and every NPC belong to the server, not your resource, and stay when it stops. + +## When and how it pays + +The server settles a death half a second after it, so a policy can still change until then. Each recipient's own game grants the kill reward and experience. + +- **Drop delivery** leaves a loot bag at the body. With `"each"`, every recipient sees their own roll in it; with `"shared"`, the first recipient to loot it takes everything, and other players see it empty and locked. The bag goes once emptied, or after five minutes, and taking from it raises `lootableTaken` like any [lootable](/guides/world/loot/). +- **Inventory delivery** puts the loot in each recipient's inventory; with `"shared"`, one recipient chosen at random gets it. + +A death from `npc.kill()` pays nobody. An NPC finished off by its owner at low health pays the last player who hit it. + +```js +// server/main.js +// A boss that pays everyone who was there. +const boss = NPC.create("Troll_Crate", x, y, z, "hostile"); +boss?.setRewardPolicy({ + recipients: "nearby", + radius: 5000, + experience: 750, + loot: [{ itemId: "TrollMucus", count: [2, 4] }], + lootMode: "each", +}); +``` + +## Where to go next + +- [Fighting NPCs](/guides/pve/fighting-npcs/) for who counts as a killer and a damager. +- [Loot](/guides/world/loot/) for lootables of your own. diff --git a/guides/overview.md b/guides/overview.md index fc5dd9d..f23b536 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -36,9 +36,14 @@ Use the navigation to browse the globals available to the selected environment. **NPCs and creatures:** -- [NPCs](/guides/npcs-and-creatures/npcs/) — spawning enemies, controlling their health, targets and size, and their damage and death events. +- [NPCs](/guides/npcs-and-creatures/npcs/) — spawning enemies, who runs their AI, controlling their health, targets and size, and switching their behaviour off. - [Beasts and characters](/guides/npcs-and-creatures/beasts-and-characters/) — tame creatures players care for, and stationary townspeople. +**PvE:** + +- [Fighting NPCs](/guides/pve/fighting-npcs/) — how several players fight one NPC, damage and death events, damage multipliers, and bosses. +- [Rewards and loot](/guides/pve/rewards-and-loot/) — what an NPC's death pays and to whom. + **Encounters:** - [Encounter policy](/guides/encounters/encounter-policy/) — whether Overland's camps and roaming encounters are off, spawned by each player's game, or run by the server. From e2f2369e5dc75fbc763a88c56078ac35f4ab47a5 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:19:06 -0400 Subject: [PATCH 7/9] docs: add a line traces guide World.lineTrace for 1.9 (World, order 540): options, what a hit reports, and three patterns: what the player is looking at, the ground under a point, and line of sight, with results sent to the server as client claims. --- guides/World/line-traces.md | 107 ++++++++++++++++++++++++++++++++++++ guides/overview.md | 3 +- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 guides/World/line-traces.md diff --git a/guides/World/line-traces.md b/guides/World/line-traces.md new file mode 100644 index 0000000..deab2f9 --- /dev/null +++ b/guides/World/line-traces.md @@ -0,0 +1,107 @@ +--- +title: Line traces +description: Cast a line through the world from a client script to find what it hits first, such as what the player is looking at, the ground under a point or whether a spot is in sight. +sidebar: + order: 540 +--- + +A line trace follows a straight line through the world and reports the first thing it hits. `World.lineTrace(start, end, options)` traces on the client, through the world the player's game has loaded. The server has no world of its own to trace, so a client traces and sends the server what it found, which the server treats like anything else a client reports. + +## Tracing + +```js +// client/main.js +const from = LocalPlayer.getPosition(); +if (from) { + const hit = World.lineTrace(from, { x: from.x, y: from.y, z: from.z - 10000 }); + if (hit) console.log(`ground ${hit.distance} cm below, on ${hit.surface}`); +} +``` + +`start` and `end` are world positions in centimetres. A trace returns the first hit, or `null` when the line hits nothing, including when it runs into an area the player's game has not loaded. A malformed argument throws. + +## What it hits + +By default a trace stops where the game's visibility checks stop: the ground, buildings and objects, and players' bodies, where it also names the bone it hit. The options change that: + +| Option | Default | Does | +| --- | --- | --- | +| `channel` | `"visibility"` | `"camera"` passes through players, as the game's camera does | +| `objectTypes` | | Stops only on these kinds of thing instead, such as `["worldStatic"]` for the level, `["pawn"]` for characters or `["shallowWater", "deepWater"]` for water. It cannot be combined with `channel`, and it also stops on trigger volumes. | +| `ignore` | | Ids of players, NPCs, objects, beasts or characters to pass through | +| `ignoreSelf` | `true` | Passes through the local player | +| `complex` | `false` | Traces against detailed collision rather than the simple shapes | + +## What a hit says + +| Field | Meaning | +| --- | --- | +| `location`, `normal` | Where it hit and the surface's direction there; on flat ground, `normal.z` is close to 1 | +| `distance` | How far from `start`, in centimetres | +| `surface` | What the surface is made of, such as `"Grass"`, `"Stone"`, `"Wood"` or `"Water"`, or `null` | +| `objectType` | The kind of thing hit, such as `"worldStatic"` | +| `entity` | For a player, NPC, world object, beast or character, its `type` and `id`, the same id the server uses; `null` for the level and anything else | +| `bone` | The bone hit on a body, or `null` | +| `startPenetrating` | Whether the line started inside what it hit; its `normal` is then unreliable | + +`actor` and `component` name the game's own objects, and the names differ between players' games. + +## What the player is looking at + +Trace along the camera's view. `Camera.capture()` returns where the camera is and which way it faces: + +```js +// client/main.js +const RAD = Math.PI / 180; + +function lookingAt(range = 5000) { + const view = Camera.capture(); + if (!view) return null; + const pitch = view.rotation.pitch * RAD; + const yaw = view.rotation.yaw * RAD; + const start = view.position; + return World.lineTrace(start, { + x: start.x + Math.cos(pitch) * Math.cos(yaw) * range, + y: start.y + Math.cos(pitch) * Math.sin(yaw) * range, + z: start.z + Math.sin(pitch) * range, + }); +} + +Key.bind("f", () => { + const hit = lookingAt(); + if (hit && hit.entity) Events.emitServer("inspect", { type: hit.entity.type, id: hit.entity.id }); +}); +``` + +## The ground under a point + +```js +// client/main.js +function groundHeight(x, y, z) { + const hit = World.lineTrace({ x, y, z: z + 100 }, { x, y, z: z - 10000 }, { objectTypes: ["worldStatic", "shallowWater", "deepWater"] }); + return hit && !hit.startPenetrating ? hit.location.z : null; +} +``` + +`null` means the ground is not loaded there, or is more than 100 m down. + +## Line of sight + +Tracing on the camera channel from the camera to a point, ignoring the thing you are looking for, tells you whether anything solid is in between. Players do not block it: + +```js +// client/main.js +Events.on("sight:check", (target) => { + const eye = Camera.capture(); + if (!eye) return; + const blocker = World.lineTrace(eye.position, { x: target.x, y: target.y, z: target.z + 60 }, { channel: "camera", ignore: [target.id] }); + Events.emitServer("sight:result", { id: target.id, visible: blocker === null }); +}); +``` + +Each trace is one check on the game's own collision, cheap enough for a few every frame. + +## Where to go next + +- [Camera](/guides/camera-and-sound/camera/) for where the player's view is. +- [Authority](/guides/core-concepts/authority/) for treating what a client sends as a claim. diff --git a/guides/overview.md b/guides/overview.md index f23b536..57f44b0 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -59,11 +59,12 @@ Use the navigation to browse the globals available to the selected environment. **World:** -- [Time and weather](/guides/world/time-and-weather/) — the shared clock, calendar, season and weather, holiday decor and the Headmaster's office. +- [Time and weather](/guides/world/time-and-weather/) — the shared clock, calendar, season and weather, holiday decor, the Headmaster's office and Great Hall meals. - [Objects and effects](/guides/world/objects-and-effects/) — placing and moving props from the game's meshes and Blueprints, and visual effects that can follow an entity. - [Physics objects](/guides/world/physics-objects/) — objects that fall, roll and fly, pushing and throwing them, and letting players carry them. - [Doors](/guides/world/doors/) — holding the game's doors locked or open for everyone, and deciding who gets in. - [Loot](/guides/world/loot/) — server-owned chests, moonstones and foragables, per-player contents, and switching off the game's own loot. +- [Line traces](/guides/world/line-traces/) — finding what a line through the world hits first, from a client script. **Room of Requirement:** From af17e5efcdc920ec9ec4fc1569da637f83777a20 Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 00:19:07 -0400 Subject: [PATCH 8/9] docs: update existing guides for 1.9 Brooms gain the server's upgrade tiers and speed multiplier in place of the removed Broom.setMaxSpeed, with the two server.json keys. The clock page covers the Headmaster's staircase closing in place and Great Hall meals. Appearance and inventory cover appearanceOverride, ghost disguises, LocalPlayer.setVisibility and a nullable look; loot adds coin pouches. The events tables gain the 1.9 npcDied and every missing native event, and drop three client events that never fire. Docker tags and the TypeScript install lines move to 1.9.0. --- guides/Core concepts/events.md | 44 +++++++++++++++---- guides/Getting started/typescript.md | 4 +- guides/Hosting a server/configuration.md | 2 + guides/Hosting a server/docker.md | 10 ++--- guides/Mounts and races/brooms-and-mounts.md | 21 +++++++-- .../beasts-and-characters.md | 2 +- guides/Players/appearance.md | 10 +++-- guides/Players/inventory.md | 5 ++- guides/World/loot.md | 6 +-- guides/World/time-and-weather.md | 30 +++++++++++-- 10 files changed, 103 insertions(+), 31 deletions(-) diff --git a/guides/Core concepts/events.md b/guides/Core concepts/events.md index aa39af7..438f84d 100644 --- a/guides/Core concepts/events.md +++ b/guides/Core concepts/events.md @@ -136,6 +136,8 @@ Events.on("seasonChanged", (payload) => { Runtime-generated events use the normal `Events.on` and `Events.once` subscriptions. They are raised by the framework or the native HogwartsMP module rather than by `Events.emit`. Avoid emitting custom events with these names. +The declarations type the handlers of the events in these tables, so an editor knows each argument. Events of your own, and the few native ones the declarations leave out, such as `chatSend` and the voice events, hand a TypeScript handler `unknown` arguments. + `resourceStart(resourceName)` and `resourceStop(resourceName)` are emitted by both the server and client resource managers when a JavaScript resource starts or is about to stop. ### Native server events @@ -156,6 +158,8 @@ Runtime-generated events use the normal `Events.on` and `Events.once` subscripti | `playerDownedEnd` | `(player)` | A player recovers from the downed state or respawns | | `playerHoldEnded` | `(player, owner, reason)` | A server-owned hold ends; `reason` identifies why | | `playerHoldStrained` | `(player, owner, distanceCm)` | A held player continues reporting positions beyond the hold boundary | +| `playerWandChanged` | `(player)` | The player's wand changes | +| `playerGearChanged` | `(player)` | What the player wears changes | | `worldReady` | `(player)` | The player's world, pawn, and replication are ready | | `loadingFinished` | `(player)` | The player's load screen is dismissed after `worldReady` | | `playerInventoryChanged` | `(player, items)` | The player's reported native inventory changes; `items` is the full native inventory | @@ -165,9 +169,28 @@ Runtime-generated events use the normal `Events.on` and `Events.once` subscripti | `playerAppearanceChanged` | `(player, look, revision)` | The server accepts a new look from the player's game or `player.setLook`; `look` is the checked [look](/guides/players/appearance/#the-look) and `revision` increases for the connection | | `playerTeleportComplete` | `(player, requestId, status, completion)` | A `player.teleport` request finishes; `completion` includes the normalized request, client-observed final transform, and ground-snap result | | `playerFastTravelComplete` | `(player, requestId, status)` | A `player.fastTravel` request finishes or times out | -| `npcDied` | `(npcId, enemyId)` | A server-spawned NPC dies | +| `npcDied` | `(npcId, enemyId, killerId, damagerIds)` | A server-spawned NPC dies; `killerId` is the last player to damage it, or `null`, and `damagerIds` every player who did. See [Fighting NPCs](/guides/pve/fighting-npcs/#damage-and-death) | | `npcDamaged` | `(npcId, attackerId, amount)` | A player reports damage to a server-spawned NPC | +| `beastCared` | `(beast, player, kind)` | A player pets or feeds a beast; return `false` synchronously to refuse it | +| `campActivated` | `(campId)` | A [camp](/guides/encounters/camps/)'s enemies spawn | +| `campCleared` | `(campId)` | Every enemy of an active camp is dead | +| `worldEventStarted` | `(spotId, kind)` | A [world event](/guides/encounters/world-events/) starts | +| `worldEventCleared` | `(spotId)` | Every enemy of a running world event is dead | +| `worldEventEnded` | `(spotId)` | A world event ends | +| `zoneEnter` | `(zone, entity)` | An entity comes into a [zone](/guides/encounters/zones/) | +| `zoneExit` | `(zone, entity, reason, entityId)` | An entity leaves a zone; `reason` is `"left"` or `"gone"` | +| `doorUnlockRequest` | `(player, door)` | A player tries to open a lock the server holds shut, by key or Alohomora | +| `lootableTaken` | `(player, lootableId, items)` | A player takes a server lootable; `items` is what it gave them | +| `housePointsChange` | `(house, points, previous)` | A house's points change | +| `housePointsRequest` | `(player, request)` | A player's game asks to change house points; return `false` synchronously to refuse it | +| `sanctuaryChanged` | `(id, revision, editor)` | An editor changes a [Room of Requirement](/guides/room-of-requirement/rooms/) room | +| `playerSanctuaryEnter` | `(player, id)` | A player enters a room | +| `playerSanctuaryLeave` | `(player, id)` | A player leaves a room | | `pvpHit` | `(casterId, victimId, spell, damage)` | A PvP hit has passed server arbitration and policy and was relayed to its victim; `damage` is the final amount | +| `pvpDefeat` | `(victim, killer, kind)` | A player defeats another in [PvP](/guides/pvp/hits-and-defeat/#defeat); `kind` is `"death"` or `"kneel"` | +| `pvpRulesChanged` | `(rules)` | The PvP rules change | +| `pvpFlagChanged` | `(player, on)` | A player's PvP flag changes | +| `wandLinkEnded` | `(link, winner, reason)` | A [wand link](/guides/pvp/duels-and-wand-links/#wand-links) ends | | `entityStateChange` | `(entity, key, value, previous)` | A script writes or removes one key of an entity's state or of `World.state`; `value` is `undefined` for a removal | Some native server events originate in the connected player's game. The reserved `playerDied`, `playerDowned`, and `playerDownedEnd` messages use a dedicated native RPC and cannot be forged through `Events.emitServer`, but they are still client-authored observations. Location context, native inventory reports, request completions, and `npcDamaged` also cross the client/server boundary. Apply server-side validation appropriate to the consequences of the handler. In particular, adopt a `playerInventoryNativeDelta` with `player.inventory.adoptNative()` only under an explicit allowlist. `playerAppearanceChanged` is emitted after the server accepts and sanitizes an appearance, while `pvpHit` is a post-policy informational event; use `Pvp.setPolicy` to veto or reprice PvP hits. @@ -199,11 +222,18 @@ These events are delivered only inside the client runtime. Receiving a native cl | `chatMessage` | `({ author, text, color })` | A chat message arrives from the server | | `chatSend` | `(text)` | The local player submits text through the chat overlay | | `spellCast` | `(spellPath)` | The local player casts a spell; the path identifies its spell record | -| `spellProbe` | `(label)` | The native spell hook produces a diagnostic line | | `playerRegionChanged` | `(region)` | The local player enters a named region | -| `creatorOpened` | `()` | The character creator opens | +| `creatorOpened` | `({})` | The character creator opens | | `creatorConfirmed` | `({ first, last })` | The character creator closes after confirmation | -| `creatorCancelled` | `()` | The character creator closes without confirmation | +| `creatorCancelled` | `({})` | The character creator closes without confirmation | +| `creatorChanged` | `(state)` | A creator session's selections change | +| `abilityStatus` | `(handle)` | An ability the local player plays starts or settles | +| `spellAssignment` | `({ source, loadout, slot })` | A spell is assigned to a slot, from the game's menu or a script | +| `spellLearningFinished` | `(result)` | A spell lesson the resource started finishes; it grants no spell | +| `minimapChanged` | `(state, previous)` | The [minimap](/guides/user-interface/map-markers/#the-minimap) switches map, floor or tile | +| `pvpRelationChanged` | `(relation)` | The local player's [PvP relation](/guides/pvp/rules-and-relations/) to another player is resolved or changes | +| `localDowned` | `()` | The local player kneels after a PvP defeat | +| `localDownedEnd` | `({ wasDead })` | The local player gets up, from a kneel or a respawn | | `entityStateChange` | `(entity, key, value, previous)` | A replicated state write or removal arrives on the client; `value` is `undefined` for a removal | A `chatSend` handler may synchronously return `false` to prevent that line from being sent. An asynchronous return value cannot veto it. @@ -225,8 +255,6 @@ Call `CombatEvents.arm()` before subscribing to the combat observations below. A | `spellHit` | `(spellType, spellPath, victimClass, victimNpcId, victimIsLocal, casterClass, casterIsLocal)` | A watched spell hits an actor | | `spellHitPlayer` | `(victimId, spellId, spellPath, damage)` | The local player's native spell impact hits another player's proxy | | `damageReceived` | `(damage, targetClass, targetNpcId, targetIsLocal, instigatorClass, instigatorIsLocal)` | A watched actor receives damage | -| `protegoBlocked` | `(spellPath, targetClass, targetIsLocal)` | Protego blocks a spell | -| `attackDeflected` | `(deflectorIsLocal)` | Protego deflects an attack | | `gameEvent` | `(functionName, objectClass)` | Another explicitly watched native function fires | ```js @@ -237,7 +265,7 @@ if (CombatEvents.arm()) { } ``` -`spellHitPlayer` is informational. The native client relays the impact separately for server arbitration; server resources can observe the resulting final hit through `pvpHit`. It does not currently have a typed `ClientEvents.on` overload in the public declarations. +`spellHitPlayer` is informational. The native client relays the impact separately for server arbitration; server resources can observe the resulting final hit through `pvpHit`. `gameEvent` is not an unrestricted stream of every Unreal Engine event. It only reports functions installed by the available typed or feature-specific watchers. @@ -250,7 +278,7 @@ The native brewing and herbology hooks currently expose two additional events us | `brewingInput` | `(payloadJson)` | `{ action, siteUid, stationUid, potionId, yield }`; `action` is `begin`, `collect`, or `destroy` | | `herbInput` | `(payloadJson)` | `{ action, plotUid, plantId, fertilizerId, state }`; `action` is `plant`, `fertilize`, `harvest`, or `destroy` | -These two names are native implementation events but do not yet have typed `ClientEvents.on` overloads in the public declarations. +`breakableState` fires with an object's id, `"broken"` or `"repaired"`, and its class when the local player breaks or repairs a repairable object, once `Breakables.arm()` has been called. #### Web-view events diff --git a/guides/Getting started/typescript.md b/guides/Getting started/typescript.md index ed1309f..9c2b1a5 100644 --- a/guides/Getting started/typescript.md +++ b/guides/Getting started/typescript.md @@ -14,7 +14,7 @@ There are two ways to use them. Keep writing JavaScript and let the editor check The package version is the release version. From the resource's directory, install the one your server runs: ```sh -npm install --save-dev --save-exact @hogwartsmp/types@1.8.2 +npm install --save-dev --save-exact @hogwartsmp/types@1.9.0 ``` A resource's `package.json` is also an ordinary npm manifest, so the dependency is recorded beside the `mafiahub` block. As long as the manifest declares `files`, only what it lists [reaches players](/guides/getting-started/first-resource/#what-reaches-the-player); `node_modules/` stays on your machine and the server's disk. @@ -91,7 +91,7 @@ npm install --save-dev --save-exact typescript@5.9.3 "build": "tsc -p tsconfig.json && tsc -p src/client/tsconfig.json" }, "devDependencies": { - "@hogwartsmp/types": "1.8.2", + "@hogwartsmp/types": "1.9.0", "typescript": "5.9.3" }, "mafiahub": { diff --git a/guides/Hosting a server/configuration.md b/guides/Hosting a server/configuration.md index 3d4f810..b683aa4 100644 --- a/guides/Hosting a server/configuration.md +++ b/guides/Hosting a server/configuration.md @@ -55,6 +55,8 @@ HogwartsMP's own settings live under the `mod` object, so they can never collide | --- | --- | --- | | `curseforge.mods` | `[]` | CurseForge project IDs every connecting player must activate. See [Required CurseForge mods](#required-curseforge-mods). | | `autoRespawn` | `true` | Whether a dead player respawns on their own after a few seconds. See [Respawning](#respawning). | +| `broomUpgradeTier` | `0` | The broom upgrades, from 0 to 3, every player starts with. Scripts change it per player with [`player.setBroomUpgradeTier`](/guides/mounts-and-races/brooms-and-mounts/#broom-upgrades-and-speed). | +| `broomSpeedMultiplier` | `1` | How fast every player's broom flies, from 0.25 to 3, on top of its upgrades. Values outside the range are clamped. | ### Validation diff --git a/guides/Hosting a server/docker.md b/guides/Hosting a server/docker.md index 41eaba7..161f7d6 100644 --- a/guides/Hosting a server/docker.md +++ b/guides/Hosting a server/docker.md @@ -13,7 +13,7 @@ ghcr.io/hogwarts-mp/hogwartsmp-server: The image holds the same native server the release ZIP ships, repackaged on Ubuntu 24.04 for `linux/amd64`. It is not a source build, and like the ZIP it carries **no game-mode resources**. Pulling it needs no login. -Tags are exact release versions with no leading `v`, such as `1.8.0`. There is no `latest`, `stable` or `testing` tag, because the server must match the release your players run: a mismatched client is rejected before it loads in. Changing the tag is how you update. +Tags are exact release versions with no leading `v`, such as `1.9.0`. There is no `latest`, `stable` or `testing` tag, because the server must match the release your players run: a mismatched client is rejected before it loads in. Changing the tag is how you update. This page covers the container. Configuration, Foundations, ports and the masterlist work exactly as in [Hosting a server](/guides/hosting-a-server/run-a-server/). @@ -42,7 +42,7 @@ volumes: Next to it, create a `.env` that pins the release: ```dotenv -HOGWARTSMP_VERSION=1.8.0 +HOGWARTSMP_VERSION=1.9.0 ``` Start it and follow the log: @@ -61,7 +61,7 @@ Without Compose, the same container is: docker run -d --name hogwartsmp --init --restart unless-stopped \ -p 27015:27015/udp -p 127.0.0.1:27016:27016/tcp \ -v hogwartsmp-data:/home/container \ - ghcr.io/hogwarts-mp/hogwartsmp-server:1.8.0 + ghcr.io/hogwarts-mp/hogwartsmp-server:1.9.0 ``` ## Data and resources @@ -112,7 +112,7 @@ Arguments after the image name go straight to the server, as the [command-line f docker run -d --name hogwartsmp --init --restart unless-stopped \ -p 28015:28015/udp -p 127.0.0.1:28016:28016/tcp \ -v hogwartsmp-data:/home/container \ - ghcr.io/hogwarts-mp/hogwartsmp-server:1.8.0 --port 28015 --apiport 28016 + ghcr.io/hogwarts-mp/hogwartsmp-server:1.9.0 --port 28015 --apiport 28016 ``` In Compose, edit `command` and `ports` together: @@ -146,7 +146,7 @@ The server handles `SIGTERM` and `SIGINT` by shutting down cleanly: every resour Each release has a ready-made egg that points at that release's exact image, so importing it pins the version. The egg ships inside the image, so you can extract it from any machine with Docker: ```sh -docker run --rm --user 0 --entrypoint cat ghcr.io/hogwarts-mp/hogwartsmp-server:1.8.0 \ +docker run --rm --user 0 --entrypoint cat ghcr.io/hogwarts-mp/hogwartsmp-server:1.9.0 \ /usr/share/hogwartsmp/egg-hogwartsmp.json > egg-hogwartsmp.json ``` diff --git a/guides/Mounts and races/brooms-and-mounts.md b/guides/Mounts and races/brooms-and-mounts.md index 33dade1..da69aa4 100644 --- a/guides/Mounts and races/brooms-and-mounts.md +++ b/guides/Mounts and races/brooms-and-mounts.md @@ -1,11 +1,11 @@ --- title: Brooms and mounts -description: Give players brooms and creatures to ride, put them on and take them off from a client script, change how a broom flies, and keep riders out of an area. +description: Give players brooms and creatures to ride, put them on and take them off from a client script, set how fast brooms fly, change how a broom handles, and keep riders out of an area. sidebar: order: 800 --- -Players fly brooms and ride the game's creatures, and everyone else sees them do it without any script. Client scripts control the local player's riding with `Broom`, `FlyingMount` and `GroundMount`. The server decides what each player owns, through their [inventory](/guides/players/inventory/), and has no call of its own for riding. +Players fly brooms and ride the game's creatures, and everyone else sees them do it without any script. Client scripts control the local player's riding with `Broom`, `FlyingMount` and `GroundMount`. The server decides what each player owns, through their [inventory](/guides/players/inventory/), and how fast their broom flies, but has no call to put a player on a broom or take them off. ## What players own @@ -56,11 +56,24 @@ Events.on("broom:mount", () => { `Broom.dismount()` takes the player off at once, wherever they are, so a player in the air falls. `Broom.dismount(true)` asks the game to land them the normal way instead, which does nothing where the game will not let them land. Both return `false` until the player is seated. `equip` and `unequip` refuse while the player is riding. +## Broom upgrades and speed + +The server sets how fast each player's broom flies. `player.setBroomUpgradeTier(tier)` gives a player the game's broom upgrades from Albie Weekes' shop, from 0 to 3: each tier flies and boosts faster, and lets the boost meter refill higher above the ground. `player.setBroomSpeedMultiplier(multiplier)` scales cruising, boost and turbo speed on top of that, from 0.25 to 3. Faster than 3 would outrun the world loading around the player. + +```js +// server/main.js +Events.on("loadingFinished", (player) => { + player.setBroomUpgradeTier(3); + player.setBroomSpeedMultiplier(1.5); // half again as fast as a fully upgraded broom +}); +``` + +Both apply at once, mounted or not, and last through respawns and travel. `null` sets them back to the server's defaults, `mod.broomUpgradeTier` and `mod.broomSpeedMultiplier` in [`server.json`](/guides/hosting-a-server/configuration/#mod-keys), which every joining player starts with. `player.broomUpgradeTier` and `player.broomSpeedMultiplier` read them back. A value outside its range throws. Broom races keep their own speeds. + ## Changing how a broom flies | Call | Does | | --- | --- | -| `Broom.setMaxSpeed(speed)` | Caps the speed, in centimetres per second | | `Broom.setMaxHeight(z)` | Caps how high the broom can climb, as a world height; `clearMaxHeight()` lifts it | | `Broom.setInputDisabled(disabled)` | Stops the rider steering | | `Broom.setForceLand(forceLand)` | Brings the broom down | @@ -68,7 +81,7 @@ Events.on("broom:mount", () => { | `Broom.setCastingAllowed(allowed)` | Lets the rider cast spells other than heavy ones while flying | | `Broom.teleport(x, y, z, yaw)` | Moves a rider and stops them dead; leaving out `yaw` faces them to 0, not their heading | -These belong to the client, not to your resource, and stay until something changes them. `Broom.resetOverrides()` undoes every one of them set through `Broom`, so call it when your game ends and as your resource stops. Set the speed again after the player switches broom. +These belong to the client, not to your resource, and stay until something changes them. `Broom.resetOverrides()` undoes every one of them set through `Broom`, so call it when your game ends and as your resource stops. A knock out of the sky, without dismounting: diff --git a/guides/NPCs and creatures/beasts-and-characters.md b/guides/NPCs and creatures/beasts-and-characters.md index 9b3c80d..d93c808 100644 --- a/guides/NPCs and creatures/beasts-and-characters.md +++ b/guides/NPCs and creatures/beasts-and-characters.md @@ -80,5 +80,5 @@ The prompt is only text. Handling the interaction it invites, such as opening a ## Where to go next -- [NPCs](/guides/npcs-and-creatures/npcs/) for enemies, their AI and what their deaths pay. +- [NPCs](/guides/npcs-and-creatures/npcs/) for enemies and their AI, and [Rewards and loot](/guides/pve/rewards-and-loot/) for what their deaths pay. - [Authority](/guides/core-concepts/authority/) for which client runs a creature's AI and what that means. diff --git a/guides/Players/appearance.md b/guides/Players/appearance.md index d65e74a..6a50762 100644 --- a/guides/Players/appearance.md +++ b/guides/Players/appearance.md @@ -43,7 +43,7 @@ Events.on("loadingFinished", async (player) => { }); Events.on("playerAppearanceChanged", (player, look) => { - if (!restored.has(player.id) || Object.keys(look.presets).length === 0) return; + if (!look || !restored.has(player.id) || Object.keys(look.presets).length === 0) return; saveLook(player, JSON.stringify(look)); }); @@ -58,7 +58,7 @@ Events.on("playerDisconnect", (player) => restored.delete(player.id)); `player.setLook(look, callback)` takes a look or its JSON text. The callback runs with `(error, result)` once the player's game has rebuilt their character, within 20 seconds, and `result.revision` is the revision it produced. Without a callback, a malformed look throws `INVALID_APPEARANCE`. The server checks a look's shape, not that its ids exist in the game, and the player's game leaves out ids it does not know, so read the look back if it matters. -A look's `gear` and `house` show how other players see the player, but do not dress the player themselves. Restore clothes from their [inventory](/guides/players/inventory/#saving-inventories), and the house with [`player.house`](#house-and-gender). +A look's `gear`, `hood` and `house` show how other players see the player, but do not dress the player themselves. What they wear, their hood and the look a worn item shows all come from their [inventory](/guides/players/inventory/#saving-inventories), so restoring it restores them. Restore the house with [`player.house`](#house-and-gender). ## The character creator @@ -113,7 +113,7 @@ A look saved while a player is resized carries the size, and restoring it resize ## Disguises -`player.setTransmog(characterId)` makes a player look like one of the game's characters, to everyone including themselves, and `player.setTransmog("")` takes it off. Check an id with `Character.isAllowed(characterId)`: the server accepts any well-formed id, and only the game's human characters work. `player.getTransmog()` reads it back. +`player.setTransmog(characterId)` makes a player look like one of the game's characters, to everyone including themselves, and `player.setTransmog("")` takes it off. The server accepts any well-formed id. `Character.isAllowed(characterId)` checks it against the human characters [`Character.create`](/guides/npcs-and-creatures/beasts-and-characters/#characters) can place. The game's ghosts work as disguises too, though most of them fail that check, and a ghost disguise looks ghostly to the player as well. `player.getTransmog()` reads it back. A disguise is not part of the look: `getLook` never includes it, and `setLook` takes it off. @@ -132,6 +132,10 @@ function handApple(player) { A prop handed to a player before they have a body is dropped, so wait for `loadingFinished`. +## Hiding your own body + +On the client, `LocalPlayer.setVisibility(false)` hides the player's own body, wand and held prop on their screen only, for a first-person camera or a cutscene. Everyone else still sees them, unless the server hides them with `player.setVisible(false)`. It lasts through respawns and travel until you change it. + ## What a visit keeps House, size, disguise and props last for the visit and are gone when the player reconnects, like the look itself. Save what should last and set it again on `loadingFinished`. diff --git a/guides/Players/inventory.md b/guides/Players/inventory.md index 25125bc..16b1801 100644 --- a/guides/Players/inventory.md +++ b/guides/Players/inventory.md @@ -29,6 +29,7 @@ Each row in `list()` is an item: | `variation` | Gear's own variant id; empty for ordinary items | | `unique` | Whether the row is a unique item | | `identified`, `equipped`, `hoodUp` | Gear only: whether it is identified, whether it is worn, and its hood | +| `appearanceOverride` | Worn gear only: the gear id whose look it shows, as players choose in the Equipment menu, or `""` for its own | `InventoryCatalog` describes every item the game knows: `InventoryCatalog.get(itemId)` returns its default holder, stack size and kind, `has(itemId)` tests an id, and `list(filter)` lists definitions, for example every consumable. @@ -55,7 +56,7 @@ player.inventory.give("Edurus", 3, {}, (error, result) => { }); ``` -Ids are checked against the catalog: an unknown one fails with `INVALID_ITEM`, and taking more than a player has fails with `INSUFFICIENT_COUNT`. Gear gets a variation of its own when given, and `options` can hand it over identified or already worn: +Ids are checked against the catalog: an unknown one fails with `INVALID_ITEM`, and taking more than a player has fails with `INSUFFICIENT_COUNT`. Gear gets a variation of its own when given, and `options` can hand it over identified, already worn, with its hood up, or showing another piece's look with `appearanceOverride`, a bare gear id. A malformed override fails the whole change: ```js // server/main.js @@ -102,7 +103,7 @@ Events.on("loadingFinished", async (player) => { }); ``` -Keep the rows as `list()` returns them: `replace` needs a gear row's `variation` to restore that exact piece. `saveInventory` and `loadInventory` stand for your own store, such as the database behind [Persisting state](/guides/getting-started/first-resource/#persisting-state). +Keep the rows as `list()` returns them: `replace` needs a gear row's `variation` to restore that exact piece, and its `appearanceOverride` to restore the look it shows. `saveInventory` and `loadInventory` stand for your own store, such as the database behind [Persisting state](/guides/getting-started/first-resource/#persisting-state). ## Waiting for the player's game diff --git a/guides/World/loot.md b/guides/World/loot.md index c52ae0e..3d93239 100644 --- a/guides/World/loot.md +++ b/guides/World/loot.md @@ -66,17 +66,17 @@ function showDailyReward(player, alreadyClaimed) { ## Switching off the game's own loot -The game places its own chests, moonstone deposits and foragables everywhere, and each player's game spawns and empties them separately. To make your lootables the only loot, switch the game's families off with `World.suppressLoot`: +The game places its own chests, coin pouches, moonstone deposits and foragables everywhere, and each player's game spawns and empties them separately. To make your lootables the only loot, switch the game's families off with `World.suppressLoot`: ```js // server/main.js Events.on("resourceStart", (resourceName) => { if (resourceName !== "my-gamemode") return; - World.suppressLoot({ chests: true, moonstones: true, foragables: true }); + World.suppressLoot({ chests: true, pouches: true, moonstones: true, foragables: true }); }); ``` -Suppressed chests are hidden, and Overland's moonstone and foragable spawns are removed along with placed deposits and foragables. Room of Requirement storage and quest containers are never touched. Spawns are removed only from areas that load after the change, so call it when your resource starts. `false` switches a family back on, a family you leave out keeps its setting, and `World.lootSuppressed` reads the current settings. +Suppressed chests and coin pouches are hidden, prompt and all, and Overland's moonstone and foragable spawns are removed along with placed deposits and foragables. Room of Requirement storage and quest containers are never touched. Spawns are removed only from areas that load after the change, so call it when your resource starts. `false` switches a family back on, a family you leave out keeps its setting, and `World.lootSuppressed` reads the current settings. ## Where to go next diff --git a/guides/World/time-and-weather.md b/guides/World/time-and-weather.md index abd0fc0..fbfd59c 100644 --- a/guides/World/time-and-weather.md +++ b/guides/World/time-and-weather.md @@ -1,11 +1,11 @@ --- title: Time and weather -description: Set the shared clock, calendar, season and weather every player sees, change how fast time passes, and pin holiday decor and the Headmaster's office. +description: Set the shared clock, calendar, season and weather every player sees, change how fast time passes, and pin holiday decor, the Headmaster's office and the meals in the Great Hall. sidebar: order: 500 --- -The server owns one world state that every client follows: the clock, the calendar, the season, the weather, holiday decor and the Headmaster's office. Each player's game shows what the server sets, so everyone sees the same sky. +The server owns one world state that every client follows: the clock, the calendar, the season, the weather, holiday decor, the Headmaster's office and the meals on the tables. Each player's game shows what the server sets, so everyone sees the same sky. ## The clock and calendar @@ -66,7 +66,31 @@ Every client re-applies the decor whenever its world loads or a mission changes ## The Headmaster's office -`World.setHeadmasterOffice(office)` pins the gargoyle staircase to the Headmaster's office for every player: `"open"` raises it, `"closed"` keeps it down, and `"game"`, the default, leaves it to the story quest. A raised staircase stays up until its part of the castle reloads, so `"closed"` takes effect on a player's next visit there. +`World.setHeadmasterOffice(office)` pins the gargoyle staircase to the Headmaster's office for every player: `"open"` raises it, `"closed"` keeps it down, and `"game"`, the default, leaves it to the story quest. A loaded staircase follows at once: `"closed"` folds the stairs away, sinks the gargoyle and closes the stairwell off again. Going back to `"game"` leaves the staircase as it is until the quest changes it. + +## Meals + +`World.setMeal(meal)` sets the food on the tables of the Great Hall and the Kitchens for every player, and `World.meal` reads it back: + +| Meal | Tables | +| --- | --- | +| `"game"` | Follow the clock, the default: breakfast at 6:00, lunch at 11:00, dinner at 16:00, dessert at 19:00, cleared at 20:00 | +| `"breakfast"`, `"lunch"`, `"dinner"`, `"dessert"` | That meal, whatever the time | +| `"prep"` | Plates and goblets, with no food | +| `"none"` | Bare | + +```js +// server/main.js +function startFeast() { + World.setMeal("dessert"); +} + +function endFeast() { + World.setMeal("game"); +} +``` + +A meal you set holds through mealtimes. Any other value returns `false`. ## Reacting to changes From e32b7edc3d156b9370eb628018e7b9e82b2288ab Mon Sep 17 00:00:00 2001 From: KHeartz Date: Wed, 14 Oct 2026 17:31:45 -0400 Subject: [PATCH 9/9] docs: add a catching beasts guide --- guides/Core concepts/authority.md | 2 +- guides/Core concepts/events.md | 4 + .../beasts-and-characters.md | 7 +- guides/NPCs and creatures/catching-beasts.md | 143 ++++++++++++++++++ guides/overview.md | 1 + 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 guides/NPCs and creatures/catching-beasts.md diff --git a/guides/Core concepts/authority.md b/guides/Core concepts/authority.md index 96f8a92..e503983 100644 --- a/guides/Core concepts/authority.md +++ b/guides/Core concepts/authority.md @@ -36,7 +36,7 @@ Everything a client sends is a claim about its own game, and a modified client c | `npcDamaged` | Clamped and rate-capped by the server, but reported by the attacker's client. | | `playerInventoryChanged`, `playerInventoryNativeDelta` | The client's account of its game's inventory. Adopt a delta with `player.inventory.adoptNative()` only for items on an allowlist. | | `playerLocationChanged`, teleport and fast-travel completions | The client's account of where it is and how a request went. | -| `housePointsRequest`, `beastCared` | Requests. They take effect unless a handler refuses them by returning `false`. | +| `housePointsRequest`, `beastCared`, `beastCaptureStart`, `beastCaptureRequest` | Requests. They take effect unless a handler refuses them by returning `false`. | | `doorUnlockRequest` | A request. The door stays locked unless a script releases it. | | `steamId`, `epicId`, `discordId`, `hardwareId` | Announced by the client. Never authorize anyone with them. | diff --git a/guides/Core concepts/events.md b/guides/Core concepts/events.md index 438f84d..661aea3 100644 --- a/guides/Core concepts/events.md +++ b/guides/Core concepts/events.md @@ -172,6 +172,10 @@ The declarations type the handlers of the events in these tables, so an editor k | `npcDied` | `(npcId, enemyId, killerId, damagerIds)` | A server-spawned NPC dies; `killerId` is the last player to damage it, or `null`, and `damagerIds` every player who did. See [Fighting NPCs](/guides/pve/fighting-npcs/#damage-and-death) | | `npcDamaged` | `(npcId, attackerId, amount)` | A player reports damage to a server-spawned NPC | | `beastCared` | `(beast, player, kind)` | A player pets or feeds a beast; return `false` synchronously to refuse it | +| `beastCaptureStart` | `(beast, player)` | A player's Nab-Sack starts drawing in a beast they may catch; return `false` synchronously to make it let go. See [Catching beasts](/guides/npcs-and-creatures/catching-beasts/#refereeing-a-catch) | +| `beastCaptureRequest` | `(beast, player)` | A player's Nab-Sack finished on a beast and the catch waits for the server; return `false` synchronously to refuse it | +| `beastCaptured` | `(player, captured)` | A catch landed: the beast is in the player's bag and has left the world | +| `beastReleased` | `(player, released, at)` | A player released a beast from their Field Guide; `at` is where they stood | | `campActivated` | `(campId)` | A [camp](/guides/encounters/camps/)'s enemies spawn | | `campCleared` | `(campId)` | Every enemy of an active camp is dead | | `worldEventStarted` | `(spotId, kind)` | A [world event](/guides/encounters/world-events/) starts | diff --git a/guides/NPCs and creatures/beasts-and-characters.md b/guides/NPCs and creatures/beasts-and-characters.md index d93c808..c54fa3e 100644 --- a/guides/NPCs and creatures/beasts-and-characters.md +++ b/guides/NPCs and creatures/beasts-and-characters.md @@ -1,6 +1,6 @@ --- title: Beasts and characters -description: Spawn tame beasts players can pet and feed, decide who may care for them, and place stationary townspeople and vendors with nameplates. +description: Spawn tame beasts players can pet, feed and catch, decide who may care for them, and place stationary townspeople and vendors with nameplates. sidebar: order: 420 --- @@ -51,6 +51,10 @@ Events.on("beastCared", (beast, player, kind) => { Care is refused on its own across virtual worlds, and the server rate-limits it. +### Catching + +Players can catch a beast with the game's Nab-Sack once a script makes it catchable with `beast.capturable` or `beast.setCapturableFor(player, true)`. [Catching beasts](/guides/npcs-and-creatures/catching-beasts/) covers refereeing each catch, the player's bag, keeping it across reconnects and releasing beasts. + ## Characters A character is a dressed, stationary human placed by the server: a vendor, a townsperson or a quest giver. It has no AI, no owner and no health. `Character.create(key, characterId, options)` creates one under a key of your choosing, and creating again with the same key replaces it: @@ -80,5 +84,6 @@ The prompt is only text. Handling the interaction it invites, such as opening a ## Where to go next +- [Catching beasts](/guides/npcs-and-creatures/catching-beasts/) for the Nab-Sack, the player's bag and releasing beasts. - [NPCs](/guides/npcs-and-creatures/npcs/) for enemies and their AI, and [Rewards and loot](/guides/pve/rewards-and-loot/) for what their deaths pay. - [Authority](/guides/core-concepts/authority/) for which client runs a creature's AI and what that means. diff --git a/guides/NPCs and creatures/catching-beasts.md b/guides/NPCs and creatures/catching-beasts.md new file mode 100644 index 0000000..1bafd38 --- /dev/null +++ b/guides/NPCs and creatures/catching-beasts.md @@ -0,0 +1,143 @@ +--- +title: Catching beasts +description: Let players catch beasts with the Nab-Sack, referee each catch, keep what they catch across reconnects, and release beasts back into the world. +sidebar: + order: 425 +--- + +Players catch [beasts](/guides/npcs-and-creatures/beasts-and-characters/#beasts) with the game's Nab-Sack. The server referees every catch, so a beast goes into one player's bag and leaves every player's world. The bag is the game's own, shown in the player's Field Guide, but it belongs to the connection: it is empty when a player joins and gone when they leave. A server that keeps catches saves the bag itself. + +## Making a beast catchable + +No beast can be caught until a script allows it, and players need the Nab-Sack, the `CaptureDevice` tool: + +```js +// server/main.js +Events.on("worldReady", (player) => { + if (!player.inventory.has("CaptureDevice")) player.inventory.give("CaptureDevice", 1); +}); + +const niffler = Beast.create("Niffler", { x, y, z }); +niffler.capturable = true; +``` + +| Member | Meaning | +| --- | --- | +| `beast.capturable` | Whether players may catch it. False by default | +| `beast.setCapturableFor(player, value)` | `true` or `false` for one player whatever `capturable` says; `null` returns them to it. Dropped when the player leaves | +| `beast.capturableFor(player)` | Whether this player may catch it: their override, or `capturable` | +| `beast.caged` | Caged beasts are refused for everyone | + +A player who may not catch a beast is turned away by their own Nab-Sack, with the game's "This beast cannot be rescued", or "Caged beasts cannot be rescued" for a caged one. Their attempt never reaches your scripts. + +```js +// server/main.js +// Only the player who earned it may catch the reward hippogriff. +const reward = Beast.create("Hippogriff", { x, y, z, name: "Buckbeak" }); +reward.setCapturableFor(player, true); +``` + +## Refereeing a catch + +A catch reaches the server twice. Both events run synchronously, so a handler that returns `false` refuses it: + +1. The Nab-Sack starts drawing in the beast, and `beastCaptureStart(beast, player)` fires. Refuse it and the Nab-Sack lets go. +2. The Nab-Sack finishes, and the player's game holds the catch until the server answers. The server checks that the beast is still there, within 25 m, in a [virtual world](/guides/core-concepts/virtual-worlds/) the player can see, catchable for them, not caged and not already being caught by someone else. Then `beastCaptureRequest(beast, player)` fires. Refuse it and the beast stays in the world. + +Once a catch is approved, the beast leaves every player's world, the server destroys it, and `beastCaptured(player, captured)` fires with the new bag entry. When two players catch the same beast, the first catch approved wins. + +```js +// server/main.js +const BAG_LIMIT = 6; + +Events.on("beastCaptureRequest", (beast, player) => { + if (Beast.bagOf(player).length >= BAG_LIMIT) { + player.sendChat(`Your Nab-Sack is full. Release a beast before catching the ${beast.species}.`); + return false; + } +}); + +Events.on("beastCaptured", (player, captured) => { + player.sendChat(`You caught a ${captured.species}.`); +}); +``` + +The server also rate-limits what each client reports about its catches. + +## The bag + +`Beast.bagOf(player)` lists a player's bag: what they caught on this connection and what scripts gave them. Each entry is `{ id, species, color, gender, name }`. Its `id` names the entry for `Beast.takeCaptured` and lasts only as long as the connection, so never store it. + +| Call | Effect | +| --- | --- | +| `Beast.giveCaptured(player, beast)` | Puts a beast in the bag, in the player's Field Guide too, without the game's catch notification. Takes `species` and optional `color`, `gender` and `name`. Returns the new entry, or `null` when the player is not connected | +| `Beast.takeCaptured(player, id)` | Takes a beast out of the bag and the player's Field Guide. `false` when the bag holds no such entry | + +`giveCaptured` throws, like `Beast.create`, for an unknown species, a colour the species does not have, a gender other than `"male"` or `"female"`, or a name over 64 UTF-8 bytes. Leave `color` out to roll on the game's own weights, and `gender` to give a male. A beast given before the player's game is ready reaches it once it is. + +## Keeping catches + +Because the bag belongs to the connection, save it whenever it changes and give it back when the player joins. Save the species, colour, gender and name, not the entry ids: + +```js +// server/main.js +function keepBag(player) { + const beasts = Beast.bagOf(player).map(({ species, color, gender, name }) => ({ species, color, gender, name })); + saveBag(player, beasts); +} + +Events.on("beastCaptured", (player) => keepBag(player)); +Events.on("beastReleased", (player) => keepBag(player)); + +Events.on("playerConnect", async (player) => { + const saved = await loadBag(player); + if (!saved || !player.connected) return; + for (const beast of saved) Beast.giveCaptured(player, beast); +}); +``` + +`saveBag` and `loadBag` stand for your own store, such as the database behind [Persisting state](/guides/getting-started/first-resource/#persisting-state). Call `keepBag` after your own `giveCaptured` and `takeCaptured` calls too. + +## Releasing beasts + +When a player releases a beast from their Field Guide, the game drops it from the bag, and `beastReleased(player, released, at)` fires. `released` is the entry as it was, and `at` is where the player stood. The game does not put the beast back in the world, so bring it back yourself if you want it there: + +```js +// server/main.js +Events.on("beastReleased", (player, released, at) => { + const { species, color, gender, name } = released; + const beast = Beast.create(species, { x: at.x, y: at.y, z: at.z, color, gender, name, preferredOwnerId: player.id }); + beast.capturable = true; // anyone may catch it again; leave this out to keep it wild +}); +``` + +A script can release a beast without the Field Guide, for example from a menu of its own on a server that [keeps the Field Guide shut](/guides/user-interface/keys-and-menus/#the-games-own-menus). Take the beast out of the bag and create it in the world. This one appears 3 m in front of the player, facing them: + +```js +// server/main.js +function releaseInFront(player, entryId) { + const entry = Beast.bagOf(player).find((beast) => beast.id === entryId); + if (!entry || !Beast.takeCaptured(player, entry.id)) return null; + const yaw = player.location()?.yaw ?? 0; + const radians = (yaw * Math.PI) / 180; + const { x, y, z } = player.position; + return Beast.create(entry.species, { + x: x + Math.cos(radians) * 300, + y: y + Math.sin(radians) * 300, + z, + yaw: yaw + 180, + color: entry.color, + gender: entry.gender, + name: entry.name, + preferredOwnerId: player.id, + }); +} +``` + +`takeCaptured` does not raise `beastReleased`, so save the bag afterwards as well. + +## Where to go next + +- [Beasts and characters](/guides/npcs-and-creatures/beasts-and-characters/) for spawning beasts, their options, and petting and feeding them. +- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for keeping a beast to the players who should see it. +- [Keys and menus](/guides/user-interface/keys-and-menus/) for putting your own interface in place of the game's menus. diff --git a/guides/overview.md b/guides/overview.md index 57f44b0..1c47fdc 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -38,6 +38,7 @@ Use the navigation to browse the globals available to the selected environment. - [NPCs](/guides/npcs-and-creatures/npcs/) — spawning enemies, who runs their AI, controlling their health, targets and size, and switching their behaviour off. - [Beasts and characters](/guides/npcs-and-creatures/beasts-and-characters/) — tame creatures players care for, and stationary townspeople. +- [Catching beasts](/guides/npcs-and-creatures/catching-beasts/) — the Nab-Sack, refereeing catches, keeping a player's bag across reconnects, and releasing beasts. **PvE:**