feedback3 WE: spectator seats on the server — up to 2 per co-op room, never players, watch like an eliminated player, no private view (community report #26)

This commit is contained in:
sganggs committed 2026-10-04 17:20:51 +08:00
1 parent bdb0765c55
commit 42766172d1
9 files changed
+628 -56

No files matched your search

+9 -1
View File
@@ -6,6 +6,13 @@ export const PROTOCOL_VERSION = 1;
export const APP_VERSION = '0.1.2';
export const MAX_SEATS = 4;
/**
* Spectator seats of a co-op room (community report #26, owner's decision 2026-10-04) — a remake feature: the official
* room has 1–4 players and no spectator seat (there only eliminated players and 联防 bystanders watch, research 09 §3.1).
* A spectator never counts as a player, may not act, and watches like an eliminated player (server/lobby.js spectate,
* server/match/Match.js addSpectator).
*/
export const MAX_SPECTATORS = 2;
export const ROOM_CODE_LEN = 4;
export const NAME_MAX_LEN = 12;
@@ -133,6 +140,7 @@ export const ERR = Object.freeze({
ALREADY: 'ALREADY',
TEMP_NOT_EMPTY: 'TEMP_NOT_EMPTY',
ELIMINATED: 'ELIMINATED',
SPECTATOR: 'SPECTATOR', // a spectator seat only watches (MAX_SPECTATORS)
INTERNAL: 'INTERNAL',
});
@@ -142,7 +150,7 @@ export const ERR_TEXT = {
WRONG_PHASE: '当前阶段无法进行该操作', NO_FUNDS: '资金不足', HAND_FULL: '整备区已满', BOARD_FULL: '已达到部署上限',
BAD_TILE: '无法部署在该位置', BAD_TARGET: '无效的目标', SOLD_OUT: '已售出', MAX_LEVEL: '调度中心已达最高等级',
NOT_YOUR_TURN: '尚未轮到你', ALREADY: '已完成该操作', TEMP_NOT_EMPTY: '临时整备区不为空', ELIMINATED: '你已被淘汰',
INTERNAL: '服务器内部错误',
SPECTATOR: '观战中无法进行该操作', INTERNAL: '服务器内部错误',
};
// ---- Emotes (交流, research 09 §4) -----------------------------------------------------------------------------
+5
View File
@@ -248,6 +248,11 @@ export const C2S = {
'room.start': {},
// operator loadout (DESIGN §16): stored per session/seat; accepted until the match leaves INFO_CHECK
'room.loadout': { entries: isLoadoutEntries },
// spectator seats (remake feature, community report #26; MAX_SPECTATORS): take one of a co-op room's spectator seats —
// in its lobby or while its match runs — never a player seat; the host frees one by playerId (the spectator gets
// room.closed { reason: 'kicked' }). room.leave / g.leave leave a spectator seat like a player seat.
'room.spectate': { code: (v) => isStr(v, ROOM_CODE_LEN + 2) && /^[A-Za-z0-9]+$/.test(v) },
'room.removeSpectator': { playerId: isId },
// match
'g.infoReady': {},