feat(rust): the live map draws monuments by label, minor labels hidden (PLAN_REDESIGNS §4, D196)
A switch per monument label, a fleet default and a per-server override, on Admin -> Rust visibility's live map card. Substations, caves, train tunnels, wells and the other minor labels start hidden; every other label is drawn, so a monument a game update adds appears. GET /map leaves hidden labels out of the answer, so the website and the app both lose them. No schema change: rows are map.marker.<label key> in rust_settings and rust_map_overrides. No wire change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -31,11 +31,18 @@
|
||||
// (D118). Beside each server is what its map picture is, and the two buttons
|
||||
// that act at once rather than on Save: Fetch again, and Render now — shown only
|
||||
// where there is no picture, with how long it will stall that server (D109).
|
||||
//
|
||||
// PLAN_REDESIGNS §4 adds which monuments the map draws (D196): a switch per
|
||||
// monument label, because that is what staff read and it groups the variants
|
||||
// (31 substations of four prefabs are one "Substation"). The minor labels start
|
||||
// hidden and every other label drawn, so a monument Facepunch adds appears. One
|
||||
// fleet list, and each server's own labels with an override each.
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { ErrorState, Loading, useAsync } from '../../core.js'
|
||||
import api from '../../api.js'
|
||||
import { fleetLabels, fleetShows, markerDiff, serverShows, setFleet as setFleetMarker, setServer as setServerMarker } from '../../lib/mapMarkers.js'
|
||||
|
||||
const INHERIT = ''
|
||||
|
||||
@@ -117,7 +124,7 @@ export default function Visibility() {
|
||||
setNews(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, Boolean(s.on)])))
|
||||
setDelivery(Object.fromEntries(((state.news && state.news.servers) || []).map((s) => [s.id, s.delivery || 'chat'])))
|
||||
if (state.map) {
|
||||
setMapFleet({ ...state.map.fleet })
|
||||
setMapFleet({ ...state.map.fleet, markers: { ...(state.map.fleet.markers || {}) } })
|
||||
setMapServers(Object.fromEntries(state.map.servers.map((s) => [s.id, mapOverridesToForm(s.overrides)])))
|
||||
}
|
||||
}, [])
|
||||
@@ -386,6 +393,7 @@ function mapOverridesToForm(overrides) {
|
||||
const form = {}
|
||||
for (const l of MAP_LAYERS) form[l.id] = overrides[l.id] || INHERIT
|
||||
form.mates = overrides.mates === null || overrides.mates === undefined ? INHERIT : overrides.mates ? 'on' : 'off'
|
||||
form.markers = { ...(overrides.markers || {}) }
|
||||
return form
|
||||
}
|
||||
|
||||
@@ -396,6 +404,8 @@ function mapDiff(card, fleet, servers) {
|
||||
for (const key of [...MAP_LAYERS.map((l) => l.id), 'mates']) {
|
||||
if (key in fleet && fleet[key] !== card.fleet[key]) fleetChanges[key] = fleet[key]
|
||||
}
|
||||
const fleetMarkers = markerDiff(card.fleet.markers, fleet.markers)
|
||||
if (fleetMarkers) fleetChanges.markers = fleetMarkers
|
||||
if (Object.keys(fleetChanges).length) out.fleet = fleetChanges
|
||||
|
||||
const serverChanges = {}
|
||||
@@ -403,8 +413,10 @@ function mapDiff(card, fleet, servers) {
|
||||
const before = mapOverridesToForm(s.overrides)
|
||||
const now = servers[s.id] || before
|
||||
const changed = {}
|
||||
const markers = markerDiff(before.markers, now.markers)
|
||||
if (markers) changed.markers = markers
|
||||
for (const key of Object.keys(before)) {
|
||||
if (now[key] === before[key]) continue
|
||||
if (key === 'markers' || now[key] === before[key]) continue
|
||||
if (key === 'mates') changed.mates = now.mates === INHERIT ? null : now.mates === 'on'
|
||||
else changed[key] = now[key] === INHERIT ? null : now[key]
|
||||
}
|
||||
@@ -477,6 +489,8 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
|
||||
</FleetRow>
|
||||
</div>
|
||||
|
||||
<FleetMarkers card={card} fleet={fleet} setFleet={setFleet} />
|
||||
|
||||
{card.servers.length === 0 && (
|
||||
<p className="sans dim" style={{ fontSize: '0.82rem', margin: '12px 0 0' }}>No servers are configured yet.</p>
|
||||
)}
|
||||
@@ -517,6 +531,13 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<ServerMarkers
|
||||
server={s}
|
||||
minorLabels={card.minorLabels || []}
|
||||
fleetMarkers={fleet.markers}
|
||||
markers={form.markers}
|
||||
onChange={(next) => set('markers', next)}
|
||||
/>
|
||||
{capped && (
|
||||
<p className="dim" style={{ fontSize: '0.76rem', margin: '6px 0 0' }}>
|
||||
Players will be shown to {(LABEL[presence] || presence).toLowerCase()} on this server, because that is who may see who is online.
|
||||
@@ -549,6 +570,84 @@ function MapCard({ card, audiences, presenceOf, fleet, setFleet, servers, setSer
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The fleet's monument labels (D196): every label on any server's current map,
|
||||
* and the minor list, each a checkbox. Ticked is drawn.
|
||||
*/
|
||||
function FleetMarkers({ card, fleet, setFleet }) {
|
||||
const minor = card.minorLabels || []
|
||||
const labels = fleetLabels(minor, card.servers)
|
||||
const hidden = labels.filter((l) => !fleetShows(minor, fleet.markers, l.key)).length
|
||||
|
||||
return (
|
||||
<details className="sans" style={{ marginTop: 12, fontSize: '0.84rem' }}>
|
||||
<summary style={{ cursor: 'pointer', color: 'var(--head)' }}>
|
||||
Monuments on the map <span className="dim" style={{ fontSize: '0.76rem' }}>· {labels.length} labels, {hidden} hidden by default</span>
|
||||
</summary>
|
||||
<p className="dim" style={{ fontSize: '0.76rem', margin: '6px 0 8px' }}>
|
||||
Ticked labels are drawn on every server that has not chosen for itself. The minor ones — substations, caves, train
|
||||
tunnels, wells and the like — start hidden. Any label not listed here, such as a monument a game update adds, is drawn.
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(210px, 1fr))', gap: '4px 12px' }}>
|
||||
{labels.map((l) => (
|
||||
<label key={l.key} style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fleetShows(minor, fleet.markers, l.key)}
|
||||
onChange={(e) => setFleet((f) => ({ ...f, markers: setFleetMarker(minor, f.markers, l.key, e.target.checked) }))}
|
||||
aria-label={`Draw ${l.label} by default`}
|
||||
/>
|
||||
<span>{l.label}</span>
|
||||
{l.count > 0 && <span className="dim" style={{ fontSize: '0.72rem' }}>{l.count}</span>}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One server's monument labels (D196): the labels on its current map, each
|
||||
* following the fleet or drawn or hidden on this server alone.
|
||||
*/
|
||||
function ServerMarkers({ server, minorLabels, fleetMarkers, markers, onChange }) {
|
||||
const labels = server.markerLabels || []
|
||||
if (!labels.length) return null
|
||||
const hidden = labels.filter((l) => !serverShows(minorLabels, fleetMarkers, markers, l.key)).length
|
||||
const own = Object.keys(markers || {}).length
|
||||
|
||||
return (
|
||||
<details style={{ marginTop: 8, fontSize: '0.8rem' }}>
|
||||
<summary style={{ cursor: 'pointer' }} className="dim">
|
||||
Monuments: {labels.length} labels on this map, {hidden} hidden{own ? `, ${own} set for this server` : ''}
|
||||
</summary>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '4px 12px', marginTop: 6 }}>
|
||||
{labels.map((l) => {
|
||||
const mine = markers && Object.prototype.hasOwnProperty.call(markers, l.key) ? (markers[l.key] ? 'on' : 'off') : INHERIT
|
||||
const fleetOn = fleetShows(minorLabels, fleetMarkers, l.key)
|
||||
return (
|
||||
<label key={l.key} style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'space-between' }}>
|
||||
<span>
|
||||
{l.label} <span className="dim" style={{ fontSize: '0.72rem' }}>{l.count}</span>
|
||||
</span>
|
||||
<select
|
||||
value={mine}
|
||||
onChange={(e) => onChange(setServerMarker(markers, l.key, e.target.value === INHERIT ? null : e.target.value === 'on'))}
|
||||
style={{ ...selectStyle, fontSize: '0.78rem', padding: '2px 6px' }}
|
||||
aria-label={`${l.label} on ${server.name}`}
|
||||
>
|
||||
<option value={INHERIT}>{`Default (${fleetOn ? 'drawn' : 'hidden'})`}</option>
|
||||
<option value="on">Drawn</option>
|
||||
<option value="off">Hidden</option>
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
function FleetRow({ label, hint, children }) {
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user