From 2ef0c960415908b78df43408b1c43bfeb00edaef Mon Sep 17 00:00:00 2001 From: KHeartz Date: Tue, 13 Oct 2026 23:28:39 -0400 Subject: [PATCH] docs: add brooms and mounts, races and Quidditch guides New Mounts and races section (orders 80-89), written against 1.8.2: owning and riding brooms and creatures, broom overrides and keeping riders out, the game's broom races on custom tracks timed by the server, and the client-side Quidditch props with the match run on the server. --- README.md | 2 +- guides/Mounts and races/brooms-and-mounts.md | 129 +++++++++++++++++++ guides/Mounts and races/quidditch.md | 52 ++++++++ guides/Mounts and races/races.md | 99 ++++++++++++++ guides/overview.md | 6 + 5 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 guides/Mounts and races/brooms-and-mounts.md create mode 100644 guides/Mounts and races/quidditch.md create mode 100644 guides/Mounts and races/races.md diff --git a/README.md b/README.md index fc4c814..d231dff 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, 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 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. - 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/brooms-and-mounts.md b/guides/Mounts and races/brooms-and-mounts.md new file mode 100644 index 0000000..77880bf --- /dev/null +++ b/guides/Mounts and races/brooms-and-mounts.md @@ -0,0 +1,129 @@ +--- +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 +--- + +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. + +## What players own + +A player can only ride what is in their inventory, and a joining player starts with an empty one, so give them a broom or a creature first: + +| Kind | Item ids | +| --- | --- | +| Brooms | `BroomHouse`, `BroomFamilyAntique`, `BroomFlyingClass1` to `BroomFlyingClass3`, `BroomAeromancer`, `BroomBrightSpark`, `BroomDarkWizard1`, `BroomDarkWizard2`, `BroomEmberDash`, `BroomInfiltrator`, `BroomLicketySwift`, `BroomMoonTrimmer`, `BroomNightDancer`, `BroomSilverArrow`, `BroomSkyScythe`, `BroomWildFire`, `BroomWindWisp`, `BroomYewWeaver` | +| Flying creatures | `HippogriffMount`, `OnyxHippogriffMount`, `ThestralMount` | +| Ground creature | `GraphornMount` | + +The first broom a player is given goes straight into their active slot, ready to ride; later ones go into storage. The same goes for each kind of creature. The onyx hippogriff and the thestral need the game's add-ons that unlock them. + +```js +// server/main.js +Events.on("loadingFinished", (player) => { + if (!player.inventory.has("BroomHouse")) player.inventory.give("BroomHouse", 1); +}); +``` + +Taking the item away is how the server keeps a player from summoning it; a player already riding is taken off by their client, as in [Keeping riders out](#keeping-riders-out). The server cannot see whether a player is riding, and no event fires when anyone mounts or dismounts, so client scripts check for themselves. + +## Riding a broom + +`Broom.mount()` gets the local player on the broom in their active slot, and `Broom.equip(itemId)` picks which of their brooms that is. `mount` returns whether the game accepted the request, not whether the player is riding: they settle on the broom over a second or so, and the first request can be turned down while the game loads the broom, so ask again until they are on. + +| Call | Returns | +| --- | --- | +| `Broom.mounted()` | Whether the player is on a broom or getting on | +| `Broom.state().mountSettled` | Whether they are seated and flying | +| `Broom.canMount()` | Whether the game lets them fly here and now; it does not check that they own a broom | +| `Broom.isEquipped()` | Whether a broom is in their active slot | + +```js +// client/main.js +Events.on("broom:mount", () => { + if (!Broom.isEquipped()) Broom.equip(); + let attempts = 0; + const timer = setInterval(() => { + if (Broom.state().mountSettled || ++attempts > 10) { + clearInterval(timer); + return; + } + if (!Broom.mounted()) Broom.mount(); + }, 500); +}); +``` + +`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. + +## 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 | +| `Broom.addBoost(fraction)` | Fills part of the boost meter; `boost()` and `inTurbo()` read it | +| `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. + +A knock out of the sky, without dismounting: + +```js +// client/main.js +Events.on("broom:stun", () => { + Broom.setInputDisabled(true); + Broom.setForceLand(true); + setTimeout(() => { + Broom.setForceLand(false); + Broom.setInputDisabled(false); + }, 3000); +}); +``` + +## Creatures + +`FlyingMount` rides the hippogriffs and the thestral, and `GroundMount` the graphorn. They work like `Broom`: + +- `equip(itemId)` puts one of the player's creatures in the active slot, and `mount(options)` summons it. `mount` returns whether the request was accepted, so check `mounted()`, which only turns true once the player is seated, and `summoning()` for the summon in between. +- `mount({ transition: false })` skips the summon animation, and `FlyingMount.mount({ inFlight: true })` starts the player in the air. +- `dismount()` takes the player off. Called during a summon, it cancels the summon instead, so call it again. +- `canMount()` asks the game whether creatures are allowed here. It can return `false` because another player stands somewhere creatures are not allowed, so treat `false` as a maybe. + +A rider on a flying creature can be steered with `FlyingMount.takeoff()`, `FlyingMount.land()` and `FlyingMount.setAutoLanding(enabled)`, and `FlyingMount.setFlyingGait(gait)` sets the slowest pace the rider can drop to: `"stop"`, `"hover"`, `"slow"` or `"fast"`. `takeoff` is a request, so check `FlyingMount.isFlying()` and ask again. + +```js +// client/main.js +Events.on("hippogriff:fly", () => { + FlyingMount.equip("HippogriffMount"); + FlyingMount.mount({ inFlight: true }); +}); +``` + +`state()` on either reports the creature's `species`, whether the player is `mounted`, and for flying creatures whether they are `flying`. + +## Keeping riders out + +To keep players off brooms and creatures in an arena, take the items away on the server while they are inside, and have their client dismount them and keep the game's tool wheel, where players summon them, shut: + +```js +// client/main.js +Events.on("arena:enter", () => { + Game.setToolWheelEnabled(false); + if (Broom.mounted()) Broom.dismount(); + if (FlyingMount.mounted()) FlyingMount.dismount(); + if (GroundMount.mounted()) GroundMount.dismount(); +}); + +Events.on("arena:leave", () => Game.setToolWheelEnabled(true)); +``` + +The server's part is the one that holds: a client script can be changed by its player, an inventory cannot. + +## Where to go next + +- [Races](/guides/mounts-and-races/races/) for running the game's broom races on tracks of your own. +- [Inventory](/guides/players/inventory/) for giving and taking items. diff --git a/guides/Mounts and races/quidditch.md b/guides/Mounts and races/quidditch.md new file mode 100644 index 0000000..16881ba --- /dev/null +++ b/guides/Mounts and races/quidditch.md @@ -0,0 +1,52 @@ +--- +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 +--- + +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 new file mode 100644 index 0000000..6b53a82 --- /dev/null +++ b/guides/Mounts and races/races.md @@ -0,0 +1,99 @@ +--- +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 +--- + +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. + +## How a race works + +Each race in the game is an object standing at its starting pad, named like `BRR_01`, the one near the Hogwarts Quidditch pitch. `Races.list()` lists the races loaded around the player, and a race is only loaded while the player is near its pad, so bring racers there before you start. The fast-travel point `M_FT_BRR_01` is at that race's pad, so `player.fastTravel("M_FT_BRR_01")` on the server takes a player there. + +To run one, a client: + +1. Gives the race your track with `Races.setTrack(raceId, points)`: at least three world positions, in centimetres. The race puts its rings along the track. +2. Turns off the race's leash with `Races.setLeash(raceId, false)`. The leash pulls riders back towards the curve through the track's points. +3. Keeps the race loaded along the whole track with `Races.fitBounds(raceId, points)`. The game unloads a race once the player leaves the area around its pad, and takes its rings with it. +4. Starts it with `Races.start(raceId)`. The race puts the player on their broom itself, so they need to own one: [give them a broom](/guides/mounts-and-races/brooms-and-mounts/#what-players-own) first, and check with `Races.hasBroom()`. + +`Races.spawnBoosts(raceId, fractions)` adds turbo bubbles at points along the track, given as fractions from 0 at the start to 1 at the finish. `Races.getTrack(raceId)` reads a race's track, and `Races.active()` returns the race the player is in. + +`Races.setLeash(raceId)` with no second argument also turns the leash off. + +## Racing together + +Each racer runs their own copy of the race, so the server sends every racer the same track. Their rings then stand in the same places, and they see each other fly the course. Another racer cannot fly through your rings for you, but they can pick up your turbo bubbles. Players who are not racing see the racers, but none of the rings. + +The server keeps the time. A client's race clock stops when the game pauses, so it cannot be compared between players, and a time a client reports can be made up. Each client tells the server when its own countdown ends and when it finishes, and the server times the gap. + +`Races.watchEvents()` turns on the race's events, which arrive as `gameEvent` with the name of the game function that fired. The ones a race needs are `OnRaceCountdownComplete`, `OnRaceFinish` and `OnRaceAbandoned`. The same name can fire more than once, so take the first one. + +```js +// client/main.js +Races.watchEvents(); + +Events.on("race:begin", (race) => { + if (!Races.list().some((loaded) => loaded.raceId === race.raceId)) { + Events.emitServer("race:failed", { reason: "too far from the start" }); + return; + } + Races.setTrack(race.raceId, race.points); + Races.setLeash(race.raceId, false); + Races.fitBounds(race.raceId, race.points); + if (Races.start(race.raceId)) Races.spawnBoosts(race.raceId, [0.25, 0.5, 0.75]); +}); + +Events.on("gameEvent", (functionName) => { + if (functionName === "OnRaceCountdownComplete") Events.emitServer("race:started"); + if (functionName === "OnRaceFinish") Events.emitServer("race:finished"); +}); +``` + +```js +// server/main.js +const racers = new Map(); // player id -> { started, finished } + +function startRace(players, points) { + for (const player of players) { + racers.set(player.id, { started: 0, finished: 0 }); + player.emit("race:begin", JSON.stringify({ raceId: "BRR_01", points })); + } +} + +Events.onClient("race:started", (player) => { + const racer = racers.get(player.id); + if (racer && !racer.started) racer.started = Date.now(); +}); + +Events.onClient("race:finished", (player) => { + const racer = racers.get(player.id); + if (!racer || !racer.started || racer.finished) return; + racer.finished = Date.now(); + const seconds = ((racer.finished - racer.started) / 1000).toFixed(1); + Chat.sendToAll(`${player.nickname} finished in ${seconds} seconds.`); +}); +``` + +Give the race a time limit on the server too, and count anyone who has not finished by then as out. + +## Cleaning up + +Boosts and the bigger loading area belong to the client, not to your resource. `Races.clearBoosts()` removes the bubbles and `Races.restoreBounds()` puts the race's area back, so call both when a race ends and as your resource stops. A race that has started keeps running if your resource stops. + +```js +// client/main.js +Events.on("resourceStop", (resourceName) => { + if (resourceName !== "broom-races") return; + Races.clearBoosts(); + Races.restoreBounds(); +}); +``` + +`Races.abandon(raceId)` force-ends a race that will not finish, and `Races.clearGhosts()` and `Races.hideHud()` clear a pace ghost or a race display left behind by one. + +## 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. diff --git a/guides/overview.md b/guides/overview.md index c7015c4..2194a8f 100644 --- a/guides/overview.md +++ b/guides/overview.md @@ -58,6 +58,12 @@ Use the navigation to browse the globals available to the selected environment. - [Audio](/guides/camera-and-sound/audio/) — the game's sounds from the server or a client, soundbanks and music. - [Voice chat](/guides/camera-and-sound/voice-chat/) — who hears whom, voice ranges and tiers, muting, and each player's voice settings. +**Mounts and races:** + +- [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. + **Running a server:** - [Hosting a server](/guides/hosting-a-server/run-a-server/) — installing on Windows or Linux, Foundations, ports, the masterlist, and updates.