feedback3 WF: the initial deployment goes by column — the left column first, top to bottom within a column (PRTS 从上到下>从左到右 read as a scan, the 阿戈尔 devour's order; mirrored boss side from its own left); a merge's elite among several deployed copies follows it (server and client); tests whose target was the latest deployed keep it

This commit is contained in:
sganggs committed 2026-10-04 19:50:42 +08:00
1 parent 0e3429aa1f
commit ca3d0572ad
25 files changed
+94 -69

No files matched your search

+3 -3
View File
@@ -321,7 +321,7 @@ export class PlayerState {
return null;
}
/** Every owned chess piece: board (deploy order) then hand then temp. */
/** 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);
@@ -509,8 +509,8 @@ export class PlayerState {
* Merge `need` normal copies of `baseId` (the incoming, not yet stowed piece first, then temp, hand, board) into
* the elite — PRTS 卫戍协议/帮助 §干员的获得与精锐化: "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
* 则发送至作战区对应位置)" (the user's playtest #6 follow-up confirms it). The tile (`mergeTile`): when a consumed copy
* stood on the board the elite takes its tile and facing — of several, the one that deploys first (board reading
* order: top → bottom, then left → right) [ASSUMED]. The incoming copy is never deployed (a 突变细胞 transformation
* stood on the board the elite takes its tile and facing — of several, the one that deploys first (the left board
* column first, top to bottom within a column — Battle.start's order) [ASSUMED]. The incoming copy is never deployed (a 突变细胞 transformation
* destroyed its carrier before the gain: that tile is no copy's). It replaces a deployed copy, so the deploy count
* never grows. Otherwise the elite goes to the hand, overflow temp — outside PREP too (a SETTLE merge's elite waits in
* temp through the next prep, tempDue). The copies' equipment returns to the hand ("干员晋级后已配发装备会回收至整备区";
+10 -5
View File
@@ -188,7 +188,11 @@ export function legalTiles(map, pos) {
return out;
}
/** Board pieces sorted in deployment order: top→bottom (row desc) then left→right (col asc). */
/**
* Board pieces in reading order: top row first (row desc), left to right within a row (col asc) — the battle input's order
* (unit ids), the garrisons' dispatch order and the lists the views send. Not the deployment order: the battle deploys by
* column (Battle.start; mergeTile follows that one).
*/
export function boardOrder(board) {
return [...board.entries()]
.map(([k, p]) => { const [r, c] = parseKey(k); return { r, c, piece: p }; })
@@ -197,16 +201,17 @@ export function boardOrder(board) {
/**
* Where a merge's elite stands (PRTS 卫戍协议/帮助 "若消耗已部署至作战区的干员,则发送至作战区对应位置"): of the board tiles
* the consumed copies stood on (`[{ key, dir }]`), the first in deployment order (top→bottom, left→right — the copy the
* battle deploys first; the right-hand boss half is mirrored on screen but deploys in the same board order) that
* `legal(r, c)` accepts for the elite, or null (⇒ the hand). [ASSUMED] the order: the official text names one position.
* the consumed copies stood on (`[{ key, dir }]`), the first in deployment order (the left board column first, top to
* bottom within a column — the copy the battle deploys first, Battle.start; the right-hand boss half is mirrored on
* screen but deploys in the same board order) that `legal(r, c)` accepts for the elite, or null (⇒ the hand). [ASSUMED]
* the order: the official text names one position. Until 0.1.3 the deployment order, and so this one, was top row first.
* public/js/ui/gameLogic.js mergeTarget mirrors it for the client.
* @param {Array<{ key: string, dir?: string }>} tiles
* @param {(r: number, c: number) => boolean} [legal]
* @returns {{ key: string, dir?: string, r: number, c: number } | null}
*/
export function mergeTile(tiles, legal = () => true) {
const sorted = (tiles || []).map((t) => { const [r, c] = parseKey(t.key); return { ...t, r, c }; }).sort((a, b) => b.r - a.r || a.c - b.c);
const sorted = (tiles || []).map((t) => { const [r, c] = parseKey(t.key); return { ...t, r, c }; }).sort((a, b) => a.c - b.c || b.r - a.r);
return sorted.find((t) => inField(t.r, t.c) && legal(t.r, t.c)) || null;
}
+14 -10
View File
@@ -5,11 +5,11 @@
// b.step(); b.finished; b.forceEnd(reason); b.time; b.result(); b.snapshot(); b.drainEvents(); b.on/off(...)
//
// Construction creates every ally unit (undeployed) and installs content (kits + domain modules). The first
// step() (or an explicit start()) deploys everything (operators top→bottom, left→right — mirrored for the right boss
// side —, then the summon pieces the same way — except a piece content flags `deferDeploy`, which waits on its tile;
// the players of a shared field side by side), fires `deploy` (initial) for each unit, forces out the operators that
// enter already knocked out (`carryState.down`, 联防: constants.js FORCED_EXIT — down on their tile, redeploy timer
// running) and then fires `battleStart`.
// step() (or an explicit start()) deploys everything (operators by column from the left, top to bottom within a
// column — mirrored for the right boss side —, then the summon pieces the same way — except a piece content flags
// `deferDeploy`, which waits on its tile; the players of a shared field side by side), fires `deploy` (initial) for
// each unit, forces out the operators that enter already knocked out (`carryState.down`, 联防: constants.js
// FORCED_EXIT — down on their tile, redeploy timer running) and then fires `battleStart`.
// An operator that left the field — knocked out, or forced out by its own effects (史尔特尔's 余烬 …, GitHub #60) — lies
// on its `body` tile — where it fell, or its own home when it fell on another board piece's home — and redeploys there;
// no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署; `isDown`, `_layBody`, `downOn`,
@@ -350,14 +350,18 @@ export class Battle {
if (this.started) return;
this.started = true;
this._safe(() => this._spawnStageDevices(), 'stageDevices');
// PRTS 卫戍协议/帮助: "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物(如果有)" — per player the operators
// top row first, left to right within a row (the mirrored right boss side: right to left in field columns,
// research 01 §4.3 "嘲諷優先鏡射"), then the summon pieces in the same order. [ASSUMED] On a shared field (联防, boss)
// the players' fields deploy at the same time (PRTS: one unit after another with a fixed delay, from the battle
// PRTS 卫戍协议/帮助 §作战阶段: "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物(如果有)", noted "自卫戍协议:
// 盟约 下半(2026/3/14)起,部署顺序由“从左到右>从下到上”更改为“从上到下>从左到右”" — a scanning order: down each column
// (top first), the columns from left to right; act 1's along each row, the rows from the bottom. It is the order PRTS
// gives the 阿戈尔 devour ("从最先部署(更靠左和靠上的)的【阿戈尔】干员开始", content/bonds/core.js egirOrder) and the one
// act-2 videos show. Per player the operators, left column first, top to bottom within a column (the mirrored right
// boss side from its own left, i.e. the highest field column — PRTS "部署顺序…左右镜像", research 01 §4.3), then the
// summon pieces in the same order. Until 0.1.3 the top row came first (row-major). [ASSUMED] On a shared field (联防,
// boss) the players' fields deploy at the same time (PRTS: one unit after another with a fixed delay, from the battle
// start), so the i-th operators of all players come in together (in `players` order), then the summons likewise
const seq0 = this._deploySeq;
const lists = this.players.map((ps) => ps.units.filter((u) => u.kind === 'op' || u.kind === 'token').slice().sort((a, b) =>
b.homeR - a.homeR || (ps.mirror ? b.homeC - a.homeC : a.homeC - b.homeC) || a.id - b.id));
(ps.mirror ? b.homeC - a.homeC : a.homeC - b.homeC) || b.homeR - a.homeR || a.id - b.id));
// a summon piece flagged `deferDeploy` by content (one its owner's loadout does not make — 赫默 on S1 with a drone
// piece —, or a skill's summon when shared/constants.js SKILL_SUMMON_START_DEPLOY is off: content/tokens.js
// dockSkillSummons) stays off the field, its tile reserved (isReservedTile), until content deploys it
+2 -2
View File
@@ -129,7 +129,7 @@ function textNum(text, re, fallback) {
/**
* A tile a summon may take: inside the field and not reserved (Battle.isReservedTile: nobody on it, no knocked-out
* operator lying there, not the home tile of an ally that has not deployed yet / waits to redeploy — the initial
* deployment runs top→bottom: a summon placed while it runs must not steal a later board unit's tile).
* deployment runs one unit after another: a summon placed while it runs must not steal a later board unit's tile).
*/
function freeTile(battle, r, c) {
return Number.isInteger(r) && Number.isInteger(c) && battle.grid.inRect(r, c) && !battle.isReservedTile(r, c);
@@ -290,7 +290,7 @@ const KITS = {
battle.addBuff(unit, { key: 'talent:angel_bless', mods: { ...bless }, persist: true, allowDead: true });
battle.on('deploy', (ctx) => {
if (ctx.unit !== unit) return;
// after the whole board is deployed (initial deployment goes top→bottom)
// after the whole board is deployed (the initial deployment goes one unit after another)
battle.after(0, () => {
if (!alive(unit)) return;
const cands = battle.allies(unit.ownerId).filter((a) => a !== unit && a.kind === 'op' && !a.findBuff('talent:angel_bless_ally'));
+2 -1
View File
@@ -348,7 +348,8 @@ function inRectTile(battle, r, c) { return Number.isInteger(r) && Number.isInteg
/**
* A tile a summon may take: inside the field, nobody on it, and not the home tile of a board unit that has not
* deployed yet / waits to redeploy (initial deployment runs top→bottom, a summon must not steal a later unit's tile).
* deployed yet / waits to redeploy (the initial deployment runs one unit after another, a summon must not steal a later
* unit's tile).
*/
export function tileFree(battle, r, c) {
return inRectTile(battle, r, c) && !battle.isReservedTile(r, c);