feedback5 WG: 补位 on the server — room.ownership { notOwned } (lenient check, stored per session / seat, a match takes the list its seat had at the start), PlayerState.standIns: the player's not-owned NORMAL chess fight as their stand-ins (battleInput standIn: true in every field, the stand-in's position / range, no summons), m.private.standIns, stand-in art in prep scouting
This commit is contained in:
1 parent
7d20695adf
commit
24205581e1
17 files changed
+590
-24
No files matched your search
+34
-4
@@ -64,6 +64,13 @@
|
||||
// (or outside a room) it simply replaces the stored one; while the room's match runs it is also handed to
|
||||
// match.setLoadout(playerId, loadout), which accepts it only during INFO_CHECK (the 干员调配 entry of the briefing)
|
||||
// and refuses it afterwards (WRONG_PHASE: the match's loadout is locked, the stored one applies to the next match).
|
||||
// * Operator ownership (干员持有, 0.2.0 补位, owner's decision 2026-10-05): room.ownership { notOwned } — the base chess
|
||||
// ids the player marked as not owned — is checked leniently (shared/protocol.js checkNotOwned: anything that is not
|
||||
// a droppable NORMAL chess is dropped, never the whole list; only a malformed list is BAD_MSG) and stored on the
|
||||
// session and the seat like the loadout. The match receives seats[].notOwned when it starts (bots: none — they own
|
||||
// every operator) and keeps it for its whole length: the setting is out of match ("局外设置,下一局生效"), so while
|
||||
// the room's match runs a new list is only stored for the next match (ROOM_STARTED 'stored for the next match',
|
||||
// never handed to the match). A spectator's list stays on its session.
|
||||
// * Spectator seats (community report #26, owner's decision 2026-10-04 — a remake feature, the official room has none):
|
||||
// room.spectate { code } takes one of a co-op room's MAX_SPECTATORS (2) spectator seats, in its lobby or while its
|
||||
// match runs (▸ solo rooms: ROOM_FULL). A spectator is not a player: never in `seats`, never counted for the 1–4 players
|
||||
@@ -71,15 +78,15 @@
|
||||
// {empty} for its spectators). It receives room.state (`spectators: [{ playerId, name, connected }]`) and every match
|
||||
// broadcast (m.public, m.ticker, m.emote, b.pool — public data); the match registers it (opts.spectators /
|
||||
// addSpectator) and shows it fields like an eliminated player (b.start watch / m.field), never an m.private. It may
|
||||
// only g.watch (the heavy bucket, like every watcher), g.leave / room.leave, and room.loadout (stored for its session,
|
||||
// never handed to the match); anything else → SPECTATOR (▸ emotes too). Host: room.removeSpectator { playerId } any
|
||||
// only g.watch (the heavy bucket, like every watcher), g.leave / room.leave, and room.loadout / room.ownership (stored
|
||||
// for its session, never handed to the match); anything else → SPECTATOR (▸ emotes too). Host: room.removeSpectator { playerId } any
|
||||
// time → room.closed {kicked} to it. A spectator in a LOBBY room may take a free player seat with room.join of the same
|
||||
// code; a player never switches to spectating in place (ALREADY). Disconnect / grace / reconnect / expiry work as for
|
||||
// a player seat (the seat is kept and given back on resume).
|
||||
|
||||
import { randomBytes, randomInt } from 'node:crypto';
|
||||
import { ERR, MAX_SEATS, MAX_SPECTATORS, ROOM_CODE_LEN, modeIdFor } from '../shared/constants.js';
|
||||
import { checkLoadout } from '../shared/protocol.js';
|
||||
import { checkLoadout, checkNotOwned } from '../shared/protocol.js';
|
||||
import { encode, isDroppable, isErrCode, sendRaw, sendSession } from './net.js';
|
||||
import { getData as defaultGetData, lookup } from './data.js';
|
||||
import { Match as DefaultMatch } from './match/Match.js';
|
||||
@@ -109,7 +116,8 @@ const noopLog = { info() {}, warn() {}, error() {}, debug() {} };
|
||||
|
||||
/**
|
||||
* @typedef {{ seat: number, playerId: string, name: string, isBot: boolean, ready: boolean,
|
||||
* connected: boolean, left: boolean, loadout?: Record<string, { skill: number, module: string|null }> | null }} Seat
|
||||
* connected: boolean, left: boolean, loadout?: Record<string, { skill: number, module: string|null }> | null,
|
||||
* notOwned?: readonly string[] | null }} Seat
|
||||
*/
|
||||
|
||||
/** Deep-frozen copy of a checked loadout (shared by the session, the seat and the match's PlayerState). */
|
||||
@@ -289,6 +297,7 @@ export class Lobby {
|
||||
case 'room.kick': return this.kick(session, msg);
|
||||
case 'room.start': return this.start(session);
|
||||
case 'room.loadout': return this.loadout(session, msg);
|
||||
case 'room.ownership': return this.ownership(session, msg);
|
||||
case 'room.spectate': return this.spectate(session, msg);
|
||||
case 'room.removeSpectator': return this.removeSpectator(session, msg);
|
||||
default:
|
||||
@@ -575,6 +584,24 @@ export class Lobby {
|
||||
return OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* room.ownership (0.2.0 补位): keep the droppable chess of the not-owned list, store it on the session and the seat
|
||||
* (see the header). A running match never takes it: it keeps the list its seat had at its start.
|
||||
*/
|
||||
ownership(session, { notOwned }) {
|
||||
const data = this.safeData();
|
||||
const res = checkNotOwned(notOwned, (id) => lookup('chess', id, data));
|
||||
if (!res || res.error) return fail(ERR.BAD_MSG, res && res.detail);
|
||||
const list = Object.freeze(res.notOwned.slice());
|
||||
session.notOwned = list;
|
||||
const room = this.roomOf(session);
|
||||
if (!room) return OK;
|
||||
const seat = room.seatOf(session.playerId);
|
||||
if (seat) seat.notOwned = list;
|
||||
if (room.match && seat) return fail(ERR.ROOM_STARTED, 'stored for the next match');
|
||||
return OK;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
// Match wiring
|
||||
// ---------------------------------------------------------------------------------------------------
|
||||
@@ -587,6 +614,8 @@ export class Lobby {
|
||||
seat: s.seat, playerId: s.playerId, name: s.name, isBot: s.isBot, connected: s.connected,
|
||||
// DESIGN §16: the human's checked operator loadout (bots fight with the defaults)
|
||||
loadout: s.isBot ? null : s.loadout || null,
|
||||
// 0.2.0 补位: the chess the human marked as not owned (bots own every operator)
|
||||
notOwned: s.isBot ? null : s.notOwned || null,
|
||||
}));
|
||||
// lastPublic / results: the latest m.public broadcast and the m.result frames (encoded), kept for the replay.
|
||||
const ctx = { live: true, ended: false, disposed: false, match: null, lastPublic: null, sharedResult: null, results: new Map() };
|
||||
@@ -845,6 +874,7 @@ export class Lobby {
|
||||
return {
|
||||
seat: idx, playerId: session.playerId, name: session.name, isBot: false, ready: false, connected: session.connected, left: false,
|
||||
loadout: session.loadout || null,
|
||||
notOwned: session.notOwned || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user