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:
89
client/src/lib/mapMarkers.js
Normal file
89
client/src/lib/mapMarkers.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// ── Which monument labels a map draws (PLAN_REDESIGNS §4, D196) ───────────
|
||||
//
|
||||
// The admin card's half of the rule `model/map/map.model.js` applies on the
|
||||
// server: a switch per monument LABEL, the minor labels off unless somebody
|
||||
// turns them on, every other label on. A server's switch wins over the fleet's,
|
||||
// the fleet's over the built-in list.
|
||||
//
|
||||
// The form holds only the EXPLICIT switches, as the server does — `{ key: bool }`
|
||||
// — so a label nobody touched keeps following the default above it. Setting the
|
||||
// fleet back to what the built-in list says clears the row rather than storing a
|
||||
// copy of the default: the next change to the list then reaches that label.
|
||||
//
|
||||
// Pure, and free of React, so it is tested in Node.
|
||||
|
||||
/** A label as its switch's key: trimmed, single-spaced, lower case. The server's rule exactly. */
|
||||
export function markerKey(label) {
|
||||
return String(label == null ? '' : label).trim().replace(/\s+/g, ' ').toLowerCase()
|
||||
}
|
||||
|
||||
/** The built-in answer for one key: hidden for the minor labels, drawn for everything else. */
|
||||
export function builtIn(minorLabels, key) {
|
||||
return !(minorLabels || []).some((l) => markerKey(l) === key)
|
||||
}
|
||||
|
||||
const has = (o, k) => Boolean(o) && Object.prototype.hasOwnProperty.call(o, k)
|
||||
|
||||
/** Whether the fleet draws one key: its own switch, or the built-in list. */
|
||||
export function fleetShows(minorLabels, fleetMarkers, key) {
|
||||
return has(fleetMarkers, key) ? fleetMarkers[key] : builtIn(minorLabels, key)
|
||||
}
|
||||
|
||||
/** Whether one server draws one key: its switch, or the fleet's answer. */
|
||||
export function serverShows(minorLabels, fleetMarkers, serverMarkers, key) {
|
||||
return has(serverMarkers, key) ? serverMarkers[key] : fleetShows(minorLabels, fleetMarkers, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every label the fleet list offers, sorted by name: each server's current
|
||||
* labels and the minor list, each once. A minor label no map has yet is still
|
||||
* listed, so it can be decided before a wipe brings it.
|
||||
*/
|
||||
export function fleetLabels(minorLabels, servers) {
|
||||
const byKey = new Map()
|
||||
for (const s of servers || []) {
|
||||
for (const l of s.markerLabels || []) {
|
||||
const seen = byKey.get(l.key)
|
||||
if (seen) seen.count += l.count || 0
|
||||
else byKey.set(l.key, { key: l.key, label: l.label, count: l.count || 0 })
|
||||
}
|
||||
}
|
||||
for (const label of minorLabels || []) {
|
||||
const key = markerKey(label)
|
||||
if (!byKey.has(key)) byKey.set(key, { key, label, count: 0 })
|
||||
}
|
||||
return [...byKey.values()].sort((a, b) => a.label.localeCompare(b.label))
|
||||
}
|
||||
|
||||
/** The fleet form after ticking one key: an explicit switch, or none where it matches the built-in list. */
|
||||
export function setFleet(minorLabels, fleetMarkers, key, shown) {
|
||||
const next = { ...(fleetMarkers || {}) }
|
||||
if (shown === builtIn(minorLabels, key)) delete next[key]
|
||||
else next[key] = shown
|
||||
return next
|
||||
}
|
||||
|
||||
/** A server form after choosing one key: `null` follows the fleet, otherwise an explicit switch. */
|
||||
export function setServer(serverMarkers, key, shown) {
|
||||
const next = { ...(serverMarkers || {}) }
|
||||
if (shown === null) delete next[key]
|
||||
else next[key] = shown
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* What changed between two explicit maps, as the PUT takes it: `{ key: bool }`
|
||||
* for a switch set or changed, `{ key: null }` for one cleared. Null when
|
||||
* nothing changed.
|
||||
*/
|
||||
export function markerDiff(before, now) {
|
||||
const out = {}
|
||||
const keys = new Set([...Object.keys(before || {}), ...Object.keys(now || {})])
|
||||
for (const key of keys) {
|
||||
const was = has(before, key) ? before[key] : undefined
|
||||
const is = has(now, key) ? now[key] : undefined
|
||||
if (was === is) continue
|
||||
out[key] = is === undefined ? null : is
|
||||
}
|
||||
return Object.keys(out).length ? out : null
|
||||
}
|
||||
Reference in New Issue
Block a user