PLAN_REDESIGNS §11.4 (D202-D208, D386-D391), protocol 14. Spec: docs rust-link/PROTOCOL.md §20.12. - raid.started, raid.completed, raid.purchased and raid.locked public, raid.ended staff's; raidableBases.js reads each row per viewer (raiders' and a buyer's names only for the presence audience, D376/D390; Steam ids, position and a run's key only for staff). Feed lines in the World filter. - The step rust.raidbase.spawn, "Spawn a raid base" (ledgered, budget rust.raids): difficulty, profile and base from option sources listing each server's own names (D386), a spot as a world call's location with a height adjustment or an exact height, and PvP/NPCs/lock as on/off/blank. A base the raids helper made is a world resource, so eventWorld's revert and reconcile serve it and world.expired marks it expired; one it did not make records nothing and the run log says it despawns on its own timer. Refused at verify for a server that said it cannot; a dry run with a spot returns the spot check as detail.warnings (MODULE_API 1.16.0, D391). rb-not-ready retries in ten minutes. - Warlord: raids_completed, title category raidscompleted, the leaderboard's Raid bases column (sort=raidsCompleted). - The map's raids layer (D114), public by default with its own audience; the run's key is dropped before any viewer gets it. - Server settings' line for RaidableBases; its profiles in Mod configuration under a virtual path (D205). - Swagger fragment, bundle and README. Tests: server 684/684 (raidableBases.test.js new, every name in it invented); client 99/99, build and externals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
86 lines
3.3 KiB
JavaScript
86 lines
3.3 KiB
JavaScript
// ── What an event marker says (runicnpc stage 8, D302, D303) ──────────────
|
||
//
|
||
// An NPC's marker says its name and its event; a boss is drawn bigger, and no
|
||
// health is shown (D303 — the server does not even send it). A boss placed
|
||
// outside any event is in the world layer as `kind: 'boss'` (D302).
|
||
//
|
||
// The event's title comes from core's public calendar, which publishes each
|
||
// run's id for this: a map row holds a run id and nothing else. The calendar
|
||
// lists announced events only, so an unannounced event is "an event" here and
|
||
// nothing more.
|
||
//
|
||
// Pure, and free of React, so it is tested in Node.
|
||
|
||
/** `{ [runId]: title }` from core's public calendar answer. */
|
||
export function titlesFrom(calendar) {
|
||
const out = {}
|
||
for (const e of (calendar && calendar.entries) || []) {
|
||
if (e && e.kind === 'run' && e.runId != null && e.title) out[String(e.runId)] = e.title
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** The run ids an answer's events layer names that `titles` does not know yet. */
|
||
export function unknownRuns(events, titles) {
|
||
const out = new Set()
|
||
for (const e of events || []) {
|
||
if (e && e.runId != null && !Object.prototype.hasOwnProperty.call(titles || {}, String(e.runId))) out.add(String(e.runId))
|
||
}
|
||
return [...out]
|
||
}
|
||
|
||
/** The tooltip for one events-layer row that is not a zone. */
|
||
export function eventTip(row, titles) {
|
||
const event = (titles && row && row.runId != null && titles[String(row.runId)]) || 'an event'
|
||
if (!row || row.kind !== 'npc') return `Event crate · ${event}`
|
||
const who = row.name || 'Event NPC'
|
||
const role = row.boss ? ' (boss)' : row.add ? " (a boss's add)" : ''
|
||
return `${who}${role} · ${event}`
|
||
}
|
||
|
||
/** Whether a row is drawn as a boss: bigger (D303). */
|
||
export function isBoss(row) {
|
||
return Boolean(row && (row.boss || row.kind === 'boss'))
|
||
}
|
||
|
||
// ── Dynamic PVP's zones (protocol 14, PLAN_REDESIGNS §14.3.3) ──────────────
|
||
|
||
/** Dynamic PVP's own events as the map names them; a monument's or an operator's is shown as written. */
|
||
export const PVP_NAMES = Object.freeze({
|
||
Helicopter: 'Patrol helicopter',
|
||
Bradley: 'Bradley APC',
|
||
Satellite: 'Satellite crash',
|
||
TimedSupply: 'Airdrop',
|
||
SupplySignal: 'Signalled airdrop',
|
||
CargoShip: 'Cargo ship',
|
||
HackableCrate: 'Locked crate',
|
||
ExcavatorIgnition: 'Giant Excavator',
|
||
GhostShip: 'Ghost ship',
|
||
DeepSeaIsland: 'Deep Sea island',
|
||
IslandCannon: 'Island cannon',
|
||
})
|
||
|
||
/** A Dynamic PVP ring's tooltip: "PvP zone · Patrol helicopter". */
|
||
export function pvpTip(z) {
|
||
const name = PVP_NAMES[z.event] || z.event
|
||
return name ? `PvP zone · ${name}` : 'PvP zone'
|
||
}
|
||
|
||
// ── RaidableBases' bases (protocol 14, PLAN_REDESIGNS §11.4) ───────────────
|
||
|
||
/**
|
||
* A raid base's tooltip: "Raid base · Hard · raidhard2 · PvP · despawns in 42 min",
|
||
* and "· this site's event" for one a run spawned. The difficulty and base names are
|
||
* the plugin's own (D386).
|
||
*/
|
||
export function raidTip(r, now = Date.now()) {
|
||
const parts = ['Raid base']
|
||
if (r.difficulty) parts.push(r.difficulty)
|
||
if (r.base) parts.push(r.base)
|
||
if (r.pvp === true) parts.push('PvP')
|
||
const left = Number(r.despawnAt) - now
|
||
if (Number.isFinite(left) && left > 0) parts.push(`despawns in ${Math.max(1, Math.round(left / 60000))} min`)
|
||
if (r.runId) parts.push('this site’s event')
|
||
return parts.join(' · ')
|
||
}
|