feat(rust): NPC profile and placement routes, the event picker, triggers and titles (runicnpc stage 4)
Admin: /admin/rust/npcs for profiles (create, change, delete, restore a replaced one, push now) and each server's placements (list, add from a map point, change, remove, rename, respawn). Public: the profiles a leaderboard ranks by, one profile's ranking counted as the profile says (D247, D250), and one player's kills by profile (D252). Player: your own kills by profile. The Place NPCs step offers the site's profiles first, then Rust's own (D243). rust.npc.died and rust.npc.health are triggers a phase can wait on. A title rule can rank a profile's kills. Swagger fragment, engagement and route manifests regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -159,6 +159,14 @@ const HEADLINES = Object.freeze({
|
||||
title: `${d.player || d.steamId} was unbanned on ${d.server}`,
|
||||
intro: `The ban on ${d.player || d.steamId} (${d.steamId}) was lifted.`,
|
||||
}),
|
||||
'rust.npc.died': (d) => ({
|
||||
title: `${d.npc || d.profile} was killed on ${d.server}`,
|
||||
intro: `${d.killer ? `${d.killer} killed` : 'Something killed'} ${d.npc || 'an NPC'} (${d.profile}) on ${d.server}.`,
|
||||
}),
|
||||
'rust.npc.health': (d) => ({
|
||||
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.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.`,
|
||||
@@ -369,7 +377,49 @@ async function onBan(server, item, frame) {
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* RunicNPC's NPCs (runicnpc stage 4): a death, and a health threshold. What a
|
||||
* phase gate counts, so the window is short: a death an hour old is history, not
|
||||
* a wave falling. The dedupe key is the NPC's net id, which the game never
|
||||
* reuses within a boot.
|
||||
*/
|
||||
const NPC_MAX_AGE_MS = 15 * 60 * 1000
|
||||
|
||||
function npcVars(server, frame) {
|
||||
return {
|
||||
...serverVars(server),
|
||||
profile: str(frame.profile),
|
||||
npc: str(frame.name),
|
||||
byEvent: Boolean(frame.runId),
|
||||
runId: str(frame.runId),
|
||||
placement: str(frame.placement),
|
||||
}
|
||||
}
|
||||
|
||||
async function onNpc(server, item, frame) {
|
||||
const t = frameTime(item, frame)
|
||||
if (!stillNews(t, NPC_MAX_AGE_MS) || !str(frame.profile)) return 0
|
||||
|
||||
if (frame.kind === 'npc.died') {
|
||||
const contributors = Array.isArray(frame.contributors) ? frame.contributors : []
|
||||
return fire(T['rust.npc.died'], {
|
||||
data: { ...npcVars(server, frame), killer: str(frame.killerName), killerSteamId: str(frame.killerId), contributors: contributors.length },
|
||||
dedupeKey: dedupeKey('npc.died', server.id, frame.netId, t),
|
||||
occurredAt: t,
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
const percent = Math.round((Number(frame.threshold) || 0) * 100)
|
||||
return fire(T['rust.npc.health'], {
|
||||
data: { ...npcVars(server, frame), percent },
|
||||
dedupeKey: dedupeKey('npc.health', server.id, frame.netId, percent),
|
||||
occurredAt: t,
|
||||
}) ? 1 : 0
|
||||
}
|
||||
|
||||
const HANDLERS = Object.freeze({
|
||||
'npc.died': onNpc,
|
||||
'npc.health': onNpc,
|
||||
'entity.destroyed': onRaid,
|
||||
'server.wipe': onWipe,
|
||||
'clan.member.left': onClan,
|
||||
|
||||
Reference in New Issue
Block a user