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:
2026-09-30 04:34:43 -05:00
parent 564a234890
commit fa16f0ad2e
20 changed files with 3224 additions and 24 deletions

View File

@@ -16,7 +16,9 @@ const events = require('../../model/events/events.model')
const map = require('../../model/map/map.model')
const mapDb = require('../../model/map/map.db')
const mapLive = require('../../mapLive')
const npcs = require('../../model/npcs/npcs.model')
const servers = require('../../model/servers/servers.model')
const serversDb = require('../../model/servers/servers.db')
const titles = require('../../model/titles/titles.model')
const visibility = require('../../model/visibility/visibility.model')
@@ -130,6 +132,51 @@ async function listLeaderboard(req, res) {
}
}
/** GET /servers/:id/npc-profiles — what the leaderboard's profile picker offers (D250). */
async function listNpcProfiles(req, res) {
try {
res.json({ profiles: await npcs.boardProfiles(req.params.id) })
} catch (err) {
log.error('failed to read the NPC profiles', { server: req.params.id, error: err.message })
res.status(500).json({ message: 'Failed to read the NPC profiles' })
}
}
/** GET /servers/:id/npc-leaderboard?profile= — one profile's kills, counted as it says (D247, D250). */
async function npcLeaderboard(req, res) {
const profileId = Number(req.query.profile)
if (!Number.isInteger(profileId) || profileId < 1) return res.status(400).json({ message: 'profile is a profile id' })
try {
const state = await serversDb.getState(req.params.id)
const wipeId = req.query.wipe ? String(req.query.wipe) : null
const currentWipe = Boolean(wipeId && state && state.wipeId === wipeId)
const [ranked, held] = await Promise.all([
npcs.ranking({ serverId: req.params.id, profileId, wipeId, currentWipe, limit: req.query.limit }),
titles.currentFor(req.params.id),
])
res.json({ profile: ranked.profile, leaderboard: ranked.rows.map((row) => ({ ...row, kills: row.value, titles: held.get(row.steamId) || [] })) })
} catch (err) {
if (err instanceof npcs.NpcError) return res.status(err.status).json({ message: err.message })
log.error('failed to read an NPC leaderboard', { server: req.params.id, error: err.message })
return res.status(500).json({ message: 'Failed to read the leaderboard' })
}
}
/** GET /servers/:id/players/:steamId/npc-kills — one leaderboard row, opened (D252). */
async function playerNpcKills(req, res) {
const steamId = String(req.params.steamId || '')
if (!/^\d{1,20}$/.test(steamId)) return res.status(400).json({ message: 'steamId is a Steam id' })
try {
const wipeId = req.query.wipe ? String(req.query.wipe) : null
res.json({ steamId, kills: await npcs.playerKills({ serverId: req.params.id, steamId, wipeId }) })
} catch (err) {
log.error('failed to read a player’s NPC kills', { server: req.params.id, error: err.message })
res.status(500).json({ message: 'Failed to read the kills' })
}
}
async function listWipes(req, res) {
try {
res.json({ wipes: await events.wipes(req.params.id) })
@@ -353,6 +400,9 @@ module.exports = {
getServer,
listEvents,
listLeaderboard,
listNpcProfiles,
npcLeaderboard,
playerNpcKills,
listWipes,
listOnline,
listClans,