Files
Module-Rust/client/src/lib/mapEvents.js
wtclaude 481837e937
All checks were successful
PR Checks / server-tests (pull_request) Successful in 37s
PR Checks / client-build (pull_request) Successful in 24s
PR Checks / frozen-manifest (pull_request) Successful in 49s
feat(rust): RaidableBases — feed, raids map layer, Warlord, Spawn a raid base, profiles (PLAN_REDESIGNS §11.4)
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
2026-10-16 15:18:52 -05:00

86 lines
3.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── 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(' · ')
}