API.md version 5: the profile's boss and use blocks, the passive role and stand, the boss hooks and OnRunicNpcUsed, role and boss in RunicNpc_List, RunicNpc_Despawn taking a boss's adds, the example kits written once, and a Kits reload taking RunicNPC with it (D283). PROTOCOL.md §19.14: npc.boss.spawned, npc.boss.phase and npc.boss.died, their classes, and runicnpc_api 5. PLAN.md stage 6: what was built, the builds it was tested on, the harness and the site walk on both rigs, and what building it found. The example kits' permission is kits.runicnpc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
408 lines
26 KiB
Markdown
408 lines
26 KiB
Markdown
# RunicNPC — the API
|
||
|
||
**API version 5** (RunicNPC stage 6, 2026-10-05). This is the reference for other plugins. Why it has this
|
||
shape is in [PLAN.md](PLAN.md) §4 and the decisions D221–D238, D249, D253–D272 and D273–D289. **API 5 added**
|
||
the profile's `boss` and `use` blocks and the `passive` role with its `stand` movement, the hooks
|
||
`OnRunicNpcBossSpawned`, `OnRunicNpcBossPhase`, `OnRunicNpcBossDied` and `OnRunicNpcUsed`, and `role` and `boss`
|
||
in `RunicNpc_List` (PLAN.md stage 6). Nothing of API 4 changed shape. **API 4 added** the faction
|
||
table (`RunicNpc_Factions`, `RunicNpc_SetFactions`), orders for one NPC (`RunicNpc_Escort`, `RunicNpc_Ally`,
|
||
`RunicNpc_Tether`), the hook `OnRunicNpcEscortEnded`, the profile's stage 5 fields, a placement's `tether`, and
|
||
more in `RunicNpc_List` (PLAN.md stage 5). Nothing of API 3 changed shape. **API 3 added**
|
||
`RunicNpc_AddPlacement`, `RunicNpc_RenamePlacement`, `RunicNpc_RespawnPlacement` and the hook
|
||
`OnRunicNpcPlacementChanged` (stage 4).
|
||
|
||
RunicNPC is called by name through `Call`, like any Oxide or Carbon plugin API. Every call is prefixed
|
||
`RunicNpc_`, so no hook of another plugin is ever matched by accident. Every call is a non-public method, which is
|
||
what Oxide's `Call` reaches by name (PLAN.md §1.2).
|
||
|
||
```csharp
|
||
[PluginReference] private Plugin RunicNPC;
|
||
|
||
int api = RunicNPC?.Call<int>("RunicNpc_ApiVersion") ?? 0;
|
||
if (api < 5) { /* too old for this caller: say so */ }
|
||
|
||
BasePlayer npc = RunicNPC.Call("RunicNpc_Spawn", position, "warden", "plugin:MyPlugin", null) as BasePlayer;
|
||
```
|
||
|
||
**The version moves when a call or a raised hook changes shape**, not on every release. `plugin.toml` in the
|
||
repository declares the same number, and the release's `manifest.json` carries it, so an installer or the
|
||
bridge can refuse a RunicNPC that is too old before it is loaded.
|
||
|
||
A call that cannot do what it was asked **returns null (or 0, or false) and says why in the server log**. Calls
|
||
that change data return the reason as a string instead, and null means success.
|
||
|
||
---
|
||
|
||
## Owners
|
||
|
||
Every NPC has an owner, and the owner decides its lifetime (PLAN.md §2).
|
||
|
||
| Owner | Who uses it | Lifetime |
|
||
|---|---|---|
|
||
| `run:<id>` | An event run, through the bridge | Never saved. Removed by the caller, or by `RunicNpc_DespawnOwner`. |
|
||
| `plugin:<name>` | Any other plugin | Never saved. **Removed when that plugin unloads.** Use your plugin's own name. |
|
||
| `placement:<id>` | RunicNPC itself | The NPC is never saved; the placement is, and spawns a fresh one at boot. Callers cannot spawn with this owner. |
|
||
|
||
---
|
||
|
||
## Calls
|
||
|
||
### `RunicNpc_ApiVersion()` → `int`
|
||
|
||
The API version: `5`.
|
||
|
||
### `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer`
|
||
|
||
Spawns one NPC now, or returns null.
|
||
|
||
- `owner` must be `run:<id>` or `plugin:<name>`.
|
||
- `overrides` may change any profile value for this NPC only, in the profile's own shape (below). It may be null.
|
||
For example: `{"health": 500, "names": ["Captain"], "movement": {"mode": "route:gate", "radius": 0}}`. Arrays
|
||
are replaced, not merged. The merged profile is validated like a saved one.
|
||
- A roamer must stand on Rust's navmesh: within 3 m across and 2 m up or down of `at`. A sentry may stand
|
||
anywhere.
|
||
- It is refused until the map's navmesh is built, which takes 8–10 minutes on a map's first boot.
|
||
|
||
Refusals: an unknown or refused profile, a bad owner, a failed override, a spot off the navmesh, a cap an admin has
|
||
set, or a navmesh that is not built yet.
|
||
|
||
### `RunicNpc_Despawn(ulong netId)` → `int`
|
||
|
||
Removes one NPC by its net id. Returns 1, or 0 if it is not one of RunicNPC's. A placement's NPC comes back after
|
||
its respawn delay. **For a boss (API 5), it also removes the adds that boss summoned, even once the boss is dead**,
|
||
and counts them in what it returns: an event's teardown removes what its step placed by net id, and a boss's adds
|
||
are in no step's ledger (D287).
|
||
|
||
### `RunicNpc_DespawnOwner(string owner)` → `int`
|
||
|
||
Removes every NPC of `owner`, and returns how many it removed.
|
||
|
||
### `RunicNpc_List(string owner)` → `List<Dictionary<string, object>>`
|
||
|
||
The live NPCs of `owner`; null lists them all. Each has:
|
||
|
||
| Key | Type | |
|
||
|---|---|---|
|
||
| `netId` | `ulong` | |
|
||
| `profile`, `name`, `owner`, `kit` | `string` | The kit is the one picked for this NPC. |
|
||
| `placement` | `string` | Its placement's id, or null. |
|
||
| `position`, `home` | `{x, y, z}` | `home` is where it spawned: where it wanders around, and walks back to before it sleeps. |
|
||
| `health`, `maxHealth` | `float` | |
|
||
| `movement` | `string` | `wander`, `monument` or `route:<name>`. |
|
||
| `rest` | `string` | `Awake`, `GoingHome` or `Asleep` (D235). |
|
||
| `state` | `string` | Rust's AI state: `Idle`, `Roam`, `Chase`, `Combat` and so on. |
|
||
| `leashes` | `int` | How many times it has given up a chase at its chase range. |
|
||
| `regrounded` | `int` | How many times its ground went from under it and it was put on the nearest navmesh (D239). |
|
||
| `faction` | `string` | API 4. Its faction, or `profile:<name>` when its profile has none (D256). |
|
||
| `target` | `string` | API 4. The prefab of the NPC, animal or turret it is fighting itself, or null (players are Rust's design's). |
|
||
| `engaged` | `bool` | API 4. Whether it is fighting that target now (Rust's design fights players first). |
|
||
| `escort` | `string` | API 4. Whom it escorts: a player's Steam id, or an entity's prefab; null for none. |
|
||
| `ally` | `string` | API 4. `clan:<id>`, `team:<id>` or `player:<steam id>`, or null. |
|
||
| `tether` | `string` | API 4. The ZoneManager zone it is held in, or null. |
|
||
| `shotsMarked` | `int` | API 4. Shots it made at an NPC target (each marks the target `Horror` for that shot only). |
|
||
| `kitUsed` | `object` | API 4. `heals`, `throws`, `rockets` and `meleeHits`: the kit's extras it has used (D260). |
|
||
| `role` | `string` | API 5. Its profile's role: `roamer`, `sentry`, `guard` or `passive`. |
|
||
| `boss` | `object` | API 5. For a boss, `phase` (0 before the first, then 1, 2 …) and `damagers` (how many players have hurt it); null for any other NPC. |
|
||
|
||
### `RunicNpc_Profiles()` → `JObject`
|
||
|
||
`{"managed": bool, "profiles": {name: profile, …}, "factions": [pair, …], "refused": {name: reason, …}}`. A
|
||
refused profile is kept in the file but never spawns (a kit the server lacks, for example). `factions` is the
|
||
faction table (API 4).
|
||
|
||
### `RunicNpc_SetProfiles(JObject all)` → `Dictionary<string, string>`
|
||
|
||
Replaces the whole profile set with `all` (`{name: profile, …}`), as a site does on every push, and **marks the
|
||
server managed** (D221). It returns the profiles refused and why; the rest are in use at once. A placement whose
|
||
profile has gone waits, and spawns again when the profile returns (D237).
|
||
|
||
### `RunicNpc_Placements()` → `List<Dictionary<string, object>>`
|
||
|
||
Each placement as `{id, placement, alive, waiting, lastError, note}`. `placement` is the placement's JSON (below),
|
||
`alive` is how many of its NPCs are alive, and `waiting` says why it cannot spawn right now, or is null. `note` is
|
||
set while its NPCs stand somewhere other than its spot because the ground there went (D239, below), or is null.
|
||
|
||
### `RunicNpc_SetPlacement(string id, JObject placement)` → `string`
|
||
|
||
Adds or replaces a persistent placement (D222). Null on success, or the reason. Replacing one despawns its NPCs and
|
||
spawns them again from the new values. Every set logs the cost warning (D227).
|
||
|
||
A roamer's spot must be on Rust's navmesh, within 2 m up or down, when it is set; otherwise the answer says how far
|
||
off it is, as `rnpc place` does (D219). The check is skipped while the map's navmesh is still being built, and
|
||
for a profile that does not exist yet. **If the ground under a placement goes later** (a player-built floor is
|
||
destroyed, D239), a live NPC is put on the nearest navmesh within 2 s, and the placement respawns its NPCs on the
|
||
nearest navmesh until its spot is walkable again; `note` says so meanwhile. Rust itself leaves such an NPC standing
|
||
in the air.
|
||
|
||
### `RunicNpc_RemovePlacement(string id)` → `bool`
|
||
|
||
Removes a placement and its NPCs.
|
||
|
||
### `RunicNpc_AddPlacement(JObject placement)` → `Dictionary<string, object>`
|
||
|
||
API 3. **Creates a placement and names it as `rnpc place` does**: the profile's name and the first free
|
||
number, `warden-3` (D241, D246). It is checked and refused exactly as `rnpc place` is, with the same
|
||
sentences, so a placement made from a website and one made in game can never disagree.
|
||
|
||
- `placement` is a placement (below) without an id. **A `position` without `y` is a point on a map**
|
||
(D245): it is put on the ground there by a ray down on terrain and rock, never a building or a tree (a
|
||
roof is placed in game), then checked against the navmesh like any other. A point off the map, or under
|
||
water, is refused.
|
||
- The answer is `{id, position, built, cost}`: the name given, where it landed, whether that spot is on
|
||
something players built (D239's warning), and the cost warning (D227). Or `{error}`, the sentence
|
||
`rnpc place` would have said.
|
||
|
||
### `RunicNpc_RenamePlacement(string from, string to)` → `string`
|
||
|
||
API 3. Renames a placement, as `rnpc rename`: its live NPCs keep living under the new name. Null on
|
||
success, or why not (no such placement, the name taken, not a name).
|
||
|
||
### `RunicNpc_RespawnPlacement(string id)` → `int`
|
||
|
||
API 3. Removes a placement's NPCs and spawns them again at once, as `rnpc respawn`. How many, or `-1` if
|
||
there is no such placement.
|
||
|
||
### `RunicNpc_Routes()` → `JObject`
|
||
|
||
`{name: route, …}`.
|
||
|
||
### `RunicNpc_SetRoute(string name, JObject route)` → `string`
|
||
|
||
Adds or replaces a route (D234). Every point must be on Rust's navmesh. Null on success, or the reason. Unlike
|
||
`rnpc path` (D240), it does not check that an NPC can walk from each point to the next: a leg Rust cannot path
|
||
leaves the NPC standing at the last point it reached, so a caller that builds routes should check its own legs.
|
||
|
||
### `RunicNpc_RemoveRoute(string name)` → `bool`
|
||
|
||
Removes a route. Placements that walk it wait until it exists again.
|
||
|
||
### `RunicNpc_IsRunicNpc(BaseEntity entity)` → `bool`
|
||
|
||
Whether `entity` is one of RunicNPC's NPCs.
|
||
|
||
### `RunicNpc_ProfileOf(BaseEntity entity)` → `string`
|
||
|
||
Its profile's name, or null.
|
||
|
||
### `RunicNpc_Factions()` → `JArray`
|
||
|
||
API 4. The faction table (D254): `[{"a": "bandits", "b": "guards", "relation": "hostile"}, …]`, one row per pair,
|
||
both ways (D268). A pair not listed is neutral, and a faction is always allied to itself.
|
||
|
||
### `RunicNpc_SetFactions(JArray pairs)` → `string`
|
||
|
||
API 4. Replaces the faction table, as a site does with every profile push, and **marks the server managed**
|
||
(D221). Null on success; otherwise the first problem, and the old table is kept. Refused: a pair of one faction
|
||
with itself, a pair of `scientists` and `animals` (RunicNPC does not change how Rust's NPCs treat each other), a
|
||
relation other than `hostile`, `neutral` or `allied`, and a pair given twice in either order.
|
||
|
||
### `RunicNpc_Escort(ulong netId, BaseEntity target)` → `string`
|
||
|
||
API 4 (D258, D269). One of ours escorts `target`: a player, or any other entity (a crate it guards, a vehicle).
|
||
It keeps within a few metres, never targets it, and fights whoever damages it. **When the target dies, is
|
||
destroyed, or (a player) leaves the server, it walks back to its spot**, carries on as its profile does there, and
|
||
`OnRunicNpcEscortEnded` is raised. A null `target` stops it. Null on success; refused for a sentry (it never
|
||
moves) and for a player who is not on the server.
|
||
|
||
### `RunicNpc_Ally(ulong netId, string kind, string id)` → `string`
|
||
|
||
API 4 (D257, D270). One of ours is allied to `clan` (the game's own clan id), `team` (a Rust team id), or
|
||
`player` (a Steam id: that player, and their team if they have one), or to nobody with `none`. It **never
|
||
targets** the ally's people, and **defends them and what they own** (building blocks, doors, deployables): whoever
|
||
damages one of them within its leash is fought. Rust's own clans and teams only. Null on success; refused for a
|
||
clan or team the server does not have.
|
||
|
||
### `RunicNpc_Tether(ulong netId, string zone)` → `string`
|
||
|
||
API 4 (D272). Holds one of ours inside a ZoneManager zone: it never chases or wanders out, and if it ends up
|
||
outside (pushed, or the zone moved) it forgets the fight and walks home. Null frees it. Refused for a sentry, on a
|
||
server without ZoneManager, for a zone the server does not have, and for a zone that does not hold the NPC.
|
||
|
||
### `RunicNpc_CostWarning(int adding)` → `string`
|
||
|
||
The cost warning (D227) for the NPCs the server plans, plus `adding`. It is built from stage 1's measurements
|
||
(PLAN.md §9), and says what the NPCs add to a server frame, idle and fighting, and how slowly each then reacts. A
|
||
caller that adds NPCs shows it to whoever added them.
|
||
|
||
---
|
||
|
||
## Hooks it raises
|
||
|
||
All are called on every plugin, and **none of them answers**: a return value is ignored.
|
||
|
||
| Hook | When |
|
||
|---|---|
|
||
| `OnRunicNpcSpawned(BasePlayer npc, string profile, string owner)` | After an NPC spawns and is equipped. |
|
||
| `OnRunicNpcHealth(BasePlayer npc, string profile, float threshold)` | The first time its health falls to one of the profile's `healthThresholds`. |
|
||
| `OnRunicNpcDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary<ulong, float> contributors)` | When it dies. `contributors` maps each player's Steam id to the health that player took from it, the killing blow included, after Rust's protection. It never adds up to more than its health. |
|
||
| `OnRunicNpcDespawned(BasePlayer npc, string owner)` | When it is removed without dying. Not raised as RunicNPC itself unloads. |
|
||
| `OnRunicNpcPlacementChanged(string id, string change, string previous)` | API 3. Whenever a placement is set, removed or renamed, through the API or in game. `change` is `set`, `removed` or `renamed`; `previous` is the old name for a rename and null otherwise. |
|
||
| `OnRunicNpcEscortEnded(BasePlayer npc, string profile, string owner)` | API 4. Whom it escorted died, was destroyed or left the server, and it is walking back to its spot (D269). |
|
||
| `OnRunicNpcBossSpawned(BasePlayer npc, string profile, string owner)` | API 5. After `OnRunicNpcSpawned`, for a profile with a `boss` block. Its spawn line has been said. |
|
||
| `OnRunicNpcBossPhase(BasePlayer npc, string profile, string owner, int phase, float at)` | API 5. It entered a phase: `phase` counts from 1, `at` is the phase's health fraction. Raised after the phase's changes are applied and its line is said. A hit that crosses two thresholds raises both, highest first. |
|
||
| `OnRunicNpcBossDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary<ulong, float> contributors)` | API 5. After `OnRunicNpcDied`, for a boss, with the same contributors. Its death line has been said. RunicNPC gives no reward itself (D276, D285): an event's own reward steps do. |
|
||
| `OnRunicNpcUsed(BasePlayer npc, string profile, BasePlayer player)` | API 5. A player pressed E on a passive NPC and was answered (D278). At most once a second per player. |
|
||
|
||
---
|
||
|
||
## Data shapes
|
||
|
||
### A profile (D238)
|
||
|
||
```json
|
||
{
|
||
"names": ["Warden", "Old Warden"],
|
||
"kits": ["warden_rifle", "warden_smg"],
|
||
"prefab": "scientistnpc_roam",
|
||
"role": "roamer",
|
||
"movement": { "mode": "wander", "radius": 20 },
|
||
"health": 250,
|
||
"damageDealt": 1.0,
|
||
"damageTaken": { "head": 1.0, "body": 1.0, "legs": 1.0 },
|
||
"aimCone": 2.0,
|
||
"ranges": { "sense": 30, "loseTarget": 40, "chase": 40, "attack": 30 },
|
||
"visionCone": -0.8,
|
||
"sleepDistance": 160,
|
||
"healthThresholds": [0.5],
|
||
"faction": "bandits",
|
||
"relations": { "scientists": "hostile" },
|
||
"alertRadius": 40,
|
||
"turrets": "default",
|
||
"hurtByPlayers": true,
|
||
"hurtsPlayers": true,
|
||
"kitUse": { "heal": false, "grenades": false, "melee": false, "rockets": false, "flamethrower": false },
|
||
"boss": {
|
||
"barDistance": 100,
|
||
"announce": "chat",
|
||
"spawnLine": "{name} has risen!",
|
||
"deathLine": "{name} has fallen to {killer}.",
|
||
"phases": [
|
||
{ "at": 0.5, "adds": { "profile": "raider", "count": 3 }, "line": "{name} calls for help!" },
|
||
{ "at": 0.25, "damageDealt": 1.5, "speed": 1.3, "kit": "warden_heavy", "line": "{name} is enraged!" }
|
||
]
|
||
},
|
||
"use": null
|
||
}
|
||
```
|
||
|
||
A passive profile has no `boss` and a `use` block:
|
||
|
||
```json
|
||
{
|
||
"names": ["Quartermaster"], "kits": ["outpost_clothes"], "role": "passive",
|
||
"movement": { "mode": "stand", "radius": 0 },
|
||
"hurtByPlayers": false,
|
||
"use": { "mode": "chat", "lines": ["Back again, {player}?", "Supplies are short."], "title": "", "text": "" }
|
||
}
|
||
```
|
||
|
||
| Field | Rule | Meaning |
|
||
|---|---|---|
|
||
| `names` | at least one | One is picked for each NPC. |
|
||
| `kits` | at least one; each must exist in Kits | One is picked for each NPC, and it is equipped with it (D217). |
|
||
| `prefab` | a Rust `scientistnpc_*` prefab that is a plain scientist | What it is built from. |
|
||
| `role` | `roamer`, `sentry`, `guard` or `passive` | A sentry never moves, and may stand off the navmesh. A guard (API 4) holds its spot facing the way it was placed, chases within its chase range, and walks back. A passive NPC (API 5) never fights: it takes no target, does not shoot back and raises no alarm, and pressing E on it does what its `use` says (D278, D279). |
|
||
| `movement.mode` | `wander`, `monument`, `route:<name>`, or `stand` for a passive profile only | D233. A placement may override it. A passive NPC that stands (D288) holds its spot as a sentry does, may stand off the navmesh, and turns to whoever presses E on it. |
|
||
| `movement.radius` | above 0 for `wander` | How far from home it wanders. |
|
||
| `health` | above 0 | Its health, and its maximum. |
|
||
| `damageDealt` | 0 or more | Multiplies its weapon's damage; 1 is the weapon's full damage. |
|
||
| `damageTaken` | each 0 or more | Multiplies the damage it takes to the head, the legs (legs and feet), and the body (everything else, including hits with no body part, like fire and explosions). |
|
||
| `aimCone` | 0 or more | How much its aim spreads; Rust's scientists use 2. |
|
||
| `ranges.sense` | above 0 | How far it notices players. |
|
||
| `ranges.loseTarget` | at least `sense` | How far a target must get before it is forgotten. |
|
||
| `ranges.chase` | 0 or more | How far from home it will chase (0: no limit). At that distance it waits 5 s for the target to come within range, then gives up and walks home. |
|
||
| `ranges.attack` | above 0 | How far it shoots from. This replaces the weapon's own range. |
|
||
| `visionCone` | −1 to 1 | Rust's vision cone. |
|
||
| `sleepDistance` | 0 or more | D235: with no player this close it walks home, then sleeps. 0: it never sleeps. |
|
||
| `healthThresholds` | fractions between 0 and 1 | Where `OnRunicNpcHealth` is raised. |
|
||
| `faction` | API 4. null, or 1–40 of a-z, 0-9, _ and -, not `scientists` or `animals` | D254. Profiles of one faction are allies. With none, its allies are its own profile's NPCs (D256). |
|
||
| `relations` | API 4. keys: a faction, `scientists`, `animals` or `profile:<name>`; values `hostile`, `neutral` or `allied` | Its own exceptions, which win over the faction table (D254). **A profile with no faction settings fights players only** (D255). |
|
||
| `alertRadius` | API 4. 0 or more; default 40 | D256. Allies this close learn whoever attacked one of them, seen or not. 0 is off. |
|
||
| `turrets` | API 4. `default`, `ignore` or `always` | D259, D264, D267. `default`: as Rust's scientists (player turrets shoot it, safe-zone sentries do not). `ignore`: no turret targets it. `always`: Outpost's and Bandit Camp's sentries too, through a Harmony patch that answers for these NPCs only. It shoots back at a turret that shoots it. |
|
||
| `hurtByPlayers`, `hurtsPlayers` | API 4. true or false; default true, **except `hurtByPlayers` for a passive profile, which is false unless it is given** (API 5, D279) | D261, D271. Off makes it unkillable by players, or harmless to them. RunicNPC enforces both itself, and answers TruePVE's and NextGenPVE's `CanEntityTakeDamage` the same way. Turrets, fire and other NPCs are not affected. |
|
||
| `kitUse` | API 4. `heal`, `grenades`, `melee`, `rockets`, `flamethrower`, each true or false | D260. The kit's extras it uses, each only if true and each used up from its inventory. The main weapon (the belt's first slot) is always used and never runs out (D265). |
|
||
|
||
| `boss` | API 5. null, or the block below; not on a passive profile | D273. Makes any roamer, sentry or guard a boss, which keeps moving and fighting as its role does. |
|
||
| `boss.barDistance` | above 0, at most 1000; default 100 | D274. Players this close see its health bar at the top of the screen, and so does every player who has hurt it, wherever they are, until it dies or despawns. A player near two bosses sees the nearer. |
|
||
| `boss.announce` | `chat` or `popup`; default `chat` | D277. Where its lines are said. `popup` uses PopupNotifications where it is loaded and chat where it is not. |
|
||
| `boss.spawnLine`, `boss.deathLine` | at most 256 characters; blank says nothing | D286. Said to everyone on the server. `{name}` is its name; in the death line `{killer}` is the killing blow's player (empty when no player). |
|
||
| `boss.phases` | a list; each `at` a fraction between 0 and 1, no two alike | D275. When its health falls to `at`, the phase applies from then on. Every other part is optional. |
|
||
| `phases[].damageDealt`, `aimCone` | 0 or more | Replace the profile's. |
|
||
| `phases[].speed` | above 0, at most 5 | A multiple of Rust's own speed for it: 1.3 is a third faster. |
|
||
| `phases[].ranges` | as the profile's `ranges` | Replace the profile's. |
|
||
| `phases[].kit` | a Kits kit the server has | A kit swap: its inventory is stripped, the kit given and its weapon equipped. |
|
||
| `phases[].adds` | `profile`: a profile that is not a boss; `count`: 1 to 20 | NPCs of that profile placed on a 6 m ring around it, one per server frame. They belong to its owner (an event's teardown removes them) and fight on after it dies (D287). |
|
||
| `phases[].line` | at most 256 characters | D286. Said only to the players near it (within `barDistance`) and to those who have hurt it. |
|
||
| `use` | API 5. required on a passive profile, refused on any other | D278. What pressing E on it does, from up to 3 m: `mode` `chat` says one of `lines` (picked at random, each at most 256 characters) in that player's chat; `mode` `window` opens a window with `title` (blank: its name), `text` (at most 2,000 characters) and a Close button. `{player}` in either is the player's name. |
|
||
|
||
A phase changes **that boss's own copy** of its profile; another NPC of the same profile is not touched. A boss gives
|
||
no reward itself (D276, D285).
|
||
|
||
**Whom it fights besides players** is its own sensing (stage 5): only the factions it is hostile to, within its
|
||
sense range of itself **and its chase range of its spot** (D263), so it never walks into a monument to look for
|
||
scientists. Rust's scientists never fight back (Rust's own rule); they take cover. Whoever shoots one of ours, it
|
||
fights back, whatever the table says (D268).
|
||
|
||
### A placement
|
||
|
||
```json
|
||
{
|
||
"profile": "warden",
|
||
"position": { "x": 100.0, "y": 12.5, "z": -340.0 },
|
||
"yaw": 90,
|
||
"count": 3,
|
||
"respawn": 300,
|
||
"respawnMode": "each",
|
||
"movement": { "mode": "route:gate", "radius": 0 }
|
||
}
|
||
```
|
||
|
||
- `count` NPCs spawn at the spot, and all but the first are scattered within 3 m of it.
|
||
- `respawn` is in seconds, at least 1. `respawnMode` is `each` (every NPC returns `respawn` after its own death)
|
||
or `group` (none returns until all are dead, then all return together) (D236).
|
||
- `movement` is optional and overrides the profile's.
|
||
- `tether` (API 4) is optional: a ZoneManager zone its NPCs never leave (D272). Setting a placement with a tether
|
||
the server does not have, or one that does not hold the spot, is refused; a placement whose zone later goes waits,
|
||
as one whose profile has gone does (D237).
|
||
|
||
### A route
|
||
|
||
```json
|
||
{ "points": [ { "x": 1, "y": 2, "z": 3 }, { "x": 4, "y": 2, "z": 6 } ], "loop": true }
|
||
```
|
||
|
||
At least two points, each on Rust's navmesh. With `loop`, the last point leads back to the first; without it, the
|
||
route is walked back and forth. An NPC joins the route at its nearest point.
|
||
|
||
---
|
||
|
||
## Files and config
|
||
|
||
- `data/RunicNPC/profiles.json` (`managed`, `profiles` and, from API 4, `factions`), `placements.json` and `routes.json`, under `oxide/`
|
||
or `carbon/`. On a standalone server profiles are edited in the file, then `rnpc.reload` reads them again. On a
|
||
managed one the site pushes them.
|
||
- `data/RunicNPC/state.json` (API 5): `examplesWritten`, set on the first load. That load writes the four example
|
||
kits (`rnpc_raider`, `rnpc_campguard`, `rnpc_sniper`, `rnpc_juggernaut`) into Kits' own data file, each where
|
||
its name is free and each needing the permission `kits.runicnpc` (Kits' prefix, since Kits registers it), which nobody is granted, so no player can
|
||
claim one. On a server no site manages it also adds the four example profiles (`raider`, `campguard`, `sniper`,
|
||
`juggernaut`). Then it reloads Kits, which reloads RunicNPC with it. **Nothing is written again, even if you
|
||
delete them** (D280, D284, D289).
|
||
- **A Kits reload reloads RunicNPC** on both frameworks, because RunicNPC requires Kits, and the NPCs it had spawned
|
||
are lost: placements come back, an event's NPCs do not. **Do not reload Kits while an event runs** (D283).
|
||
- `config/RunicNPC.json`:
|
||
- `caps`: `total`, `perOwner`, `perProfile` and `spawnsPerSecond`. **Each is 0, meaning no cap, until an admin
|
||
sets it** (D227).
|
||
- `spawnBudgetMs`: how many milliseconds of each server frame placements may spend spawning. The default is 8.
|
||
- `rnpc.status` at the console reports the swap's field check, the NPCs by owner and rest, profiles refused,
|
||
placements waiting, the caps, and the cost warning; from API 4 also the faction pairs, the scientists and animals
|
||
it tracks, how many of ours are fighting an NPC, the alarms and defences raised, what its own fight costs a
|
||
think, and whether the sentries are patched (`turrets: always`).
|
||
- `rnpc.faction list | set <a> <b> hostile|neutral|allied | clear <a> <b>` (console) edits the faction table on a
|
||
standalone server; refused while a site manages it. `/rnpc follow <placement> <player|me|off>` makes a
|
||
placement's live NPCs escort a player, for trying it out (in memory only: a respawn or reload ends it).
|
||
`/rnpc place … tether=<zone>` holds a placement inside a zone.
|
||
- `rnpc.profile set <name> boss on|none` and `rnpc.profile set <name> use on|none` (API 5) add a profile's boss
|
||
box or press-E block with its defaults, or remove it. Their fields are then set one at a time
|
||
(`boss.spawnLine The Juggernaut has risen!`), and a list of objects as JSON
|
||
(`boss.phases [{"at":0.5,"line":"…"}]`). `role passive` also turns `hurtByPlayers` off (D279).
|