feat(bridge): boss frames for the site's public triggers (runicnpc stage 6)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 8s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-05 10:48:28 -05:00
parent 9a3ae47850
commit 90f403422a
2 changed files with 66 additions and 5 deletions

View File

@@ -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

View File

@@ -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
/// <summary>
/// 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.
/// </summary>
private const int RunicNpcApiNeeded = 4;
private const int RunicNpcApiNeeded = 5;
private Plugin RunicNpcPlugin()
{
@@ -10824,6 +10826,65 @@ namespace Oxide.Plugins
}
/// <summary>The fields every NPC frame shares. <c>owner</c> null reads it from the NPC's placement, if any.</summary>
// ---- 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.
/// <summary>A boss spawned: an admin's placement, an event's step or another plugin.</summary>
private void OnRunicNpcBossSpawned(BasePlayer npc, string profile, string owner)
{
MarkHook("OnRunicNpcBossSpawned");
if (npc == null)
return;
Dictionary<string, object> frame = Frame("npc.boss.spawned", "event");
PutNpc(frame, npc, profile, owner);
frame["maxHealth"] = (float)Math.Round(npc.MaxHealth(), 1);
EmitFrame(frame);
}
/// <summary>A boss entered a phase: <c>phase</c> counts from 1, <c>at</c> is its health fraction.</summary>
private void OnRunicNpcBossPhase(BasePlayer npc, string profile, string owner, int phase, float at)
{
MarkHook("OnRunicNpcBossPhase");
if (npc == null)
return;
Dictionary<string, object> 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);
}
/// <summary>A boss died: the killing blow's player, and how many players hurt it.</summary>
private void OnRunicNpcBossDied(BasePlayer npc, string profile, string owner, HitInfo info, Dictionary<ulong, float> contributors)
{
MarkHook("OnRunicNpcBossDied");
if (npc == null)
return;
Dictionary<string, object> 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<string, object> frame, BasePlayer npc, string profile, string owner)
{
frame["netId"] = npc.net != null ? npc.net.ID.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;