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
68 changes: 59 additions & 9 deletions guides/World/doors.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
---
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.
description: Hold the game's doors locked, unlocked, open or closed for every player, set the Alohomora level a padlock asks for, share the locks players open, find a door's name, and decide when a locked door opens for someone.
sidebar:
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.

Portcullises, the Astronomy rune gate, goblin camp gates and crawl grates are not doors, and no `Doors` call reaches them. See [Gates](/guides/world/gates/).

## Holding a door's state

`Doors.set(selector, state)` holds a door's lock, its open state or both, for every player:

```js
```jsx
// server/main.js
Doors.set(vaultDoor, { locked: true, open: null });
Doors.set(hallDoor, { locked: null, open: true });
Expand All @@ -22,13 +24,25 @@ Doors.set(hallDoor, { locked: null, open: true });
| `locked` | Held locked. Only a door that has a lock can be locked. | Held unlocked | Left to each player's save |
| `open` | Held open | Held closed; it still swings for anyone who walks into it unless it is locked | Left to swing on its own |

At runtime a field you leave out keeps whatever the server already holds for it, but the current type declarations ask for both fields, so pass `null` for the one you mean to leave to the game.
At runtime a field you leave out keeps whatever the server already holds for it, but the current type declarations ask for every field, `lockLevel` included, so pass `null` for the ones you mean to leave to the game.

Each player's game re-applies what the server holds as the door streams in, so players who arrive later see it too.

- `Doors.get(selector)` returns the state held under a selector, and `Doors.list()` every held state.
- `Doors.clear(selector)` releases a selector, and `Doors.clearAll()` releases them all. A released door keeps its current state until it streams in again, when each player's save takes over.
- A selector left holding nothing, with both fields `null`, is released.
- A selector left holding nothing, with every field `null`, is released.
- Each leaf of a double door is its own door and swings on its own, so hold both leaves to keep a double door shut.

### Lock levels

A padlocked door asks for a level of Alohomora, from 1 to 3, and its callout shows that level. `lockLevel` holds the level for every player, so a player needs that level of Alohomora to cast it on the door. `null` leaves the door at the level it shipped with. Only padlocked doors carry a level.

```js
// server/main.js
Doors.set(towerDoor, { locked: null, open: null, lockLevel: 3 });
```

Players gain the levels through the Alohomora locks in [Progression](/guides/players/progression/#spells). Held together with `locked: true`, the lock refuses even a player whose Alohomora is high enough, and the server raises [`doorUnlockRequest`](#when-a-player-tries-a-locked-door) so your script can decide.

## Finding a door

Expand All @@ -52,14 +66,30 @@ Events.on("resourceStart", (resourceName) => {
});
```

`Doors.list(radius)` returns each loaded door within `radius` centimetres, 3000 by default, with its name, persistent id, full path, distance and lock state. The output goes to the [client log](/guides/getting-started/debugging/#where-output-goes).
`Doors.list(radius)` returns each loaded door within `radius` centimetres, 3000 by default, with its name, persistent id, full path, distance and lock state. A padlocked door also has its `lockLevel`, or `"key"` when only a key opens it. The output goes to the [client log](/guides/getting-started/debugging/#where-output-goes).

## When a player tries a locked door
## When a player unlocks a door

When a player tries to open a lock the server holds shut, with a key or Alohomora, their game refuses at once and the server raises `doorUnlockRequest` with the player and the door: its `selector`, `name` and `id`. Release the lock there if the player should get in:
When a player opens a lock with a key or Alohomora, the server holds that door unlocked for everyone, under its persistent id or, without one, its path. `Doors.setShareUnlocks(false)` keeps each unlock in the player's own game instead, and `Doors.getShareUnlocks()` reads the setting.

Either way the server raises `doorUnlocked` with the player and the door: its `name`, persistent `id`, `path` and whether the unlock was `shared`. `id` is empty for a lock that is not on a door.

```js
// server/main.js
Events.on("doorUnlocked", (player, door) => {
if (!door.shared) return;
Chat.sendToAll(`${player.nickname} unlocked ${door.name} for everyone.`);
});
```

`Doors.set` or `Doors.clear` on that selector takes a shared unlock back. An unlock is not shared while the server holds the door locked.

## When a player tries a locked door

When a player tries to open a lock the server holds shut, with a key or Alohomora, their game refuses at once and the server raises `doorUnlockRequest` with the player and the door: its `selector`, `name` and `id`. It fires at most once a second for each door. Release the lock there if the player should get in:

```jsx
// server/main.js
const vaultKeyHolders = new Set();

Events.on("doorUnlockRequest", (player, door) => {
Expand All @@ -72,18 +102,38 @@ Events.on("doorUnlockRequest", (player, door) => {
});
```

Unlocking with `Doors.set` opens the door for everyone, not only the player who asked.
Unlocking with `Doors.set` unlocks the door for everyone, not only the player who asked.

- On a double door, `selector` may name the other leaf, when that leaf's held lock is what refused the player.
- A lock the player's own client policy keeps shut raises it too, with an empty `selector`.

## On the client

Client scripts can change doors too, but only for their own player, and the server's held state always wins:

- `Doors.setLocked(selector, locked)` locks or unlocks matching doors on this client only, until they stream in again.
- `Doors.setPolicy(policy)` keeps streamed doors unlocked or locked on this client: unlock all of them, a list, or all except a list, and lock a list, which beats any unlock.
- `Doors.superAlohomora(true)` lets every lock open to a key or Alohomora, whatever the player's Alohomora level. Locks the server holds still refuse, and it is cleared on disconnect.
- `Doors.superAlohomora(true)` lets every lock open to a key or Alohomora, whatever the player's Alohomora level, key-only locks included. Locks the server holds still refuse, and it is cleared on disconnect.
- `Doors.held()` lists the door states the server holds, as this client received them.

## With Foundations

HMP Foundations' [`hmp-doors`](https://github.com/hogwarts-mp/foundations/tree/main/resources/hmp-doors) decides which doors, gates and locks each player may pass, from ranked rules in its `data/hmp-doors.json`. A rule allows, denies or locks doors by name or path, can apply only to players in certain `hmp-core` groups, and can hold a padlock's `lockLevel` for everyone. Lower priorities are stronger. `shareUnlocks` turns shared unlocks on, and is `false` by default.

```json
{
"shareUnlocks": false,
"rules": [
{ "priority": 1000, "action": "allow", "doors": "*" },
{ "priority": 100, "action": "lock", "doors": ["BP_Door_Headmaster"], "lockLevel": 3 }
]
}
```

That is a fragment of the file. Staff in its `adminGroups` manage it in game with `/doors`: `/doors list [radius]` lists the doors near you with their names and paths. Its README covers every rule, grant and command, and its gate rules are in [Gates](/guides/world/gates/#with-foundations).

## Where to go next

- [Gates](/guides/world/gates/) for portcullises, the rune gate, camp gates and crawl grates.
- [Loot](/guides/world/loot/) for chests behind your doors.
- [Authority](/guides/core-concepts/authority/) for why a request like `doorUnlockRequest` is the server's to decide.
157 changes: 157 additions & 0 deletions guides/World/gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
title: Gates
description: Open, close and lock the game's portcullises, the Astronomy rune gate, goblin camp gates and crawl grates, for everyone or for one player.
sidebar:
order: 525
---

Besides its doors, the game has gates: portcullises, a rune-puzzle gate, goblin camp gates and the grates over goblin crawl spaces. Gates are not doors. Each kind is its own actor, so no `Doors` call reaches a gate and no `Gates` call reaches a door. The server can hold a gate's state for everyone, and a client script can change gates for its own player.

## The four kinds

| `kind` | Gates | Opens | Closes | Locks |
| --- | --- | --- | --- | --- |
| `portcullis` | The Clocktower, outer wall and dungeon portcullises | Yes | Yes | Yes |
| `runeGate` | The rune gate from the Astronomy Class quest, `BP_ZZS_TundraGate_2` | Yes, skipping its puzzle | Yes | No |
| `swingGate` | Goblin camp gates | Yes | Yes | No |
| `crawlGrate` | Breakable grates over goblin crawl spaces | Yes, by breaking it | No | No |

- An unlocked portcullis stays down, rises as a player walks up to it and lowers a second after the last one leaves. An open one stays raised.
- The rune gate lets players through about 7 seconds after it starts to open.
- Opening a crawl grate breaks it, which cannot be undone.

## Finding a gate

To learn a gate's name, stand near it with a client script that lists the gates around the player:

```js
// client/main.js
Events.on("resourceStart", (resourceName) => {
if (resourceName !== "my-gamemode") return;
for (const gate of Gates.list(3000)) {
console.log(`${gate.kind} ${gate.name} open=${gate.isOpen} locked=${gate.isLocked} ${Math.round(gate.dist)} cm away`);
}
});
```

`Gates.list(radius, kind)` returns the loaded gates within `radius` centimetres, nearest first. A `radius` of 0, the default, returns every loaded gate, and `kind` narrows the list to one kind or an array of them. The output goes to the [client log](/guides/getting-started/debugging/#where-output-goes).

| Field | Meaning |
| --- | --- |
| `name` | The actor name. Several placements can share one. |
| `path` | The full path name, which belongs to one placement |
| `kind` | One of the four kinds above |
| `dist`, `bearing` | Centimetres from the player, -1 when unknown, and degrees off the player's facing, 999 when unknown |
| `x`, `y`, `z` | World position, left out when the gate could not be located |
| `isOpen` | Open or opening. The rune gate reads `true` once it no longer blocks the way. |
| `isLocked` | The portcullis lock, or `null` for a kind without one |
| `canClose` | Whether it can be closed. `false` for a crawl grate. |
| `canLock` | Whether it can be locked. `true` only for a portcullis. |
| `lift` | How far open, from 0 to 1, or `null` where that cannot be measured |
| `hasBeenOpened`, `useSchedule`, `wantsToOpen` | Portcullis only: whether walking up to it raises it, whether the clock still moves it, and whether it stays raised after players walk through |

Every `Gates` call that takes a selector reads it as an actor name, which can match several placements, or as a path name when it holds `/` or `:`, which matches one.

## Holding a gate's state

`Gates.set(selector, state)` holds a gate's lock, its open state or both, for every player:

```js
// server/main.js
Gates.set("BP_ZZS_TundraGate_2", { locked: null, open: true });
Gates.set("BP_HW_ClocktowerPortcullis2", { locked: true, open: null });
```

| Field | `true` | `false` | `null` |
| --- | --- | --- | --- |
| `locked` | Held locked and closed. A kind without a lock is held closed instead. | Held unlocked | Left to each player's save and gate policy |
| `open` | Held open | Held closed. A crawl grate cannot close. | Left to each player's game |

As with `Doors.set`, a field you leave out keeps whatever the server already holds for it at runtime, but the current type declarations ask for both fields, so pass `null` for the one you mean to leave to the game.

Each player's game applies the held state when it arrives and again whenever a matching gate streams in, whatever that player's save or gate policy says.

- A held lock beats a held open.
- Only a portcullis can be held shut. A player can still open any other gate held closed their own way, by its puzzle, a spell or Interact, until it next streams in.
- `Gates.get(selector)` returns the state held under a selector, and `Gates.list()` every held state.
- `Gates.clear(selector)` releases a selector, and `Gates.clearAll()` releases them all, other resources' included. A released gate keeps its current state until it streams in again.
- A selector left holding nothing, with both fields `null`, is released. A malformed selector or a field that is not a boolean or `null` throws a `TypeError`.

## On the client

Client scripts can change gates for their own player. A change lasts until the gate streams in again, and the server's held state always wins:

```js
// client/main.js
const opened = Gates.setOpen("BP_ZZS_TundraGate_2", true);
if (opened === 0) console.log("The rune gate is not loaded.");
```

- `Gates.setOpen(selector, open)` opens, and unlocks, or closes matching gates.
- `Gates.setLocked(selector, locked)` locks, and closes, or unlocks matching portcullises.
- `Gates.freezeSchedule(selector)` takes matching portcullises off the clock.

Each returns how many loaded gates matched and support the call, so closing a crawl grate or locking a camp gate counts 0.

### A policy for each player

`Gates.setPolicy(policy)` keeps gates open, unlocked or locked on this client, on every loaded gate now and on each gate as it streams in:

```js
// client/main.js
Gates.setPolicy({
unlockAll: ["portcullis"],
openAll: ["swingGate"],
freezeSchedule: true,
lockGates: ["BP_HW_ClocktowerPortcullis_2", "BP_HW_ClocktowerPortcullis2", "BP_HW_ClocktowerPortcullis_LOD2"],
});
```

| Field | Meaning |
| --- | --- |
| `unlockAll` | `true`, or a list of kinds: unlock every gate of those kinds and leave it closed |
| `openAll` | `true`, or a list of kinds: open every gate of those kinds, and keep a portcullis raised |
| `allExcept` | Gates that `unlockAll` and `openAll` skip |
| `unlockGates` | Gates to unlock, whatever their kind |
| `openGates` | Gates to open, whatever their kind |
| `lockGates` | Gates to close and lock. This beats any unlock or open in the policy. A kind without a lock is closed instead, until the player opens it their own way. |
| `freezeSchedule` | Take every portcullis off the clock |

Each call replaces the policy before it and returns how many loaded gates it checked. `Gates.policy()` returns the policy last set, with `true` spelled out as all four kinds and every list present. `Gates.held()` lists the gate states the server holds, as this client received them.

Each field the server holds overrides the policy for that gate. A lock still beats an open, so a gate the policy locks stays shut while the server only holds it open. Hold it `{ locked: false, open: true }` to open it for everyone.

## With Foundations

HMP Foundations' [`hmp-doors`](https://github.com/hogwarts-mp/foundations/tree/main/resources/hmp-doors) handles gates beside [doors](/guides/world/doors/#with-foundations). In its `data/hmp-doors.json`, a rule with `gates` in place of `doors` takes gate names or `"*"`, and ranks and matches groups like a door rule:

```json
{
"gateSchedule": "freeze",
"heldGates": { "BP_ZZS_TundraGate_2": { "open": true } },
"rules": [
{ "priority": 1000, "action": "allow", "gates": "*", "kind": "portcullis" },
{ "priority": 500, "action": "lock", "gates": ["BP_HW_ClocktowerPortcullis_2", "BP_HW_ClocktowerPortcullis2", "BP_HW_ClocktowerPortcullis_LOD2"] }
]
}
```

- `allow` unlocks a gate and leaves it closed, and with `"open": true` opens it instead. `deny` withholds both.
- `lock` closes and locks the gates it names. A kind without a lock is closed instead, until a player opens it their own way.
- `kind` narrows a `"*"` rule to one kind or a list of them. Without it, `"*"` covers every kind.
- `heldGates` holds gates for everyone through `Gates.set` while the resource runs, and releases what it held when it stops.
- `gateSchedule` is `"local"` by default, which leaves the game's schedule running, or `"freeze"` to take every portcullis off the clock.

Staff list the gates near them with `/doors gates [radius]`, hold one for everyone with `/doors gateset <locked|unlocked|open|closed|release> <name>`, and list what the server holds with `/doors gatesheld`.

## Things to watch for

- **The Clocktower gate is three actors:** `BP_HW_ClocktowerPortcullis_2`, the one players walk through, plus the distant copies `BP_HW_ClocktowerPortcullis2` and `BP_HW_ClocktowerPortcullis_LOD2`. A rule or selector that names it must list all three, or use `"*"` with `kind: "portcullis"`.
- **Scope wide opens with `kind`.** `openAll: true`, or an `open: true` rule on `"*"` without a `kind`, breaks every crawl grate and skips the rune gate's puzzle.
- **Freeze portcullis schedules.** Most portcullises rise at 06:00 and lower at 20:00 on each player's own game clock, which the server's [shared clock](/guides/world/time-and-weather/#the-clock-and-calendar) does not drive, so two players can see the same gate in different places. Set `freezeSchedule: true` in the policy, or `hmp-doors`' `gateSchedule` to `"freeze"`.
- **A gate a player opens their own way is not shared.** Solving the rune gate's puzzle, breaking a grate with a spell or opening a camp gate with Interact changes it in that player's game only, and the server is not told. Hold the gate open with `Gates.set` when everyone should find it open.

## Where to go next

- [Doors](/guides/world/doors/) for the game's doors and locks.
- [Time and weather](/guides/world/time-and-weather/) for the shared clock.
1 change: 1 addition & 0 deletions guides/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Use the navigation to browse the globals available to the selected environment.
- [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.
- [Gates](/guides/world/gates/) — portcullises, the rune gate, goblin camp gates and crawl grates, held for everyone or set for one player.
- [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.

Expand Down
Loading