feedback5: merge WE2 — fidelity (data / match / UI): summon hand counts from the tokens' hidden talents (麦哲伦 / 令 3, 白铁 2), 望's 棋子 on any tile, the 一会儿见! marker no hand card, TRY_SEARCH_ALLY_SKILL → SKILL_RANGE + allies, 斩业星熊's normal attack phys, IS / RA-only modules refused (SO-A / SO-B / RA-A), 黑键 S2 at full SP with a free tile, 重构体 / 战术锚点 placement, 塑心's cap per trait, random grants draw 自选 stock, diyBanned shown, AI 托管 buys 自选, 谬因 S2 / 薇薇安娜 S3 area triggers, #148 regression test, tier*.js / diyRecord shims removed; golden: coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116 moved (WE2's own), the W4B moves kept

This commit is contained in:
sganggs committed 2026-10-06 16:15:52 +08:00
commit 723ee72d38
79 files changed
+1389 -405

No files matched your search

+20
View File
@@ -92,3 +92,23 @@ export function pickGetter(getChess, pick, data) {
/** Whether a record is a composed 自选 record. */
export const isDiyRecord = (rec) => isObj(rec) && typeof rec.diyFor === 'string';
/**
* The player's slotted 自选 pieces this match leaves out of the shop because every bond of the operator is switched off
* (m.private.diyBanned — the server's initDiyStock: no stock), in slot order: [{ slotId, charId, name }] (the operator's
* name from its composed record; the charId when the data lacks it). [] without any.
* @param {any} priv m.private @param {(id: string) => any} getChess @param {{ chess?: any, backups?: any }} data
*/
export function diyBannedPieces(priv, getChess, data) {
const ids = Array.isArray(priv?.diyBanned) ? priv.diyBanned.filter((id) => typeof id === 'string') : [];
const picks = diyPicks(priv);
const out = [];
for (const slotId of ids) {
const pick = picks[slotId];
if (!isObj(pick) || typeof pick.charId !== 'string') continue;
const chess = getChess(slotId);
const rec = chess ? diyRecordFor(chess, pick, data) : null;
out.push({ slotId, charId: pick.charId, name: rec?.name || pick.charId });
}
return out;
}
+35 -6
View File
@@ -227,8 +227,13 @@ export function piecePosition(ctx, piece) {
if (meleeOnHighGround(rec)) return 'ALL';
return rec?.position === 'MELEE' ? 'MELEE' : 'RANGED';
}
// tokens: MELEE → ground only; RANGED / ALL → any deployable tile
if (piece.kind === 'token') return ctx.getToken(piece.id)?.position === 'MELEE' ? 'MELEE' : 'RANGED';
// tokens: MELEE → ground only; RANGED / ALL → any deployable tile; `rangedTilesOnly` (战术锚点 "仅可以部署在…远程位")
// → 'HIGH', the ranged tiles only (server/match/board.js positionClass 'high')
if (piece.kind === 'token') {
const t = ctx.getToken(piece.id);
if (t?.rangedTilesOnly === true) return 'HIGH';
return t?.position === 'MELEE' ? 'MELEE' : 'RANGED';
}
return null;
}
@@ -237,6 +242,7 @@ export function tileAllows(ctx, piece, row, col) {
const pos = piecePosition(ctx, piece);
if (!pos) return false;
const k = tileKey(row, col);
if (pos === 'HIGH') return ctx.deploy.ranged.has(k) && !ctx.deploy.melee.has(k);
return pos === 'MELEE' ? ctx.deploy.melee.has(k) : ctx.deploy.ranged.has(k);
}
@@ -251,6 +257,23 @@ export function tileAllows(ctx, piece, row, col) {
*/
export function summonRange(ctx, piece, owner = null) {
if (!isObj(piece) || piece.kind !== 'token' || ctx?.getToken?.(piece.id)?.ownerRange !== true) return null;
return ownerRangeOf(ctx, piece, owner);
}
/**
* Board tiles ('r,c') an outside-bound summon may NOT stand on — its owner's attack range (tokens.json
* `ownerRangeOutside`: 凯尔希·思衡托's 战术锚点 "仅可以部署在凯尔希·思衡托攻击范围外的远程位") — or null. Mirror of
* server/match/player/placement.js summonExcluded.
* @param {ReturnType<typeof placementContext>} ctx
* @returns {Set<string> | null}
*/
export function summonExcluded(ctx, piece, owner = null) {
if (!isObj(piece) || piece.kind !== 'token' || ctx?.getToken?.(piece.id)?.ownerRangeOutside !== true) return null;
return ownerRangeOf(ctx, piece, owner);
}
/** The attack-range tiles of a summon piece's owner on the board (summonRange / summonExcluded), or null. */
function ownerRangeOf(ctx, piece, owner = null) {
let at = owner;
if (!at) for (const e of ctx.boardAt.values()) if (e.piece.uid === piece.ownerUid && e.piece.kind === 'chess') { at = e; break; }
const rec = at && ctx.getChess(at.piece.id);
@@ -260,11 +283,14 @@ export function summonRange(ctx, piece, owner = null) {
return new Set(rangeTiles(grid || rec.rangeGrid, at.row, at.col, pieceDir(at.piece)).map(([r, c]) => tileKey(r, c)));
}
/** tileAllows plus the owner-range rule of a range-bound summon (summonRange). */
/** tileAllows plus the owner-range rules of a range-bound summon (summonRange, summonExcluded). */
function unitAllowed(ctx, piece, row, col, owner = null) {
if (!tileAllows(ctx, piece, row, col)) return false;
const k = tileKey(row, col);
const range = summonRange(ctx, piece, owner);
return !range || range.has(tileKey(row, col));
if (range && !range.has(k)) return false;
const out = summonExcluded(ctx, piece, owner);
return !out || !out.has(k);
}
/**
@@ -329,13 +355,16 @@ export function canPlace(ctx, uid, target) {
if (src.area === 'board' && src.row === row && src.col === col) return { ok: true, action: 'orient' };
if (!tileAllows(ctx, piece, row, col)) {
const deployable = ctx.deploy.ranged.has(tileKey(row, col));
return no('BAD_TILE', deployable && piecePosition(ctx, piece) === 'MELEE' ? '近战单位只能部署在地面' : '无法部署在该位置');
const pos = piecePosition(ctx, piece);
return no('BAD_TILE', deployable && pos === 'MELEE' ? '近战单位只能部署在地面' : deployable && pos === 'HIGH' ? '只能部署在远程位' : '无法部署在该位置');
}
// a range-bound summon (战术点): inside its owner's attack range — seen from the summon's old tile when it is
// dropped onto its own owner (the two swap)
// dropped onto its own owner (the two swap); an outside-bound one (战术锚点) outside it
const ownerSwap = src.area === 'board' && occ && occ.piece.uid === piece.ownerUid ? { row: src.row, col: src.col, piece: occ.piece } : null;
const range = summonRange(ctx, piece, ownerSwap);
if (range && !range.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围内');
const out = summonExcluded(ctx, piece, ownerSwap);
if (out && out.has(tileKey(row, col))) return no('BAD_TILE', '只能部署在召唤者攻击范围外');
if (src.area === 'board') {
// board → board: move or swap (the occupant must be legal on the source tile — the mover's own summon excepted:
// a moved operator's summons go back to the hand anyway)