feat(rust): bosses and passive NPCs on the profile form, and public boss triggers (runicnpc stage 6)
The profile form's boss box (D273-D277): the bar's distance, where its lines are said, the spawn and death lines, and phases at health percentages, each with optional damage, aim, speed, ranges, a kit swap, adds of another profile and a line. The passive role (D278, D279, D288): press E answers with a chat line or a window, it moves as its profile says (now including `stand`), and it starts unhurtable. The site checks both as RunicNPC does, in its words; a phase's kit is checked on each server like the profile's own, and its adds must name a site profile that is not a boss. Public triggers rust.boss.spawned, rust.boss.phase and rust.boss.killed (D277) from the bridge's new frames. In the feed, a boss appearing is public; its death names the killer, so it sits behind the presence setting; its phases are staff's, as the game tells them only to players near it (D286). engagement-triggers.json regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -167,6 +167,19 @@ const HEADLINES = Object.freeze({
|
||||
title: `${d.npc || d.profile} is below ${d.percent}% on ${d.server}`,
|
||||
intro: `${d.npc || 'An NPC'} (${d.profile}) fell to ${d.percent}% of its health on ${d.server}.`,
|
||||
}),
|
||||
'rust.boss.spawned': (d) => ({
|
||||
title: `${d.boss} has appeared on ${d.server}`,
|
||||
intro: `${d.boss} is up on ${d.server}.`,
|
||||
}),
|
||||
'rust.boss.phase': (d) => ({
|
||||
title: `${d.boss} is down to ${d.percent}% on ${d.server}`,
|
||||
intro: `${d.boss} fell to ${d.percent}% of its health on ${d.server} and entered phase ${d.phase}.`,
|
||||
}),
|
||||
'rust.boss.killed': (d) => ({
|
||||
title: `${d.boss} was killed on ${d.server}`,
|
||||
intro: `${d.killer ? `${d.killer} landed the killing blow` : `${d.boss} fell`}`
|
||||
+ `${d.damagers > 1 ? `, with ${d.damagers} players fighting it` : ''}.`,
|
||||
}),
|
||||
'rust.login.denied': (d) => ({
|
||||
title: `A login to ${d.server} was not approved`,
|
||||
intro: `${d.player || 'Someone'} (${d.steamId}) tried to join ${d.server} and was not let in within a minute.`,
|
||||
@@ -417,7 +430,40 @@ async function onNpc(server, item, frame) {
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* A RunicNPC boss (runicnpc stage 6, D277): spawned, a phase, killed. Public
|
||||
* news, so a replay older than a broadcast's window tells nobody (D63). The
|
||||
* dedupe key is the boss's net id, with the phase for a phase.
|
||||
*/
|
||||
async function onBoss(server, item, frame) {
|
||||
const t = frameTime(item, frame)
|
||||
if (!stillNews(t, BROADCAST_MAX_AGE_MS) || !str(frame.profile)) return 0
|
||||
const base = { ...serverVars(server), profile: str(frame.profile), boss: str(frame.name) || str(frame.profile), byEvent: Boolean(frame.runId) }
|
||||
|
||||
if (frame.kind === 'npc.boss.spawned') {
|
||||
return fire(T['rust.boss.spawned'], { data: base, dedupeKey: dedupeKey('boss.spawned', server.id, frame.netId, t), occurredAt: t }) ? 1 : 0
|
||||
}
|
||||
|
||||
if (frame.kind === 'npc.boss.phase') {
|
||||
const phase = Number(frame.phase) || 0
|
||||
return fire(T['rust.boss.phase'], {
|
||||
data: { ...base, phaseKey: `${base.profile}:${phase}`, phase, percent: Math.round((Number(frame.at) || 0) * 100) },
|
||||
dedupeKey: dedupeKey('boss.phase', server.id, frame.netId, phase),
|
||||
occurredAt: t,
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
return fire(T['rust.boss.killed'], {
|
||||
data: { ...base, killer: str(frame.killerName), damagers: Number(frame.damagers) || 0 },
|
||||
dedupeKey: dedupeKey('boss.killed', server.id, frame.netId, t),
|
||||
occurredAt: t,
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
const HANDLERS = Object.freeze({
|
||||
'npc.boss.spawned': onBoss,
|
||||
'npc.boss.phase': onBoss,
|
||||
'npc.boss.died': onBoss,
|
||||
'npc.died': onNpc,
|
||||
'npc.health': onNpc,
|
||||
'entity.destroyed': onRaid,
|
||||
|
||||
@@ -472,7 +472,83 @@ const NPCS = [
|
||||
},
|
||||
]
|
||||
|
||||
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, REWARD, ...CLANS, ...MODERATION, ...NPCS])
|
||||
// ── A RunicNPC boss (docs/runicnpc/PLAN.md stage 6, D277) ───────────────────
|
||||
//
|
||||
// Public, unlike the two above: a boss rising, changing phase and falling is
|
||||
// server news, as a wipe is. `everyone` is the ceiling and `subscribers` the
|
||||
// default, as for the broadcasts. The game also says these lines in chat; these
|
||||
// are for the app, the in-app inbox and email. Discord hears a boss only through
|
||||
// an event's own announce step (D277), so core does not change.
|
||||
//
|
||||
// No Steam id is declared, the killer's included: a public notice names the
|
||||
// killer by their in-game name only.
|
||||
|
||||
const BOSS_VARS = [
|
||||
{ name: 'profile', type: 'string', required: true, example: 'juggernaut',
|
||||
description: 'The RunicNPC profile the boss was made from.' },
|
||||
{ name: 'boss', type: 'string', required: true, example: 'Juggernaut',
|
||||
description: 'The boss\'s own name, as players see it.' },
|
||||
{ name: 'byEvent', type: 'boolean', required: true, example: true,
|
||||
description: 'Whether an event placed it.' },
|
||||
]
|
||||
|
||||
const BOSSES = [
|
||||
{
|
||||
id: 'rust.boss.spawned',
|
||||
label: 'A boss appeared',
|
||||
description: 'A RunicNPC boss spawned on a server: an admin\'s placement, an event, or another plugin.',
|
||||
kind: 'event',
|
||||
// One per kind of boss per cooldown, however often it respawns.
|
||||
subjectKey: 'profile',
|
||||
audience: 'subscribers',
|
||||
ceiling: 'everyone',
|
||||
version: V1,
|
||||
variables: [...SERVER, ...HEADLINE, ...BOSS_VARS],
|
||||
},
|
||||
{
|
||||
id: 'rust.boss.phase',
|
||||
label: 'A boss changed phase',
|
||||
description: 'A RunicNPC boss fell to one of its phases\' health thresholds and changed: stronger, faster, or calling for help.',
|
||||
kind: 'event',
|
||||
// A profile's phase: phase 2 is not swallowed by phase 1's cooldown.
|
||||
subjectKey: 'phaseKey',
|
||||
audience: 'subscribers',
|
||||
ceiling: 'everyone',
|
||||
version: V1,
|
||||
variables: [
|
||||
...SERVER,
|
||||
...HEADLINE,
|
||||
...BOSS_VARS,
|
||||
{ name: 'phaseKey', type: 'string', required: true, example: 'juggernaut:2',
|
||||
description: 'The profile and the phase. The cooldown subject; not meant for display.' },
|
||||
{ name: 'phase', type: 'int', required: true, example: 2,
|
||||
description: 'Which phase it entered, counting from 1.' },
|
||||
{ name: 'percent', type: 'int', required: true, example: 25,
|
||||
description: 'The health it fell to, as a percentage.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'rust.boss.killed',
|
||||
label: 'A boss was killed',
|
||||
description: 'Players killed a RunicNPC boss on a server.',
|
||||
kind: 'event',
|
||||
subjectKey: 'profile',
|
||||
audience: 'subscribers',
|
||||
ceiling: 'everyone',
|
||||
version: V1,
|
||||
variables: [
|
||||
...SERVER,
|
||||
...HEADLINE,
|
||||
...BOSS_VARS,
|
||||
{ name: 'killer', type: 'string', required: false, example: 'Marisol',
|
||||
description: 'The player who landed the killing blow, by in-game name. Absent when no player did.' },
|
||||
{ name: 'damagers', type: 'int', required: true, example: 5,
|
||||
description: 'How many players hurt it, the killer included.' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const TRIGGERS = Object.freeze([RAID, ...BROADCASTS, ACCOUNT, REWARD, ...CLANS, ...MODERATION, ...NPCS, ...BOSSES])
|
||||
|
||||
const TRIGGER_IDS = Object.freeze(Object.fromEntries(TRIGGERS.map((t) => [t.id, t.id])))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user