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:
sganggs committed 2026-10-05 19:13:46 +08:00
1 parent 7d20695adf
commit 24205581e1
17 files changed
+590 -24

No files matched your search

+52 -3
View File
@@ -1,13 +1,14 @@
// server/match/player/basics.js — PlayerState methods: the basics — seat flags and counts (isHumanActive,
// botControlled, deployCap, deployCount, tempEmpty), the prep that resolves a temp piece (tempDue, _putTemp: every
// write into a temp slot), the operator loadout (DESIGN §16: setLoadout, loadoutFor), the deploy map on the field the
// write into a temp slot), the operator loadout (DESIGN §16: setLoadout, loadoutFor), the not-owned operators fielded
// as their stand-ins (0.2.0 补位: setNotOwned, fieldsStandIn, fieldRecord), the deploy map on the field the
// player deploys on (Match.deployFieldOf) and the withdrawal of pieces a terrain / deploy-field change left on tiles
// they may no longer occupy (_evictIllegal), dirty.
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
import { PHASE } from '../../../shared/constants.js';
import { checkLoadout, resolveLoadout } from '../../../shared/protocol.js';
import { checkLoadout, checkNotOwned, resolveLoadout } from '../../../shared/protocol.js';
import { tileKey, boardOrder } from '../board.js';
export class PlayerBasics {
@@ -73,11 +74,59 @@ export class PlayerBasics {
return true;
}
/** The skill index / module a chess record fights with under this player's loadout (DESIGN §16). */
/**
* The skill index / module a chess record fights with under this player's loadout (DESIGN §16) — for a chess this
* player fields as its stand-in (0.2.0 补位) the stand-in's backup selection, whatever the loadout says.
*/
loadoutFor(chessRecord) {
if (this.fieldsStandIn(chessRecord)) {
return resolveLoadout(null, this.gd.standIn(chessRecord.chessId), (id) => this.gd.standIn(id) || this.gd.chess(id));
}
return resolveLoadout(this.loadout, chessRecord, (id) => this.gd.chess(id));
}
/**
* Replace the not-owned list (0.2.0 补位; the seat's list at the match start — the setting never changes during a
* match): keeps the droppable chess (checkNotOwned) whose stand-in this match's data has. Bots own every operator.
* @param {any} list base chess ids
* @returns {boolean} false when the list is malformed (nothing changes) or the player is a bot
*/
setNotOwned(list) {
if (this.isBot) return false;
const res = checkNotOwned(list, (id) => this.gd.chess(id));
if (!res || !res.ok) {
this.m.log?.warn?.(`[match ${this.m.roomCode}] not-owned list of ${this.playerId} ignored: ${res && res.detail}`);
return false;
}
const ids = res.notOwned.filter((id) => !!this.gd.standIn(id));
this.standIns = Object.freeze(ids);
this._standInSet = new Set(ids);
return true;
}
/**
* Whether this player's piece of chess record (or id) `rec` fights as its stand-in (0.2.0 补位): its base chess is
* in `standIns` and the data has the stand-in. Normal and elite alike (the elite uses the same backup).
* @param {object|string|null} rec
*/
fieldsStandIn(rec) {
if (!this._standInSet || this._standInSet.size === 0 || !rec) return false;
const r = typeof rec === 'string' ? this.gd.chess(rec) : rec;
if (!r || typeof r.chessId !== 'string') return false;
return this._standInSet.has(r.baseId || r.chessId) && !!this.gd.standIn(r.chessId);
}
/**
* The record this player's piece of chess record `rec` fights with (0.2.0 补位): the stand-in record
* (gd.standIn — the chess's identity, the stand-in's body) when the player fields its stand-in, else `rec` itself.
* Rules about the unit's body read it (placement class, summon / bot ranges, the scouting art); rules about the chess
* (price, bonds, 特质, merges, pools) keep reading gd.chess.
* @param {object|null} rec
*/
fieldRecord(rec) {
return rec && this.fieldsStandIn(rec) ? this.gd.standIn(rec.chessId) || rec : rec;
}
/**
* Deploy classes of the board tiles (server/match/board.js buildDeployMap) on the field the player deploys on now
* (Match.deployFieldOf: the own board, or its half of the boss field in a boss round — user playtest #5 item 7).
+2
View File
@@ -181,6 +181,8 @@ export class PlayerPieces {
grantTokensFor(owner) {
const rec = owner && owner.kind === 'chess' ? this.gd.chess(owner.id) : null;
if (!rec) return;
// 0.2.0 补位: a chess fielded as its stand-in makes the stand-in's summons — none of the 17 has one (DATA.md §18)
if (this.fieldsStandIn(rec)) return;
for (const { tokenId, count } of this.gd.placeableTokens(owner.id, this.loadoutFor(rec))) {
const missing = count - this._tokenCountOf(owner.uid, tokenId);
if (missing <= 0) continue;
+4 -3
View File
@@ -66,9 +66,10 @@ export class PlayerPlacement {
if (!piece || piece.kind !== 'token' || this.gd.token(piece.id)?.ownerRange !== true) return null;
let at = owner;
if (!at) for (const [key, p] of this.board) if (p.uid === piece.ownerUid && p.kind === 'chess') { at = { key, piece: p, dir: pieceDir(p) }; break; }
const rec = at && this.gd.chess(at.piece.id);
if (!rec) return null;
const grid = attackRangeGrid(loadoutRecord(rec, resolveRecordLoadout(rec, this.loadoutFor(rec)))) || rec.rangeGrid;
const chess = at && this.gd.chess(at.piece.id);
if (!chess) return null;
const rec = this.fieldRecord(chess); // 0.2.0 补位: a stand-in's own range (its backup selection)
const grid = attackRangeGrid(loadoutRecord(rec, resolveRecordLoadout(rec, this.loadoutFor(chess)))) || rec.rangeGrid;
const [r, c] = parseKey(at.key);
return ownerRangeKeys(grid, r, c, at.dir);
}
+11 -5
View File
@@ -1,7 +1,7 @@
// server/match/player/round.js — PlayerState methods: the round lifecycle the match calls — startRound (income, pending
// coins, the shop, summon stacks topped up), endPrep, eliminate (every copy back to the pool), recompute (legality,
// out-of-range summons, temp pieces into free hand slots, bonds), the bond views — and battleInput (the player's
// PlayerBattleInput: board units with their loadout, carried 联防 state, the reached layers).
// PlayerBattleInput: board units with their loadout or their 补位 mark, carried 联防 state, the reached layers).
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
@@ -102,10 +102,16 @@ export class PlayerRound {
for (const { r, c, piece } of boardOrder(this.board)) {
if (piece.kind === 'chess') {
const u = { uid: piece.uid, kind: 'chess', chessId: piece.id, row: r, col: c, dir: pieceDir(piece), items: (piece.items || []).map((i) => i.id) };
// DESIGN §16: the equipped skill / module (elite only) from the loadout (defaults when absent)
const lo = this.loadoutFor(this.gd.chess(piece.id));
u.skillIndex = lo.skillIndex;
u.moduleId = lo.moduleId;
if (this.fieldsStandIn(piece.id)) {
// 0.2.0 补位: a chess this player does not own fights as its stand-in (sim getChess(id, { standIn: true }): the
// backup skill / module — no loadout fields, docs/SIM.md §12)
u.standIn = true;
} else {
// DESIGN §16: the equipped skill / module (elite only) from the loadout (defaults when absent)
const lo = this.loadoutFor(this.gd.chess(piece.id));
u.skillIndex = lo.skillIndex;
u.moduleId = lo.moduleId;
}
if (carry && carry.has(piece.uid)) u.carryState = carry.get(piece.uid);
units.push(u);
} else if (piece.kind === 'token') {
+3
View File
@@ -88,6 +88,9 @@ export class PlayerViews {
nextEnemies: this.m.nextEnemiesFor(this),
// DESIGN §16: the effective operator loadout ({ [baseChessId]: { skill, module } }; chess not listed use defaults)
loadout: this.loadout,
// 0.2.0 补位: the base chess ids this player fields as their stand-ins in this match (the not-owned list the seat had
// at the match start; [] = every operator owned) — the client's 「替补:X」 badges, the stand-in's art and card
standIns: this.standIns,
stats: {
dmgDealt: Math.round(this.stats.dmgDealt), kills: this.stats.kills, leaks: this.stats.leaks, gold: this.stats.gold,
refreshes: this.stats.refreshes, merges: this.stats.merges,