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).
198 lines
10 KiB
JavaScript
198 lines
10 KiB
JavaScript
// server/match/player/pieces.js — PlayerState methods: piece bookkeeping — new pieces, lookup by uid (board / hand /
|
|
// temp / equipped), the chess locations, detach and stow (hand first, overflow temp; a free hand slot pulls a temp piece
|
|
// in, _fillHandFromTemp), the per-piece round counters (拉普兰德), pool copies back to the shared pool (a 自选 piece's to
|
|
// the player's own stock), and summon stacks (removal, lifting, counts, the stack granted for an owner — a 自选 owner's
|
|
// from its pick, player/diy.js placeableTokens).
|
|
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
|
|
// the player state).
|
|
|
|
import { tileKey, boardOrder, freeSlot } from '../board.js';
|
|
|
|
export class PlayerPieces {
|
|
newPiece(kind, id, extra = {}) {
|
|
return { uid: this.m.nextUid(), kind, id, items: kind === 'chess' ? [] : undefined, count: kind === 'token' ? 1 : undefined, ownerUid: undefined, poolCopies: 0, boughtRound: this.m.round, meta: {}, ...extra };
|
|
}
|
|
|
|
/**
|
|
* Locate a piece by uid. Returns { piece, area: 'board'|'hand'|'temp'|'equipped', idx?, key?, holder? } or null.
|
|
*/
|
|
find(uid) {
|
|
if (!Number.isInteger(uid)) return null;
|
|
for (let i = 0; i < this.hand.length; i++) {
|
|
const p = this.hand[i];
|
|
if (!p) continue;
|
|
if (p.uid === uid) return { piece: p, area: 'hand', idx: i };
|
|
if (p.items) for (const it of p.items) if (it.uid === uid) return { piece: it, area: 'equipped', holder: p };
|
|
}
|
|
for (let i = 0; i < this.temp.length; i++) {
|
|
const p = this.temp[i];
|
|
if (!p) continue;
|
|
if (p.uid === uid) return { piece: p, area: 'temp', idx: i };
|
|
if (p.items) for (const it of p.items) if (it.uid === uid) return { piece: it, area: 'equipped', holder: p };
|
|
}
|
|
for (const [key, p] of this.board) {
|
|
if (p.uid === uid) return { piece: p, area: 'board', key };
|
|
if (p.items) for (const it of p.items) if (it.uid === uid) return { piece: it, area: 'equipped', holder: p };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Every owned chess piece: board (reading order, board.js boardOrder) then hand then temp. */
|
|
allChess() {
|
|
const out = [];
|
|
for (const { piece } of boardOrder(this.board)) if (piece.kind === 'chess') out.push(piece);
|
|
for (const p of this.hand) if (p && p.kind === 'chess') out.push(p);
|
|
for (const p of this.temp) if (p && p.kind === 'chess') out.push(p);
|
|
return out;
|
|
}
|
|
|
|
/** Locations of owned pieces, in merge-consumption preference order: temp, hand (left→right), board (reading order). */
|
|
_chessLocations() {
|
|
const out = [];
|
|
for (let i = 0; i < this.temp.length; i++) if (this.temp[i] && this.temp[i].kind === 'chess') out.push({ piece: this.temp[i], area: 'temp', idx: i });
|
|
for (let i = 0; i < this.hand.length; i++) if (this.hand[i] && this.hand[i].kind === 'chess') out.push({ piece: this.hand[i], area: 'hand', idx: i });
|
|
for (const { r, c, piece } of boardOrder(this.board)) if (piece.kind === 'chess') out.push({ piece, area: 'board', key: tileKey(r, c) });
|
|
return out;
|
|
}
|
|
|
|
/** Remove a located piece from its container (no side effects). */
|
|
_detach(loc) {
|
|
if (!loc) return;
|
|
if (loc.area === 'hand') this.hand[loc.idx] = null;
|
|
else if (loc.area === 'temp') { this.temp[loc.idx] = null; this._tempDue.delete(loc.piece.uid); }
|
|
else if (loc.area === 'board') this.board.delete(loc.key);
|
|
else if (loc.area === 'equipped') {
|
|
const i = loc.holder.items.indexOf(loc.piece);
|
|
if (i >= 0) loc.holder.items.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Put a piece into the hand (right→left) or, when `allowTemp`, the temp slots (due at the deadline of the first prep
|
|
* in which the player can act on it, _tempDueNow). Returns 'hand' | 'temp' | null. A piece put into temp while a hand
|
|
* slot is free (`toTemp`) moves into the hand at the next recompute (_fillHandFromTemp).
|
|
*/
|
|
stow(piece, { allowTemp = true, toTemp = false, preferIdx = null } = {}) {
|
|
if (!toTemp) {
|
|
if (Number.isInteger(preferIdx) && preferIdx >= 0 && preferIdx < this.hand.length && this.hand[preferIdx] == null) {
|
|
this.hand[preferIdx] = piece;
|
|
return 'hand';
|
|
}
|
|
const i = freeSlot(this.hand);
|
|
if (i >= 0) { this.hand[i] = piece; return 'hand'; }
|
|
if (!allowTemp) return null;
|
|
}
|
|
const j = freeSlot(this.temp);
|
|
if (j >= 0) { this._putTemp(j, piece); return 'temp'; }
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* PRTS 卫戍协议/帮助 §手牌区 "溢出单位会自动进入临时手牌区,常规手牌区出现空位时自动移入" (GitHub #82; until 0.1.3 the
|
|
* player had to drag them back): every free regular hand slot takes a temp piece at once. Order [ASSUMED] (PRTS names
|
|
* none): the temp row empties in the order it fills — right→left, the way stow fills it (freeSlot), so the piece that
|
|
* overflowed first moves first — and each piece takes the hand's next free slot right→left ("被发送至手牌区的物资优先
|
|
* 从右到左填充空位"). recompute() runs it, so it follows every change that frees a slot: a sale, a deployment from the
|
|
* hand, a merge that consumed hand copies, an item equipped / destroyed / used from the hand, a summon stack removed
|
|
* with its owner, an effect's destroyPiece… A piece that leaves temp is no longer due (tempDue). Returns the number
|
|
* moved.
|
|
* @returns {number}
|
|
*/
|
|
_fillHandFromTemp() {
|
|
let moved = 0;
|
|
for (let j = this.temp.length - 1; j >= 0; j--) {
|
|
const p = this.temp[j];
|
|
if (!p) continue;
|
|
const i = freeSlot(this.hand);
|
|
if (i < 0) break;
|
|
this._detach({ piece: p, area: 'temp', idx: j });
|
|
this.hand[i] = p;
|
|
moved++;
|
|
}
|
|
return moved;
|
|
}
|
|
|
|
/**
|
|
* Per-piece counter of the current round (`piece.meta.round` = { r, n: { key: count } }): 0 for a key not counted yet
|
|
* this round. The counters belong to the operator: a move keeps them, a new piece (bought, granted, transformed)
|
|
* starts at 0, and an elite merged this round keeps the highest count of its copies (_mergeChess) — 拉普兰德's
|
|
* "本回合首次主动刷新" is the first manual refresh she witnesses (player feedback after 0.1.0, garrisons/meta.js).
|
|
*/
|
|
pieceRoundCount(piece, key) {
|
|
const rc = piece && piece.meta && piece.meta.round;
|
|
return rc && rc.r === this.m.round && Number.isFinite(rc.n[key]) ? rc.n[key] : 0;
|
|
}
|
|
|
|
/** Add `n` to a piece's counter of the current round (pieceRoundCount); returns the new count. */
|
|
bumpPieceRoundCount(piece, key, n = 1) {
|
|
if (!piece || typeof key !== 'string' || !Number.isFinite(n)) return 0;
|
|
if (!piece.meta || typeof piece.meta !== 'object') piece.meta = {};
|
|
const v = this.pieceRoundCount(piece, key) + n;
|
|
if (!piece.meta.round || piece.meta.round.r !== this.m.round) piece.meta.round = { r: this.m.round, n: {} };
|
|
piece.meta.round.n[key] = v;
|
|
return v;
|
|
}
|
|
|
|
/** Return a piece's pool copies — to the shared pool, or a 自选 piece's to this player's stock (poolOf). */
|
|
returnCopies(piece) {
|
|
if (piece && piece.kind === 'chess' && piece.poolCopies > 0) {
|
|
const base = this.gd.baseIdOf(piece.id);
|
|
this.poolOf(base).give(base, piece.poolCopies);
|
|
piece.poolCopies = 0;
|
|
}
|
|
}
|
|
|
|
/** Remove every token owned by a chess piece (board, hand, temp). */
|
|
removeTokensOf(ownerUid) {
|
|
for (const [k, p] of [...this.board]) if (p.kind === 'token' && p.ownerUid === ownerUid) this.board.delete(k);
|
|
for (let i = 0; i < this.hand.length; i++) if (this.hand[i] && this.hand[i].kind === 'token' && this.hand[i].ownerUid === ownerUid) this.hand[i] = null;
|
|
for (let i = 0; i < this.temp.length; i++) if (this.temp[i] && this.temp[i].kind === 'token' && this.temp[i].ownerUid === ownerUid) this.temp[i] = null;
|
|
}
|
|
|
|
/**
|
|
* An owner that changes its board tile (moved, swapped): its summons on the board go back onto its stack (PRTS
|
|
* 卫戍协议/帮助 "移动干员时,其所属召唤物全部退场并重置至手牌区"); overflow temp when no stack or slot is left (a
|
|
* stack lost there comes back at the next round start). `keep`: a summon the player just placed (the one dragged
|
|
* onto its owner, which swapped the owner away) stays where it was put.
|
|
*/
|
|
_liftTokensOf(ownerUid, keep = null) {
|
|
for (const [k, p] of [...this.board]) {
|
|
if (p.kind !== 'token' || p.ownerUid !== ownerUid || p === keep) continue;
|
|
this.board.delete(k);
|
|
if (!this._returnToken(p, null, { allowTemp: true })) this.board.set(k, p); // nowhere to go: it stays put
|
|
}
|
|
}
|
|
|
|
/** Copies of one summon type an owner has (placed pieces + stacks in the hand / temp). */
|
|
_tokenCountOf(ownerUid, tokenId) {
|
|
const mine = (p) => !!p && p.kind === 'token' && p.ownerUid === ownerUid && p.id === tokenId;
|
|
let n = 0;
|
|
for (const p of this.board.values()) if (mine(p)) n += p.count || 1;
|
|
for (const p of this.hand) if (mine(p)) n += p.count || 1;
|
|
for (const p of this.temp) if (mine(p)) n += p.count || 1;
|
|
return n;
|
|
}
|
|
|
|
/**
|
|
* Owner on the board: send its placeable summons to the hand (one stack per token type, topped up to the deploy limit;
|
|
* gamedata.placeableTokens / tokens.json `placeable`, user playtest #6) — those its equipped skill / module makes
|
|
* (DESIGN §16: 赫默 S2 医疗无人机 a drone, 赫默 S1 none). Called when the owner is placed and at every round start, which
|
|
* returns a stack removed from temp at the last prep deadline (PRTS 卫戍协议/帮助 §手牌区 "干员所属召唤物会于下一回合
|
|
* 返还"); copies the owner still has (placed or stacked) are not granted again.
|
|
*/
|
|
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;
|
|
const stack = [...this.hand, ...this.temp].find((p) => p && p.kind === 'token' && p.ownerUid === owner.uid && p.id === tokenId);
|
|
if (stack) { stack.count = (stack.count || 1) + missing; continue; }
|
|
const t = this.newPiece('token', tokenId, { count: missing, ownerUid: owner.uid });
|
|
this.stow(t, { allowTemp: true });
|
|
}
|
|
}
|
|
}
|