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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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, 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.
Expand Down
116 changes: 116 additions & 0 deletions guides/User interface/hud-and-nametags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
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
---

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.

## Hiding the HUD

The game sorts its HUD into named groups. `Hud.groups()` lists them, read from the game itself, and `Hud.hideGroup(group)` hides one until `Hud.showGroup(group)` lets it go:

```js
// client/main.js
function startCutscene() {
for (const group of Hud.groups()) Hud.hideGroup(group);
}

function endCutscene() {
for (const group of Hud.groups()) Hud.showGroup(group);
}
```

The game also hides groups of its own, and a group stays hidden while either side hides it. The player's own HUD settings are left alone, so there is nothing to put back afterwards.

A hidden group is held by the mod, not by your resource: `showGroup` from any resource releases it, and stopping your resource does not, so show what you hid yourself. Every group comes back when the player disconnects.

`Hud.setVisible(false)` hides the whole HUD with one switch for the whole client. Any resource can turn it back on, and it is not restored on disconnect, so prefer groups. `Settings.setHud` changes the player's own HUD settings and belongs only behind a settings panel they opened.

## Interaction prompts

`Hud.showPrompt(key, label, x, y, z)` floats a `[key] label` prompt at a world position, in centimetres. It follows that point on screen and hides while the point is off screen. There is one prompt per client, so showing another replaces it, and `Hud.hidePrompt()` takes it away.

The prompt only shows text. Pair it with a [key binding](/guides/user-interface/keys-and-menus/#binding-keys), and use `Key.getLabel` so the key it names matches the player's keyboard layout:

```js
// client/main.js
const counter = { x: 12000, y: -4500, z: 300 };
let atCounter = false;

setInterval(() => {
const pos = LocalPlayer.getPosition();
const near = !!pos && Math.hypot(pos.x - counter.x, pos.y - counter.y, pos.z - counter.z) <= 250;
if (near && !atCounter) Hud.showPrompt(Key.getLabel("f"), "Browse wands", counter.x, counter.y, counter.z + 100);
if (!near && atCounter) Hud.hidePrompt();
atCounter = near;
}, 200);

Key.bind("f", () => {
if (atCounter) Events.emitServer("shop:open", { shop: "wands" });
});
```

The server receives `shop:open` as a request from the client, so check there that the player really stands at the counter before opening anything. [Authority](/guides/core-concepts/authority/) covers handling what a client sends.

## Nametags

Every player has a nametag: their name over their head, with a health bar under it. The server sets what everyone sees on a player's nametag:

| Call | Does |
| --- | --- |
| `player.setNametagText(text)` | Shows `text` instead of the player's name, cut to 64 bytes. Empty or no text restores the name. |
| `player.setNametagColor(color)` | Tints the text, as a packed `0xAARRGGBB` number. |
| `player.setNametagVisible(visible)` | Shows or hides the player's nametag for every other player. |
| `player.setNametagHealthVisible(visible)` | Shows or hides only the health bar under it. |

`getNametagText()`, `getNametagColor()`, `isNametagVisible()` and `isNametagHealthVisible()` read them back.

```js
// server/main.js
const STAFF_GOLD = 0xffd4af37; // alpha first, then red, green, blue

function markStaff(player) {
player.setNametagText(`[Staff] ${player.nickname}`);
player.setNametagColor(STAFF_GOLD);
}
```

The nametag colour puts alpha first, while chat colours put it last as `0xRRGGBBAA`.

### On the client

Each client can change what its own player sees, without affecting anyone else:

- `Nametags.setVisible(false)` hides every nametag. A nametag the server hid stays hidden either way.
- `Nametags.setHealthVisible(false)` hides the health bars and keeps the names.
- `Nametags.setSelfVisible(true)` draws the player's own nametag, as others see it. It is off by default.

`Nametags.setLabel(entityId, text, durationMs, color)` hangs a passing line above a player's name, for speech, an emote or a status. It follows them and fades and hides with the name. It lasts 6 seconds by default, a duration of 0 or less holds it until cleared, and `\n` starts a new line. `color` is a packed `0xAARRGGBB` number; leave it out for the nametag's own colour. Labels are local: the server sends the event, and each client draws it:

```js
// server/main.js
Events.on("chatCommand", (player, message, command, args) => {
if (command.toLowerCase() !== "me" || args.length === 0) return;
Events.emitAllClients("emote", { playerId: player.id, text: `*${args.join(" ")}*` });
});
```

```js
// client/main.js
Events.on("emote", (payload) => {
Nametags.setLabel(payload.playerId, payload.text, 5000);
});
```

`Nametags.clearLabel(entityId)` removes a label early, and `Nametags.clearLabels()` removes all of them.

## Notices

`Game.notify(text)` shows a line in the local chat, seen only by this player. From the server, [`player.sendChat`](/guides/players/chat-and-commands/#sending-chat) does the same for one player.

## Where to go next

- [Keys and menus](/guides/user-interface/keys-and-menus/) for key bindings and the game's own menus.
- [Web views](/guides/user-interface/web-views/) for drawing interfaces of your own in HTML.
74 changes: 74 additions & 0 deletions guides/User interface/keys-and-menus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
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
---

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.

## Binding keys

`Key.bind(key, handler)` runs `handler` when a key is pressed. Pass `"up"` or `"both"` before the handler to run it when the key is released, or on both:

```js
// client/main.js
Key.bind("f2", () => Events.emitServer("menu:open"));

Key.bind("lshift", "both", (key, state) => {
Events.emitServer("sprint", { held: state === "down" });
});
```

The handler receives the key's name and `"down"` or `"up"`. A key name stands for a key's position, named as on a US keyboard, so `y` is the same physical key on every layout. Names ignore case:

| Keys | Names |
| --- | --- |
| Letters and digits | `a` to `z`, `0` to `9` |
| Function keys | `f1` to `f12` |
| Number pad | `numpad0` to `numpad9`, or `num0` to `num9` |
| Modifiers | `shift`, `lshift`, `rshift`, `ctrl` or `control`, `lctrl`, `rctrl`, `alt`, `lalt`, `ralt` |
| Arrows | `up`, `down`, `left`, `right` |
| Others | `space`, `enter` or `return`, `escape` or `esc`, `tab`, `backspace`, `capslock`, `insert`, `delete`, `home`, `end`, `pageup`, `pagedown` |
| Mouse buttons | `mouse1` to `mouse5` |

An unknown name throws. A key pressed while Alt is held does not fire, so Alt+Tab never reaches your handlers. Bindings fire only while the player is in the game and nothing else has their input: not while the game is in the background or loading, while the chat is open, while a [web view](/guides/user-interface/web-views/) has focus, or while the controls are [locked](#locking-the-controls). A binding does not take the key from the game, which still acts on it, so pick keys the game leaves free.

A binding belongs to the resource that made it and goes when that resource stops. `Key.unbind(key, state, handler)` removes your bindings for a key, all of them when you leave out the state and handler.

`Key.isDown(key)` reads whether a key is held, behind the same checks. `Key.getLabel(key)` returns what the player's own layout prints on that key, such as `Z` for `y` on a German keyboard, so a [prompt](/guides/user-interface/hud-and-nametags/#interaction-prompts) names the key the player sees.

## Locking the controls

`Game.lockControls(true)` shows the cursor and stops the player's character from responding to input, and `Game.lockControls(false)` gives control back. Each resource keeps a count: pair every `true` with one `false`, and the controls stay locked while any resource holds one. Stopping a resource releases what it held. `Game.areControlsLocked()` tells you whether anything holds a lock.

A focused web view locks the controls and shows the cursor by itself while it has focus, so there is no need to lock them around `Web.focusView`.

## The game's own menus

Each of these switches keeps one of the game's menus from opening, so a resource can put its own interface on the keys behind it:

| Call | Keeps shut |
| --- | --- |
| `Game.setPauseMenuEnabled(false)` | The Field Guide, the game's pause menu, with every page hotkey and the escape key |
| `Game.setMenuPageEnabled(page, false)` | One page of the Field Guide; the rest keeps working |
| `Game.setActionScreenEnabled(false)` | The action screen, where players edit their spell sets |
| `Game.setToolWheelEnabled(false)` | The tool wheel, along with its quick-item slot on the HUD |

The pages are `"inventory"`, `"gear"`, `"talents"`, `"quests"`, `"map"`, `"mail"`, `"collections"`, `"challenges"` and `"settings"`. A page kept shut is greyed out, skipped when paging and ignored by its hotkey.

```js
// client/main.js
// This resource keeps inventories itself, so the game's inventory page stays out of reach.
Game.setMenuPageEnabled("inventory", false);
Game.setToolWheelEnabled(false);
```

Each resource holds a switch once, however often it calls it, and a menu stays shut while any resource holds it. Passing `true` gives back only your own hold. Holds go when the resource stops or reloads and when the player disconnects, so there is nothing to undo. A menu that is already open when you shut it stays open until the player closes it.

`Game.isPauseMenuEnabled()` and its siblings return `false` while any resource keeps that menu shut. `Game.pauseMenuState()` and its siblings also name the resources holding it, for finding out which one does.

## Where to go next

- [Web views](/guides/user-interface/web-views/) for drawing the menus that replace the game's.
- [HUD, prompts and nametags](/guides/user-interface/hud-and-nametags/) for prompts that tell players which key to press.
119 changes: 119 additions & 0 deletions guides/User interface/map-markers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
---
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
---

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.

## Markers and circles

`Blips.setMarker(key, x, y, z, icon, label)` puts a marker at a world position, in centimetres. `Blips.setCircle(key, x, y, z, radius)` draws a search circle instead, with its radius in **metres**, 50 by default.

The key is a name of your choosing. Setting the same key again moves or restyles that blip rather than adding another, so a moving target is a repeated `setMarker`. `Blips.remove(key)` takes one away. Leave `icon` out for the game's default marker icon.

```js
// client/main.js
Blips.setMarker("hunt:clue", 15200, -3400, 250, undefined, "Next clue");
Blips.setCircle("hunt:area", 15200, -3400, 250, 40);
Blips.pulseCircle("hunt:area", true);
```

`Blips.pulseCircle(key, true)` makes a circle pulse, and `false` fades it in instead. `Blips.inArea(key, x, y, z)` tells you whether a point is inside a circle.

### Who owns a blip

Blips belong to the client, not to the resource that set them. Every resource shares one set of keys, so prefix yours with something your own, and `Blips.clear()` removes every script blip, whichever resource set it. Stopping your resource leaves its blips on the map; only disconnecting clears them.

Keep the list on the server and send it to clients, including players who join after a blip went up:

```js
// server/main.js
const markers = new Map(); // key -> { key, x, y, z, label }

function showMarker(marker) {
markers.set(marker.key, marker);
Events.emitAllClients("marker:set", marker);
}

function hideMarker(key) {
markers.delete(key);
Events.emitAllClients("marker:remove", { key });
}

// A client asks for the whole set once its script is running.
Events.onClient("marker:ready", (player) => {
player.emit("marker:all", JSON.stringify([...markers.values()]));
});
```

```js
// client/main.js
Events.on("marker:set", (m) => Blips.setMarker(m.key, m.x, m.y, m.z, undefined, m.label));
Events.on("marker:remove", (m) => Blips.remove(m.key));
Events.on("marker:all", (list) => {
for (const m of list) Blips.setMarker(m.key, m.x, m.y, m.z, undefined, m.label);
});

Events.emitServer("marker:ready");
```

A marker the server sends to everyone is on everyone's map. When a position should stay secret, such as the circle hunters are searching, send it only to the players meant to see it.

## Routes

`Blips.setRoute(target, label)` draws the game's gold mission route on the map and minimap, to a marker or circle by its key, or to another player's blip by their network id while they have one. It returns `false` when there is no such blip or the game did not take the route. Each client follows one route, so setting another replaces it:

```js
// client/main.js
Blips.setRoute("hunt:area", "Search for the clue");
```

- A route to a circle stops at its ring, so the player is led there and then searches inside it.
- It survives its blip being set again, so moving the circle to the next clue keeps the route on it.
- While the player has placed a waypoint of their own, their purple line takes over the minimap. The big map keeps drawing your route beside it.

`Blips.clearRoute()` ends the route, and removing its blip ends it too. `Blips.route()` returns the key or network id the route leads to, or `null`.

## Player blips

Every other player has a blip by default. To show only some of them, turn that off and name the players who keep theirs:

```js
// client/main.js
Events.on("party:members", (payload) => {
Blips.trackAllPlayers(false);
Blips.setTrackedPlayers(payload.memberIds);
});
```

`Blips.trackPlayer(networkId, enabled)` adds or removes one player, and works before that player has spawned. `Blips.trackedPlayers()` lists the network ids that have a blip.

`Blips.setPlayerColor(networkId, r, g, b, a)` colours one player's blip, with components from 0 to 1. Leave out the colour to go back to their house colour. `Blips.setAllPlayerColors(r, g, b, a)` colours every blip, and `Blips.setBlipScale(scale)` sizes them.

Hiding a blip hides an icon, not a player: their position still reaches the client. To keep players apart, use [virtual worlds](/guides/core-concepts/virtual-worlds/), and to hide one from everyone, `player.setVisible(false)` on the server, which takes their blip with them.

## The minimap

`Minimap.state()` reports what the game's own minimap is drawing:

| Field | Meaning |
| --- | --- |
| `map` | The region on screen: `"overland"`, `"hogwarts"`, `"hogsmeade"`, `"dungeon"`, `"tutorial"`, `"sanctuary"`, `"tents"`, or `"none"` behind a load screen |
| `floor` | In Hogwarts, the floor the game picked for the player, counting from 0; `null` elsewhere |
| `texture` | In Hogwarts, the floor's map tile; `null` elsewhere |

The `minimapChanged` event fires with the new state and the previous one whenever any of them changes. `Minimap.floorAt(z)` returns the Hogwarts floor the game would pick at a height, for placing other things on floors the way the game places the player.

```js
// client/main.js
Events.on("minimapChanged", (state, previous) => {
if (state.map === "hogwarts" && previous.map !== "hogwarts") Game.notify("Welcome back to the castle.");
});
```

## Where to go next

- [HUD, prompts and nametags](/guides/user-interface/hud-and-nametags/) for the rest of the game's HUD.
- [Virtual worlds](/guides/core-concepts/virtual-worlds/) for keeping groups of players apart.
Loading
Loading