From 90f403422a5465de91c0892038a147e3cdbb4688 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 5 Oct 2026 10:48:28 -0500 Subject: [PATCH] feat(bridge): boss frames for the site's public triggers (runicnpc stage 6) npc.boss.spawned, npc.boss.phase and npc.boss.died, from RunicNPC API 5's boss hooks, for the site's spawned, phase and killed triggers (D277). The death frame names the killer and counts the players who hurt the boss; npc.died still carries every contributor. A profile's `boss` and `use` blocks travel in the push unchanged. runicnpc_api and RunicNpcApiNeeded are 5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- overlay.toml | 4 +- overlay/oxide/plugins/RunicGateway.cs | 67 +++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/overlay.toml b/overlay.toml index 07d111a..18c7490 100644 --- a/overlay.toml +++ b/overlay.toml @@ -80,7 +80,7 @@ requires_plugins = ["Kits", "ZoneManager"] # ── The RunicNPC API the bridge needs ─────────────────────────────────────── # -# RunicNPC (docs/runicnpc/PLAN.md stages 4 and 5): the bridge's npc.* commands call +# RunicNPC (docs/runicnpc/PLAN.md stages 4 to 6): the bridge's npc.* commands call # RunicNPC's API and need at least this version of it. It MUST equal # `RunicNpcApiNeeded` in RunicGateway.cs; `node scripts/checkPlugin.js` holds the # two equal. The release copies it into manifest.json, and the installer's @@ -88,4 +88,4 @@ requires_plugins = ["Kits", "ZoneManager"] # # RunicNPC is optional to the bridge until its stage 9: without it every npc.* # command answers `runicnpc-missing`, and events place Rust's own scientists. -runicnpc_api = 4 +runicnpc_api = 5 diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index 26ef994..870a237 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -249,7 +249,8 @@ namespace Oxide.Plugins "OnHealingItemUse", "OnPlayerRevive", "OnRocketLaunched", "OnExplosiveThrown", "OnExplosiveDropped", "OnMissionSucceeded", "OnEnterZone", "OnExitZone", "OnRgDomesReady", - "OnRunicNpcDied", "OnRunicNpcHealth", "OnRunicNpcPlacementChanged" + "OnRunicNpcDied", "OnRunicNpcHealth", "OnRunicNpcPlacementChanged", + "OnRunicNpcBossSpawned", "OnRunicNpcBossPhase", "OnRunicNpcBossDied" }; private void Init() @@ -10369,9 +10370,10 @@ namespace Oxide.Plugins /// /// The RunicNPC API these commands call: 3 added placements from a website (D249), 4 the - /// faction table and an event's escort, ally and tether (runicnpc stage 5). + /// faction table and an event's escort, ally and tether (runicnpc stage 5), 5 bosses and + /// passive NPCs (runicnpc stage 6): a profile's `boss` and `use`, and the boss hooks. /// - private const int RunicNpcApiNeeded = 4; + private const int RunicNpcApiNeeded = 5; private Plugin RunicNpcPlugin() { @@ -10824,6 +10826,65 @@ namespace Oxide.Plugins } /// The fields every NPC frame shares. owner null reads it from the NPC's placement, if any. + // ---- bosses (runicnpc stage 6: D277) ---- + // + // Three frames for the site's public boss triggers: spawned, a phase, killed. A boss's death + // also raises npc.died as every NPC's does; npc.boss.died adds the boss's own facts, and names + // no Steam id but the killer's, because the triggers it feeds are public. + + /// A boss spawned: an admin's placement, an event's step or another plugin. + private void OnRunicNpcBossSpawned(BasePlayer npc, string profile, string owner) + { + MarkHook("OnRunicNpcBossSpawned"); + + if (npc == null) + return; + + Dictionary frame = Frame("npc.boss.spawned", "event"); + PutNpc(frame, npc, profile, owner); + frame["maxHealth"] = (float)Math.Round(npc.MaxHealth(), 1); + EmitFrame(frame); + } + + /// A boss entered a phase: phase counts from 1, at is its health fraction. + private void OnRunicNpcBossPhase(BasePlayer npc, string profile, string owner, int phase, float at) + { + MarkHook("OnRunicNpcBossPhase"); + + if (npc == null) + return; + + Dictionary frame = Frame("npc.boss.phase", "event"); + PutNpc(frame, npc, profile, owner); + frame["phase"] = phase; + frame["at"] = at; + frame["health"] = (float)Math.Round(npc.health, 1); + frame["maxHealth"] = (float)Math.Round(npc.MaxHealth(), 1); + EmitFrame(frame); + } + + /// A boss died: the killing blow's player, and how many players hurt it. + private void OnRunicNpcBossDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary contributors) + { + MarkHook("OnRunicNpcBossDied"); + + if (npc == null) + return; + + Dictionary frame = Frame("npc.boss.died", "event"); + PutNpc(frame, npc, profile, owner); + BasePlayer killer = info != null ? info.InitiatorPlayer : null; + + if (killer != null && !killer.IsNpc) + { + frame["killerId"] = ((ulong)killer.userID).ToString(); + frame["killerName"] = killer.displayName; + } + + frame["damagers"] = contributors != null ? contributors.Count : 0; + EmitFrame(frame); + } + private void PutNpc(Dictionary frame, BasePlayer npc, string profile, string owner) { frame["netId"] = npc.net != null ? npc.net.ID.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;