feedback5 WK: 自选编队 on the server — room.diy { picks } (lenient: illegal picks dropped, only malformed input is BAD_MSG; stored per session / seat; a match takes the picks its seat had at the start, a change during a match is for the next one; welcome lists diyKitted), PlayerState.diy with the player's data view (a slotted DIY slot is its operator for every rule: bonds from its factions, no 特质, the pick's skill and module, the slot's tier / price / merge), a per-player stock per slotted piece (8 at tier 5, 5 at tier 6 [ASSUMED]; never in the shared pool; none when every bond of it is banned), DIY pieces in that player's shop rolls and reward offers once the 调度中心 reaches the slot's level, battleInput diy, m.private.diy / diyBanned, prep scouting and results name the operator, 信标 never sends a 自选 piece to a teammate [ASSUMED]

The owner's decisions of 2026-10-05 (picks, slots, shop per player, bans); research 0.2.0 §2. Golden unchanged (no scenario slots a DIY pick).
Tests: test/match/diy-shop.test.js (9), test/lobby-diy.test.js (4).
This commit is contained in:
sganggs committed 2026-10-05 23:35:01 +08:00
1 parent 742f478526
commit 4bee542e61
26 files changed
+1026 -105

No files matched your search

+10 -4
View File
@@ -29,6 +29,8 @@
//
// The handler object (implemented by server/lobby.js) receives:
// onHello(session, { resumed, repeat }) after `welcome` was sent
// welcomeInfo() → object (optional) extra fields of every `welcome` (never one of its own keys): the lobby's
// `diyKitted` (0.2.0 自选编队 — which operators a DIY slot may field)
// onMessage(session, msg) → { ok: true } | { error: ERR code, detail?: string } | undefined
// routeGame(session, msg) → same (optional): client-side combat reports `b.progress` / `b.result` (DESIGN §14) go
// straight to the running match through it; without it they reach onMessage
@@ -61,10 +63,10 @@ export const NET_DEFAULTS = Object.freeze({
/**
* Intents that also draw from the per-connection heavy bucket: g.watch (its reply is a large state resend, m.field),
* room.loadout (a ≤ 160-entry map validated against the game data; the client debounces its edits), room.ownership
* (a ≤ 160-id list, the same way) and room.spectate
* (a ≤ 160-id list, the same way), room.diy (≤ 8 自选 picks checked against the data, the same way) and room.spectate
* (taking a spectator seat in a running match resends its state like a watcher's g.watch — server/lobby.js spectate).
*/
export const HEAVY_TYPES = new Set(['g.watch', 'room.loadout', 'room.ownership', 'room.spectate']);
export const HEAVY_TYPES = new Set(['g.watch', 'room.loadout', 'room.ownership', 'room.diy', 'room.spectate']);
/** Close codes (see header). */
export const CLOSE = Object.freeze({ REPLACED: 4001, HELLO_TIMEOUT: 4002, POLICY: 1008, SHUTDOWN: 1001 });
@@ -109,6 +111,8 @@ export class Session {
this.loadout = null;
/** @type {readonly string[] | null} checked not-owned chess ids (干员持有, lobby-owned, 0.2.0 补位) */
this.notOwned = null;
/** @type {Readonly<Record<string, { charId: string, skillIndex: number, uniEquipId: string|null }>> | null} checked 自选 picks (lobby-owned, 0.2.0 自选编队) */
this.diy = null;
/** @type {string} client address of the latest connection (logging) */
this.addr = '?';
/** @type {string | null} per-network limit key of the latest connection (null = not limited), see clientAddress */
@@ -500,7 +504,7 @@ export class Network {
/**
* @param {{
* registry: SessionRegistry,
* handler: { onHello?: Function, onMessage: Function, onDisconnect?: Function, onExpire?: Function },
* handler: { onHello?: Function, onMessage: Function, onDisconnect?: Function, onExpire?: Function, welcomeInfo?: Function },
* log?: { info: Function, warn: Function, error: Function, debug?: Function },
* now?: () => number,
* options?: Partial<typeof NET_DEFAULTS>,
@@ -656,7 +660,9 @@ export class Network {
session.addr = conn.ip;
session.limitKey = conn.key;
const welcome = { t: 'welcome', playerId: session.playerId, token: session.token, name: session.name, serverNow: now, version: PROTOCOL_VERSION, resumed };
let extra = null;
try { extra = this.handler.welcomeInfo?.() ?? null; } catch (e) { this.log.error('[net] welcomeInfo crashed', e); }
const welcome = { ...(extra && typeof extra === 'object' ? extra : null), t: 'welcome', playerId: session.playerId, token: session.token, name: session.name, serverNow: now, version: PROTOCOL_VERSION, resumed };
if (validRid(rid)) welcome.rid = rid;
this.reply(conn, welcome);
try {