- rust.npc.place's progress() (MODULE_API 1.12.0, D304). It reports the
step's NPCs still standing, by its key, of how many it placed, with
the profile's label: "Bandit: 2 of 3 left". A boss step is its
health, "Walk Juggernaut: 59%", and a boss's adds are not counted.
The data comes from the map's live answer, so it costs the game no
extra ask. A reader who may not see the map's events layer gets no
line.
- The map (D302, D303):
- an NPC's tooltip is its name and its event's title, read from
core's public calendar, which lists only announced events;
- a boss is bigger and in its own colour, and an add says so;
- a boss outside any event is in the world layer;
- a boss's health is removed from what a viewer is sent.
- The world layer's visibility hint mentions those bosses.
coreApi stays ^1.11.0: an older core ignores the optional member, and
the action works without a progress line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
// ── How a Place NPCs step stands, for its event's page (runicnpc stage 8) ──
|
|
//
|
|
// MODULE_API 1.12.0: core asks an action's `progress()` while a run is live and
|
|
// renders the answer as text it does not interpret. D304 settles what this one
|
|
// says: a line per Place NPCs step — "Bandits: 3 of 8 left" — and a boss as its
|
|
// health, "The Juggernaut: 62%". A boss's adds are not counted.
|
|
//
|
|
// **Where it comes from.** The step's ledger rows say which server and how many
|
|
// were placed; the map's live answer (`mapLive`, one `map.live` per server per
|
|
// five seconds, whoever asks) says which of them still stand, by the step's key,
|
|
// and a boss's health. So a busy event page costs the game nothing the map does
|
|
// not already cost it.
|
|
//
|
|
// **The map's events layer decides who may read it.** A page must never tell a
|
|
// reader what the map would hide from them, so a reader who may not see the
|
|
// events layer on that server gets no line at all.
|
|
//
|
|
// Never throws: core drops a throw, but a quiet `null` is the honest answer when
|
|
// the game or a setting cannot be read.
|
|
|
|
const servers = require('./model/servers/servers.model')
|
|
const serversDb = require('./model/servers/servers.db')
|
|
const npcs = require('./model/npcs/npcs.model')
|
|
const mapModel = require('./model/map/map.model')
|
|
const mapLive = require('./mapLive')
|
|
const core = require('./core')
|
|
const log = core.logger('progress')
|
|
|
|
/** The label for Rust's own NPCs, by their allowlist key, set by `eventWorld`. */
|
|
let ownLabels = new Map()
|
|
|
|
/** Called once by eventWorld with its PLACEABLE list. */
|
|
function setOwnLabels(placeable) {
|
|
ownLabels = new Map(placeable.filter((p) => p.kind === 'npc').map((p) => [p.key, p.label]))
|
|
}
|
|
|
|
/** The server a step's NPCs are on, from its ledger rows. */
|
|
function serverOf(resources) {
|
|
for (const r of resources || []) {
|
|
if (r && r.payload && r.payload.serverId) return String(r.payload.serverId)
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* `progress()` for `rust.npc.place`: `{ label, left, of }`, `{ label, percent }`
|
|
* or null.
|
|
*/
|
|
async function npcProgress({ runId, idempotencyKey, params, resources, viewer }) {
|
|
try {
|
|
const of = (resources || []).length
|
|
if (of === 0) return null
|
|
|
|
const serverId = serverOf(resources)
|
|
if (!serverId) return null
|
|
|
|
const access = await mapModel.access({ user: viewer && viewer.userId != null ? { id: viewer.userId } : null }, serverId)
|
|
if (!access.layers.events.visible) return null
|
|
|
|
const row = await serversDb.getServer(serverId)
|
|
if (!row || !row.enabled) return null
|
|
|
|
const picked = String((params && params.prefab) || '')
|
|
const profileName = picked.startsWith(npcs.PROFILE_PREFIX) ? picked.slice(npcs.PROFILE_PREFIX.length) : null
|
|
const profile = profileName ? await npcs.profileOn(serverId, profileName) : null
|
|
const label = profile ? profile.label : ownLabels.get(picked) || picked
|
|
if (!label) return null
|
|
|
|
const live = await mapLive.live(servers.withToken(row))
|
|
if (!live.ok || !live.data) return null
|
|
|
|
const standing = (live.data.events || []).filter(
|
|
(e) => e && e.kind === 'npc' && String(e.runId) === String(runId) && e.key === idempotencyKey && !e.add,
|
|
)
|
|
|
|
// A boss is its health; a dead one, nothing left.
|
|
if (profile && profile.boss) {
|
|
const boss = standing.find((e) => e.boss)
|
|
const health = boss && typeof boss.health === 'number' ? boss.health : 0
|
|
return { label, percent: Math.round(Math.max(0, Math.min(1, health)) * 100) }
|
|
}
|
|
|
|
return { label, left: Math.min(standing.length, of), of }
|
|
} catch (err) {
|
|
log.warn('could not say how a Place NPCs step stands', { run: runId, error: err.message })
|
|
return null
|
|
}
|
|
}
|
|
|
|
module.exports = { npcProgress, setOwnLabels, serverOf }
|