Add player position tracking relay + rendering (Phase 7b)
Relays the mod's throttled player_positions roster over a new /ws/players/:serverId gateway (Redis pub/sub + snapshot key so a tab connecting between mod flushes isn't empty), gated per-server by a new playerPositionsVisible admin toggle independent of the mod's own tracking config. Frontend renders the roster as map markers with a show/hide toggle and online count. Covered by unit tests (players.test.ts, ws-gateway.test.ts, admin.test.ts) and a new e2e spec that plays the real mod WS protocol from inside a browser context. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
This commit is contained in:
@@ -60,6 +60,7 @@ function adminpanel() {
|
||||
authMode: server.authMode,
|
||||
anonymousChatAllowed: server.anonymousChatAllowed,
|
||||
waypointFormat: server.waypointFormat,
|
||||
playerPositionsVisible: server.playerPositionsVisible,
|
||||
}),
|
||||
});
|
||||
server.status = res.ok ? "saved" : "save failed";
|
||||
|
||||
@@ -18,6 +18,7 @@ const DEFAULT_MARKER_HEIGHT = 60;
|
||||
// api/src/link.ts's doc comment for why that's a deliberate Phase 3 MVP tradeoff.
|
||||
const SESSION_STORAGE_KEY = "mcmapper_session";
|
||||
const NICKNAME_STORAGE_KEY = "mcmapper_nickname";
|
||||
const SHOW_PLAYERS_STORAGE_KEY = "mcmapper_show_players";
|
||||
|
||||
function mapmapper() {
|
||||
return {
|
||||
@@ -55,6 +56,16 @@ function mapmapper() {
|
||||
regionStatus: "",
|
||||
exporting: false,
|
||||
|
||||
// Phase 7b: live player positions, relayed from the mod via /ws/players/:serverId (see
|
||||
// players-gateway.ts). `onlinePlayers` always reflects the full current roster (not a diff,
|
||||
// same "current state" philosophy as markers/columns) — a player logging out just stops
|
||||
// appearing in the next message. `showPlayers` only toggles the map layer; the roster (and
|
||||
// the sidebar count) keeps updating either way.
|
||||
playersSocket: null,
|
||||
playerLayer: null,
|
||||
onlinePlayers: [],
|
||||
showPlayers: localStorage.getItem(SHOW_PLAYERS_STORAGE_KEY) !== "false",
|
||||
|
||||
async init() {
|
||||
const servers = await fetch("/api/servers").then((r) => r.json());
|
||||
this.loading = false;
|
||||
@@ -63,6 +74,7 @@ function mapmapper() {
|
||||
this.leaflet = L.map("map", { crs: L.CRS.Simple, minZoom: -4, maxZoom: 6 });
|
||||
this.leaflet.setView([0, 0], 0);
|
||||
this.markerLayer = L.layerGroup().addTo(this.leaflet);
|
||||
this.playerLayer = L.layerGroup().addTo(this.leaflet);
|
||||
this.leaflet.on("click", (e) => this.onMapClick(e));
|
||||
this.leaflet.on("mousedown", (e) => this.startRegionSelect(e));
|
||||
|
||||
@@ -87,6 +99,7 @@ function mapmapper() {
|
||||
await this.loadAccount();
|
||||
await this.loadMarkers();
|
||||
this.connectChat();
|
||||
this.connectPlayers();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -393,6 +406,37 @@ function mapmapper() {
|
||||
};
|
||||
},
|
||||
|
||||
connectPlayers() {
|
||||
const proto = location.protocol === "https:" ? "wss" : "ws";
|
||||
this.playersSocket = new WebSocket(`${proto}://${location.host}/ws/players/${this.server.id}`);
|
||||
this.playersSocket.onmessage = (ev) => {
|
||||
const { players } = JSON.parse(ev.data);
|
||||
this.onlinePlayers = players;
|
||||
this.renderPlayerLayer(players);
|
||||
};
|
||||
},
|
||||
|
||||
renderPlayerLayer(players) {
|
||||
this.playerLayer.clearLayers();
|
||||
if (!this.showPlayers) return;
|
||||
for (const player of players) {
|
||||
L.circleMarker(worldToLatLng(player.x, player.z), {
|
||||
radius: 5,
|
||||
color: "#facc15",
|
||||
fillColor: "#facc15",
|
||||
fillOpacity: 1,
|
||||
weight: 2,
|
||||
})
|
||||
.bindTooltip(player.username, { permanent: true, direction: "top", offset: [0, -6] })
|
||||
.addTo(this.playerLayer);
|
||||
}
|
||||
},
|
||||
|
||||
onShowPlayersChange() {
|
||||
localStorage.setItem(SHOW_PLAYERS_STORAGE_KEY, String(this.showPlayers));
|
||||
this.renderPlayerLayer(this.onlinePlayers);
|
||||
},
|
||||
|
||||
sendChat() {
|
||||
const message = this.chatInput.trim();
|
||||
if (!message || !this.chatSocket) return;
|
||||
|
||||
Reference in New Issue
Block a user