Files
ark-stronghold-protocol/server/match/player/pieces.js
T
sganggs 661f1c074e feedback5 WY: a newly merged elite 拉普兰德 is a new 拉普兰德 — its first manual refresh this round adds +8 even when its copies already fired (GitHub #169; the owner's decision of 2026-10-06)
garrison_123 「<刷新时>若为本回合首次主动刷新…此干员在整备区时也有效」 counts per piece (§21.1). No source says whether a
merge resets the count; since 0.1.1 _mergeChess carried the copies' highest per-piece round count onto the elite
([ASSUMED], conservative), so two white copies that had fired this round made an elite that did not fire again. The
owner decided on 2026-10-06 that a newly merged elite counts as a new 拉普兰德.

server/match/player/acquire.js _mergeChess no longer carries the per-piece round counters (拉普兰德's refresh count,
garrison:SERVER_GAIN_BOND_LAYER_BY_REFRESH_CNT:refreshes, is the only one): the elite starts at 0 like any new piece;
an in-place promotion keeps its count. test/match/feedback1-meta.test.js: the test that locked the old inherit is
updated on purpose (the elite adds +8 on its next refresh, once), and the case "two white copies refreshed this round,
merge, refresh → +8" is new; both fail without the change. Docs: META (counter API, edge cases), PLAYING, the comments
(PlayerState, pieces, effectsMeta, garrisons/meta), DESIGN §25.13.3 and the §25.10 table.
Golden unchanged (no corpus match merges 拉普兰德 after a refresh in the same round).
2026-10-06 21:41:49 +08:00

199 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, or
* an elite merged this round — _mergeChess carries none over: GitHub #169, the owner's decision of 2026-10-06)
* starts at 0 — 拉普兰德'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 });
}
}
}