feat(rust): event NPCs named on the map, and a Place NPCs step's progress line (runicnpc stage 8)
- 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
This commit is contained in:
@@ -21,6 +21,7 @@ import Empty from './Empty.jsx'
|
||||
import usePolled from '../hooks/usePolled.js'
|
||||
import { ago } from '../lib/format.js'
|
||||
import { boundsOf, countdown, fromLatLng, grid, gridLabel, toLatLng } from '../lib/mapGeometry.js'
|
||||
import { eventTip, isBoss, titlesFrom, unknownRuns } from '../lib/mapEvents.js'
|
||||
import api, { BASE } from '../api.js'
|
||||
|
||||
const STYLE_ID = 'rust-leaflet-css'
|
||||
@@ -45,6 +46,7 @@ const COLOURS = {
|
||||
supply: '#66bb6a',
|
||||
crate: '#ffee58',
|
||||
event: '#ce93d8',
|
||||
boss: '#d500f9',
|
||||
online: '#ffffff',
|
||||
sleeping: '#9e9e9e',
|
||||
tc: '#ff7043',
|
||||
@@ -60,6 +62,7 @@ const WORLD_NAMES = {
|
||||
bradley: 'Bradley APC',
|
||||
supply: 'Supply drop',
|
||||
crate: 'Locked crate',
|
||||
boss: 'Boss',
|
||||
}
|
||||
|
||||
const AUDIENCE_WORDS = { public: 'everyone', signed_in: 'signed-in players', staff: 'staff only' }
|
||||
@@ -104,6 +107,34 @@ export default function MapView({ serverId, online, onPick = null, pins = null }
|
||||
enabled: Boolean(geometry) && anyLive,
|
||||
})
|
||||
|
||||
// Event titles by run id, from core's public calendar (runicnpc D303): asked
|
||||
// when the events layer names a run this page does not know, and at most once
|
||||
// a minute, so a run that is not announced is not asked about on every poll.
|
||||
const [titles, setTitles] = useState({})
|
||||
const askedAt = useRef(0)
|
||||
const liveEvents = live.data && live.data.events
|
||||
useEffect(() => {
|
||||
if (!unknownRuns(liveEvents, titles).length || Date.now() - askedAt.current < 60_000) return undefined
|
||||
askedAt.current = Date.now()
|
||||
let active = true
|
||||
api.coreEvents
|
||||
.calendar()
|
||||
.then((calendar) => {
|
||||
if (!active) return
|
||||
// Every run asked about is filed, named or not, so it is not asked again.
|
||||
const known = titlesFrom(calendar)
|
||||
setTitles((prev) => {
|
||||
const next = { ...prev, ...known }
|
||||
for (const id of unknownRuns(liveEvents, prev)) if (!(id in next)) next[id] = null
|
||||
return next
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [liveEvents, titles])
|
||||
|
||||
const container = useRef(null)
|
||||
const mapRef = useRef(null)
|
||||
const groups = useRef(null)
|
||||
@@ -213,7 +244,10 @@ export default function MapView({ serverId, online, onPick = null, pins = null }
|
||||
let tip = WORLD_NAMES[w.kind] || w.kind
|
||||
if (w.kind === 'crate' && w.hackLeftSec != null) tip += ` — ${countdown(w.hackLeftSec)} left on the hack`
|
||||
if (w.kind === 'crate' && w.hacked) tip += ' — hacked'
|
||||
dot(L, at(w), COLOURS[w.kind] || COLOURS.crate, w.kind === 'cargo' ? 7 : 5).bindTooltip(escape(tip)).addTo(made.world)
|
||||
// A RunicNPC boss outside any event (D302): its name, drawn as big as a ship.
|
||||
if (w.kind === 'boss' && w.name) tip = `${w.name} (boss)`
|
||||
const size = w.kind === 'cargo' || isBoss(w) ? 7 : 5
|
||||
dot(L, at(w), COLOURS[w.kind] || COLOURS.crate, size, isBoss(w) ? 2 : 1).bindTooltip(escape(tip)).addTo(made.world)
|
||||
}
|
||||
|
||||
const px = (metres) => metres * ((geometry.width - 2 * geometry.oceanMargin) / geometry.worldSize)
|
||||
@@ -223,7 +257,10 @@ export default function MapView({ serverId, online, onPick = null, pins = null }
|
||||
.bindTooltip(escape(e.name ? `Event zone · ${e.name}` : 'Event zone'))
|
||||
.addTo(made.events)
|
||||
} else {
|
||||
dot(L, at(e), COLOURS.event, 5).bindTooltip(e.kind === 'npc' ? 'Event NPC' : 'Event crate').addTo(made.events)
|
||||
// A name and its event; a boss bigger, never its health (D303).
|
||||
dot(L, at(e), isBoss(e) ? COLOURS.boss : COLOURS.event, isBoss(e) ? 8 : 5, isBoss(e) ? 2 : 1)
|
||||
.bindTooltip(escape(eventTip(e, titles)))
|
||||
.addTo(made.events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +279,7 @@ export default function MapView({ serverId, online, onPick = null, pins = null }
|
||||
.bindTooltip(escape(m.self ? `You${m.online ? '' : ' (asleep, offline)'}` : m.name || 'Clan mate'))
|
||||
.addTo(made.mates)
|
||||
}
|
||||
}, [leaflet, geometry, live.data])
|
||||
}, [leaflet, geometry, live.data, titles])
|
||||
|
||||
// An admin page's own pins, above everything else.
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user