From 6d319c843b6dcf17947b725e19feea4b13cd7b34 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 5 Oct 2026 13:12:16 -0500 Subject: [PATCH] =?UTF-8?q?docs(runicnpc):=20stage=206=20built=20and=20wal?= =?UTF-8?q?ked;=20API=205;=20protocol=20=C2=A719.14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- runicnpc/API.md | 79 ++++++++++++++++++++++++++++++++++++++----- runicnpc/PLAN.md | 66 ++++++++++++++++++++++++++++++++++-- rust-link/PROTOCOL.md | 41 ++++++++++++++++++++++ 3 files changed, 174 insertions(+), 12 deletions(-) diff --git a/runicnpc/API.md b/runicnpc/API.md index 8e3ef63..230e99c 100644 --- a/runicnpc/API.md +++ b/runicnpc/API.md @@ -1,7 +1,10 @@ # RunicNPC — the API -**API version 4** (RunicNPC stage 5, 2026-10-01). 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 and D253–D272. **API 4 added** the faction +**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** @@ -16,7 +19,7 @@ what Oxide's `Call` reaches by name (PLAN.md §1.2). [PluginReference] private Plugin RunicNPC; int api = RunicNPC?.Call("RunicNpc_ApiVersion") ?? 0; -if (api < 4) { /* too old for this caller: say so */ } +if (api < 5) { /* too old for this caller: say so */ } BasePlayer npc = RunicNPC.Call("RunicNpc_Spawn", position, "warden", "plugin:MyPlugin", null) as BasePlayer; ``` @@ -46,7 +49,7 @@ Every NPC has an owner, and the owner decides its lifetime (PLAN.md §2). ### `RunicNpc_ApiVersion()` → `int` -The API version: `4`. +The API version: `5`. ### `RunicNpc_Spawn(Vector3 at, string profile, string owner, JObject overrides)` → `BasePlayer` @@ -66,7 +69,9 @@ 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. +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` @@ -96,6 +101,8 @@ The live NPCs of `owner`; null lists them all. Each has: | `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` @@ -231,6 +238,10 @@ All are called on every plugin, and **none of them answers**: a return value is | `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 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. | --- @@ -259,7 +270,29 @@ All are called on every plugin, and **none of them answers**: a return value is "turrets": "default", "hurtByPlayers": true, "hurtsPlayers": true, - "kitUse": { "heal": false, "grenades": false, "melee": false, "rockets": false, "flamethrower": false } + "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": "" } } ``` @@ -268,8 +301,8 @@ All are called on every plugin, and **none of them answers**: a return value is | `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` or `guard` | 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. | -| `movement.mode` | `wander`, `monument` or `route:` | D233. A placement may override it. | +| `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:`, 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. | @@ -286,9 +319,25 @@ All are called on every plugin, and **none of them answers**: a return value is | `relations` | API 4. keys: a faction, `scientists`, `animals` or `profile:`; 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 | 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. | +| `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 @@ -332,6 +381,14 @@ route is walked back and forth. An NPC joins the route at its nearest point. - `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). @@ -344,3 +401,7 @@ route is walked back and forth. An NPC joins the route at its nearest point. standalone server; refused while a site manages it. `/rnpc follow ` 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=` holds a placement inside a zone. +- `rnpc.profile set boss on|none` and `rnpc.profile set 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). diff --git a/runicnpc/PLAN.md b/runicnpc/PLAN.md index 485a58e..b54febb 100644 --- a/runicnpc/PLAN.md +++ b/runicnpc/PLAN.md @@ -11,7 +11,7 @@ design answered 2026-09-30 and 2026-10-01** (D253–D266, §0), **its spike meas and **its build's own questions answered 2026-10-01** (D267–D272). **Stage 5 built and tested 2026-10-01, walked on the site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 4. **Stage 6's design answered 2026-10-05** (D273–D282, §0), **its spike measured 2026-10-05** on both rigs, and **its two questions answered the -same day** (D283, D284, §9). **Its build's own questions were answered the same day too** (D285–D289). +same day** (D283, D284, §9). **Its build's own questions were answered the same day too** (D285–D289), and **stage 6 was built and walked on the site 2026-10-05** on both rigs (§9); its API is [API.md](API.md) version 5. RunicNPC is Runic Gateway's own NPC plugin for Rust servers, in its own repository, [`RunicGateway/runicnpc-rust`](https://gitea.whitlocktech.com/RunicGateway/runicnpc-rust). It runs on Oxide and @@ -1236,8 +1236,10 @@ around it, each point snapped to the navmesh. What this adds to the build (D283, D284): - **The kits are named `rnpc_raider`, `rnpc_campguard`, `rnpc_sniper` and `rnpc_juggernaut`.** -- **Each carries `RequiredPermission: runicnpc.examplekits`**, which nobody is granted, so no player can claim one - by typing its name (`IsHidden` alone does not stop that). RunicNPC's `GiveKit` call ignores the permission. +- **Each carries `RequiredPermission: kits.runicnpc`**, which nobody is granted, so no player can claim one + by typing its name (`IsHidden` alone does not stop that). RunicNPC's `GiveKit` call ignores the permission. It + was `runicnpc.examplekits` until the build found Oxide warning on every Kits load that a permission Kits + registers must start with `kits.`; the org lead chose Kits' prefix (2026-10-05). - **The first install runs in this order:** write the kits, set the flag, reload Kits. - Because of D283, the reload takes RunicNPC down and back once. - On that second load the flag is already set, so nothing is written twice and nothing loops. @@ -1259,6 +1261,64 @@ What this adds to the build (D283, D284): 4. **How a passive NPC moves. → D288:** as its profile says. 5. **The example profiles. → D289:** the set in §0. +**Built (2026-10-05), three branches and this one.** RunicNPC API 5 (`runicnpc-rust` `feat/stage-6`), the bridge +(`Rust-Plugins` `feat/runicnpc-stage6`) and the site (`Module-Rust` `feat/runicnpc-stage6`). The API is +[API.md](API.md) version 5; the wire is [`PROTOCOL.md`](../rust-link/PROTOCOL.md) §19.14. + +| Piece | What it does | +|---|---| +| RunicNPC API 5 | The boss box (bar, phases, lines and adds; the hooks `OnRunicNpcBossSpawned`, `OnRunicNpcBossPhase`, `OnRunicNpcBossDied` and `OnRunicNpcUsed`); the passive role with `stand` and press E through `OnPlayerInput`; the four example profiles and kits, written once (`data/RunicNPC/state.json`); `role` and `boss` in `RunicNpc_List`. The swap's field list regenerated for Rust 2634.289.1 | +| The bridge | `npc.boss.spawned`, `npc.boss.phase`, `npc.boss.died`; `runicnpc_api = 5`. A profile's `boss` and `use` travel in the push unchanged | +| The site | The profile form's boss box (phases with damage, aim, speed, ranges, a kit swap, adds and a line) and the passive role with its press-E answer; the checks in RunicNPC's words, a phase's kit checked on each server, its adds a site profile that is not a boss; the public triggers `rust.boss.spawned`, `rust.boss.phase`, `rust.boss.killed`; the feed's boss rows (a spawn public, a death behind the presence setting, a phase staff's) | + +**The builds it was tested on (checked against the latest, 2026-10-05):** Rust 2634.289.1 (buildid 25681086, +the latest public build), Oxide 2.0.7801 and Carbon 2.0.262 (both the latest releases). This morning's spike ran on +the previous Rust build, before either rig restarted. + +**Tested.** The harness's `s6` group (42 checks) passes on both rigs: refusals, passive (never fights, unhurtable +by default, press E once a second, a window, wandering), the boss (three phases, its own copy of the profile, the +death hook's contributors, adds that fight on, `RunicNpc_Despawn` of the dead boss taking its adds) and the +examples. `rnt.run all`: 244/246 on Oxide and 243/246 on Carbon before two checks were fixed (below); each fixed +check and the escort check then passed on both rigs. Module-Rust: 491 server and 67 client tests, and every +`check:*` script. + +**Walked, 2026-10-05, through a walk site, signed in to the admin panel in the browser.** No player was on the rigs, +so a probe plugin hurt bosses as a stand-in player. + +| Row | Oxide | Carbon | +|---|---|---| +| A boss profile made on the form (two phases: 50% two adds and a line; 25% damage ×1.5, speed ×1.3 and a line), saved and pushed; the rig holds the block as typed | pass | pass (made through the same API) | +| Placed by clicking the live map; `npc.boss.spawned` stored, `rust.boss.spawned` fired | pass | — | +| Phase 1 summoned two adds, owned as the boss is; `rust.boss.phase` fired for each phase | pass | pass (and the 25% kit swap: it then held `rnpc_raider`) | +| The kill: `rust.boss.killed` fired; the adds fought on (D287) | pass | pass | +| The public events route: signed out it shows the spawn only; signed in as an admin, the death too (its killer, 1 damager); never a phase | pass | — | +| An event placing the boss: its phase gate advanced on `rust.boss.killed` where `byEvent`, and the run's teardown removed the boss's adds | pass after a fix (below) | pass | +| A passive profile made on the form: choosing passive showed Press E, hid Boss, unticked "Players can hurt it" and set `stand`; pushed as typed | pass | — | +| Placed from the map; 500 hits from a player left it at 150/150; it stood and took no target | pass | — | + +**Not walked:** everything a real client shows, waiting for the in-game walk with a player: the bar on screen and +its redraws, the lines in chat or a popup, a real E press and the window, and the example profiles on a fresh +server's own boot. + +**What building it found:** + +- **Rust's design picks targets that `IsTarget` refuses.** `HumanNPC.GetBestTarget` ranks every player in + `Senses.Players`, which `IsTarget` does not filter, and fires through `AttackTick`. A passive NPC shot at players + until RunicNPC answered both (IAIAttack, which it already re-implements) with nothing for a passive one. **Stage 5's + ally sparing may have the same hole**: it was not part of this stage and is not changed here. +- **A boss's adds outlived the event that placed it.** Teardown removes what a step placed by net id; the adds + RunicNPC spawned are in no ledger. RunicNPC now keeps each boss's adds by the boss's net id, after it dies too, + and `RunicNpc_Despawn` of that id removes them. Walked again: run 65's cleanup removed both. +- **Oxide warns on a permission Kits registers without its prefix.** The example kits' permission is `kits.runicnpc` + (the org lead's choice), not `runicnpc.examplekits`. +- **The Rust update's navmesh rebuild moved two checks' ground.** `sentry.shoots` now picks a spot with a clear line + (a rock was between the sentry and its stand-in; with a clear line it hit 28 times). `move.monument.roams`: the + military tunnel's own 30 scientists no longer move with no real player near, so ours standing too is the same + behaviour, and the check now says both. +- **The bridge's `npc.health` frame carries no owner** (stage 4's `OnRunicNpcHealth` passes null), so + `rust.npc.health` never says `byEvent`, and a gate on "the event's boss below 50%" cannot be written. Not changed + here; the boss phase trigger does carry it. + ### Stage 7 — Loot Loot tables authored on the site, the corpse's default loot cleared or kept, a crate on death, corpse removal. diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 39b4627..08b5b4e 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -2541,3 +2541,44 @@ placements), a new one by clicking the live map (D245, D246). It adds the trigge and titles (D247, D250, D252). **Walked on both rigs, 2026-09-30** (runicnpc PLAN.md stage 4). + +### 19.14 RunicNPC stage 6: bosses (runicnpc PLAN.md stage 6) + +Stage 6's wire changes **join protocol 13** while it is unreleased (D282); no message changes shape, and +`PROTOCOL_VERSION` stays 13. The bridge now needs **RunicNPC API 5**: `RunicNpcApiNeeded = 5`, +**`runicnpc_api = 5`** in `overlay.toml`. An older RunicNPC answers `npc.error` `runicnpc-old` as before. + +**A profile's `boss` and `use` blocks** travel in `npc.profiles` and `npc.profiles.set` as RunicNPC holds them +(runicnpc `API.md`); the bridge reads neither. + +**Three events,** from RunicNPC API 5's boss hooks, for the site's public boss triggers (D277): + +```json +{"kind":"npc.boss.spawned","type":"event","netId":"981204","profile":"juggernaut","name":"Juggernaut", + "owner":"run:42","runId":"42","x":-2123.7,"z":2282.7,"maxHealth":2500} +{"kind":"npc.boss.phase","type":"event","netId":"981204","profile":"juggernaut","name":"Juggernaut", + "owner":"run:42","runId":"42","x":-2120.1,"z":2290.4,"phase":2,"at":0.25,"health":611.3,"maxHealth":2500} +{"kind":"npc.boss.died","type":"event","netId":"981204","profile":"juggernaut","name":"Juggernaut", + "owner":"run:42","runId":"42","x":-2118.0,"z":2291.2,"killerId":"76561198000000002","killerName":"Marisol", + "damagers":5} +``` + +- The NPC fields are `npc.died`'s (§19.12): `owner`, and `runId` or `placement` from it. +- `phase` counts from 1; `at` is the phase's health fraction. A hit that crosses two thresholds sends both, + highest first. +- `npc.boss.died` is sent **beside** `npc.died`, which still carries every contributor. It names no Steam id + but the killer's, and counts the players who hurt the boss in `damagers`. `killerId` and `killerName` are + absent when no player landed the killing blow. + +| `kind` | Hook | `class` | Why | +|---|---|---|---| +| `npc.boss.spawned` | `OnRunicNpcBossSpawned` | public | The game says a boss's spawn to everyone on the server (D286). It names nobody | +| `npc.boss.died` | `OnRunicNpcBossDied` | **presence** | Said to everyone too, but it names the killer, which says that player was on | +| `npc.boss.phase` | `OnRunicNpcBossPhase` | **staff** | The game tells a phase only to the players near the boss (D286) | + +**Hooks.** `OnRunicNpcBossSpawned`, `OnRunicNpcBossPhase` and `OnRunicNpcBossDied` join `rg.hooks`' list. + +**The website** (Module-Rust) adds the public triggers `rust.boss.spawned`, `rust.boss.phase` and +`rust.boss.killed` (ceiling `everyone`, default `subscribers`) for app, in-app and email rules; Discord hears +a boss only through an event's announce step (D277). The feed shows a boss appearing, and its death where the +server's presence setting allows. The profile form gains the boss box and the passive role.