P14d: guest display names for players

- Players set a display name in the session panel ("Your name"); it persists
  (sessionStore.guestName) and is sent via a new `setName` message on join + edit.
- Server stores the name per connection and uses it in the roster + chat; the roster
  entry now also carries the player's character, so the GM sees "Alice · Lia".
- Realtime e2e: a player names themselves and the GM's roster/share update to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:28:32 +02:00
parent 4e13a5bdf4
commit d6eda054bf
8 changed files with 60 additions and 9 deletions
+17 -4
View File
@@ -54,7 +54,7 @@ function timingEqual(a: string, b: string): boolean {
export class RoomHub {
private rooms = new Map<string, Room>();
private byCode = new Map<string, string>();
private conns = new Map<Sender, { roomId: string; role: 'gm' | 'player'; playerId: string }>();
private conns = new Map<Sender, { roomId: string; role: 'gm' | 'player'; playerId: string; name?: string }>();
constructor(private now: () => number = () => Date.now()) {}
private mintJoinCode(): string {
@@ -175,20 +175,33 @@ export class RoomHub {
/** Tell everyone (GM + players) the current roster: the GM (if present) + players. */
private sendRoster(room: Room): void {
const players: { playerId: string; name: string }[] = [];
const players: { playerId: string; name: string; character?: string }[] = [];
if (room.gm) players.push({ playerId: 'gm', name: 'GM' });
const seen = new Set<string>();
for (const s of room.players) {
const conn = this.conns.get(s);
if (!conn || seen.has(conn.playerId)) continue;
seen.add(conn.playerId);
players.push({ playerId: conn.playerId, name: room.seats.get(conn.playerId)?.name ?? `Player ${conn.playerId.slice(0, 4)}` });
const seatName = room.seats.get(conn.playerId)?.name;
const name = conn.name ?? seatName ?? `Player ${conn.playerId.slice(0, 4)}`;
players.push({ playerId: conn.playerId, name, ...(conn.name && seatName ? { character: seatName } : {}) });
}
const msg = { t: 'roster', players } as const;
room.gm?.send(msg);
for (const p of room.players) p.send(msg);
}
/** A player sets their display name (roster + chat). */
setName(socket: Sender, name: string): void {
const conn = this.conns.get(socket);
const room = conn ? this.rooms.get(conn.roomId) : undefined;
if (!conn || conn.role !== 'player' || !room) return;
const trimmed = name.trim().slice(0, 40);
if (trimmed) conn.name = trimmed; else delete conn.name;
room.lastActivity = this.now();
this.sendRoster(room);
}
/** A seated player edits their own sheet; the diff is forwarded to the GM only. */
playerPatch(socket: Sender, characterId: string, diff: PartialCharacterDiff): void {
const conn = this.conns.get(socket);
@@ -237,7 +250,7 @@ export class RoomHub {
for (const p of room.players) p.send(msg);
}
} else {
const from = room.seats.get(conn.playerId)?.name ?? `Player ${conn.playerId.slice(0, 4)}`;
const from = conn.name ?? room.seats.get(conn.playerId)?.name ?? `Player ${conn.playerId.slice(0, 4)}`;
if (to) {
room.gm?.send({ t: 'chat', from, body, scope: 'whisper' });
} else {