diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 1806277c..a9c3e1fa 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -266,7 +266,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
### 5.5 Combat rules (engine)
-- **Initial deployment** at t=0, free: operators row-major top→bottom then left→right (right-side boss player: right→left), then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物"; `aggroSeq` ranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together [ASSUMED]. Deploy effects fire (`deploy` with `initial: true`). Then operators carried into 联防 knocked out (`carryState.down`) are forced out (`FORCED_EXIT`, HP 0, §19.3). Then `battleStart`, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too).
+- **Initial deployment** at t=0, free: operators by column — the left column first, top to bottom within a column (right-side boss player: from its own left, the highest field column) — then the summons (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", read as a scan: down each column, the columns left to right — the order PRTS gives the 阿戈尔 devour, "更靠左和靠上"; row-major until 0.1.3, §23.f4; `aggroSeq` ranks every start-of-battle summon after all operators) — a skill's placed summon (赫默 医疗探机, 巫恋 诅咒娃娃) included, once and free ("所有手动部署的召唤物,无视所属干员的持有状态…作战开始时立即部署一次"; afterwards it re-appears on its tile each time the owner's skill gives one, §20.1); on a shared field (联防, boss) the players' i-th operators deploy together [ASSUMED]. Deploy effects fire (`deploy` with `initial: true`). Then operators carried into 联防 knocked out (`carryState.down`) are forced out (`FORCED_EXIT`, HP 0, §19.3). Then `battleStart`, after which the forced-out operators' timers are re-read (redeploy-time effects that start with the battle, e.g. 机变 征召, cover them too).
- **DP**: per player, start 10, +1/s, cap 99; used only for auto-redeploy (dead op: after `respawnTime`, if the tile it lies on is free and DP ≥ cost ⇒ redeploy there with full HP, SP = initSp). A knocked-out operator (and, since 0.1.2, one forced out by its own effects, §22.15) stays **down** on the tile it fell on — its own home when it fell on another board piece's home (that piece on the field or not) and its home is free — until it redeploys there, and no ally deploys or moves onto that tile meanwhile (PRTS 卫戍协议/帮助 §作战阶段 单位部署; `Battle.downOn` / `restTile` / `_layBody`, §21.24; b.snap `down`, §18.3: counting / waiting for DP / its tile taken, a safeguard) — also one that entered 联防 knocked out (`FORCED_EXIT`, its full redeploy timer — confirmed by the user — with the battle-start redeploy effects, §19.3); summons, devices and enemies leave at once.
- **Blocking** (contact radius, PRTS 游戏数据基础 §阻挡半径 / 作战机制 "中点判定…案例:阻挡"; `constants.js BLOCK_RADIUS`, `Battle._checkBlock`, §19.2): an operator with free capacity blocks an unblocked, blockable enemy whose position lies within its block radius of its centre — ground 0.7071 (squared 0.49999037), 0.8944 for blockFly units vs flyers (起飞), devices 0.4472 (阻隔工事 / 障碍物); air units (FLY, 近地悬浮, 浮空) only by blockFly units. A ground enemy is never blocked by a unit standing on a tile ground units cannot pass (围墙 / 围栏: PRTS 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — an enemy pushed or pulled against the fence walks on; air blocking there stays [ASSUMED], §21.23) nor by an airborne (起飞) unit, which blocks flyers only (§21.22). The nearest such operator wins (ties: scan order [ASSUMED]). It is re-checked every tick, moving or not, so another operator in contact with room takes the enemy over when its blocker dies, is withdrawn or is stunned, and an operator whose capacity frees up grabs an overlapping enemy; one that finds no room walks on. A head-on enemy stops at contact (≈ 0.71 tile), outside the blocker's tile. Enemy uses `blockCnt` capacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). **Every blocker** — melee units (重装要塞 / 领主 / 哨戒铁卫 included), summons and a ranged operator on a melee tile alike — whose attack hits enemies may always target the enemies it blocks, in range or not, whatever its facing, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器; the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front; `Battle.blockedTargets`, `targeting.js sortEnemyTargets`; §20.3 — it replaces the playtest #5 QA's melee-only restriction of §19.2). A healer (dmgType `heal`, or a skill attack turned into a heal) selects injured allies only and keeps healing while it blocks (PRTS 卫戍协议/帮助 "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; §21.17); 咒愈师 follow the blocker rule. The huge leaders are 自缚 + unblockable (§7).
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)` (a `rangeAoe` profile — 阵法术师, 轰击术师 — takes every selectable enemy in range, §21.18), priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers, `trait` text parsed by build-data into `targetPriority`), (3) enemy with the **least remaining path distance** to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (`Unit.isFlying`: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed — or within 3 s of a block's end (§22.8). Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact; a 锁定攻击范围 AoE (`rangeAoe`) strikes every selectable enemy on its range at once with instant hits (§21.18).
@@ -707,7 +707,7 @@ Ten reports after playing v2.3 (绝境 on desktop, 标准 on an Android phone in
- **Enemy kits**: 掠海漂移体 — 晕眩 / 冻结 / 沉睡 → 爬行模式 for good (0.5 s stun), then it only attacks its blocker; 不会攻击飞行单位. Its model follows (integration QA: melee operators hitting an enemy that still looked airborne): the sim's fx `form` `crawl` switches the view to the skeleton's crawl clips (`*_02`, after its `Change` clip) — `render/units.js FORMS`, `spine.js setForm`, kept on the unit info for a view built later (a view built mid-battle starts crawling: UnitInfo `form`, §21.4). 吉兆飞鳞 — 晕眩 / 沉睡 → 8 s 晕眩模式 (it stays down while still asleep), a freeze grounds it when the freeze ends, its sprint is the final ×3. Of the 32 official floaters only these two hover for good in this mode; 喷气人 hovers for its 7 s 飞行模式 (§21.9) (the bot's `HOVER_KEYS`; `fieldModel` counts them as air routes on ground paths).
- **Skill effects** (per PRTS 备注; the official per-ability targetMotion is not in the data): ground-only — 隐德来希 S2 血镰, 归溟幽灵鲨 拥抱自我, 琳琅诗怀雅 S3, 乌尔比安 S1 [ASSUMED: a 捕网 like 雪雉's]; PRTS 可对空 — 德克萨斯 S2, 忍冬 S2, 焰尾 S2, 缄默德克萨斯 S3, 凛御银灰 S2, 玛恩纳 S3 (his melee skill attacks: `targeting.canHitFly`; §21.8), 锏 S3 归于宁静 (added by §20.3); no 对空 note, [ASSUMED] to hit air — 风丸 折纸生花, 乌尔比安 S3, 缄默德克萨斯 S2, 余 S2, 见行者 S2, 耀骑士临光 不畏苦暗 and the token appear bursts (“耀阳”, 沙之碑, 迷迭香的战术装备's stun, 纸偶). A stun / freeze / sleep from any of them drops a hovering 掠海漂移体.
- **#5 official**: PRTS 索敌 (敌方) "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→不容易被攻击)→最早出现"; PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", the right-hand boss player mirrored.
-- **#5 changes**: `Battle.start` deploys operators row-major, then summons; `aggroSeq` ranks every start-of-battle summon after all operators (`deploySeq` stays the deployment identity). An enemy's own rule `e.profile.canTarget` filters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (`targeting.js aggroCmp`): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flag `camou`, not 隐匿 for 隐匿 conditions, §20.2). [ASSUMED] "从上到下>从左到右" = row-major; on a shared field (联防, boss) the players' i-th operators deploy together, then the summons (it used to be player after player, which made the leader prefer the later player's pieces).
+- **#5 changes**: `Battle.start` deploys operators row-major (by column since 0.1.3, §23.f4), then summons; `aggroSeq` ranks every start-of-battle summon after all operators (`deploySeq` stays the deployment identity). An enemy's own rule `e.profile.canTarget` filters its candidates before the sort (萨卡兹枯朽战车 no longer idles when its preferred target stands on high ground; it, 掠海漂移体 and “萨科塔之眼” ignore flying allies). Special priorities break ties by taunt, then latest deployed (`targeting.js aggroCmp`): 铳 / 昆图斯 highest DEF, 胄 highest / lowest ATK, the 碎铳之簧 bounce, 唱沙 / 沙狱 max HP, “自在” nearest. Kept (PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿"; 索敌的概念): a stealthed ally (隐匿 / 迷彩 / 排气格栅) is attacked only by the enemy it blocks (迷彩 is its own flag `camou`, not 隐匿 for 隐匿 conditions, §20.2). [ASSUMED] "从上到下>从左到右" = row-major (superseded in 0.1.3: a column scan, §23.f4); on a shared field (联防, boss) the players' i-th operators deploy together, then the summons (it used to be player after player, which made the leader prefer the later player's pieces).
- Tests: `test/sim/playtest5-targeting.test.js` (18; 13 fail on 731a01c), `test/content/enemies_bosses.test.js` (吉兆飞鳞), `test/match/bot.test.js` (a hovering wave counts as air), `test/render/forms.test.js` (the crawl clips); browser (opt-in) `test/render/forms.browser.test.js` (the real sim feeding the field view).
### 19.2 Blocking by contact radius, and the hand-over (#4) — `sim/constants.js BLOCK_RADIUS`, `Battle._checkBlock` / `blockedTargets`, `ai.js`
@@ -999,7 +999,7 @@ Still [ASSUMED] (each in its section): the class rows covering every MANUAL skil
- **Seen** (the user, first-hand, after playtest #6): "官方就是合成精锐时,如果消耗了场上的干员,精锐会出现在场上那个位置".
- **Cause**: research 01 A1 row 7 read PRTS 帮助 as "1 elite goes to the hand, not to a board tile" (marked VERIFIED, repeated in `01-core-data.json` promotion.eliteGoesTo), so `_mergeChess` put every elite in the hand and used a consumed copy's tile only when the hand and temp were both full.
- **Official** (PRTS 卫戍协议/帮助): §干员的获得与精锐化 "…发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; tip "干员晋级后已配发装备会回收至整备区,需要重新配置"; §战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区", deploy order "从上到下>从左到右". The text does not say which tile wins among several deployed copies.
-- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — row desc, then col asc, the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it, `transformChess` `fromKey` / `fromDir` [ASSUMED], is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
+- **Now (server)**: when a consumed copy stood on the board the elite takes that tile and its facing, otherwise it goes to the hand, overflowing into temp (outside PREP too). Several deployed copies: the one that deploys first — col asc, then row desc since 0.1.3 (§23.f4; row desc, then col asc before), the mirrored right boss half included — `board.js mergeTile(tiles, legal)` [ASSUMED]. A 突变细胞 transformation destroys its carrier before the gain, so the carrier's freed tile is no copy's (§21.1 — official footage; the 0.1.1 WA rule that counted it, `transformChess` `fromKey` / `fromDir` [ASSUMED], is withdrawn). A tile the elite may not use is skipped; with the hand and temp also full the elite stays on the first deployed copy's tile rather than being lost (as `_evictIllegal`). The deploy count never grows (no BOARD_FULL path). Equipment returns to the hand, overflow temp; with both full it stays on the elite up to its `equipPerChess` (2) slots, any further item destroyed with a log warning (unchanged). The consumed copies' summons are removed; an elite on the board gets its own fresh stack (`grantTokensFor`, after the items) following the §16 loadout [ASSUMED: placed summons are not carried over]. Every path (g.buy, g.reward, `ctx.grantChess`, 画卷, transformations), every phase (a SETTLE / Final Assault merge places the elite at once, its reward offer waits for the next prep) and the boss-field prep (legality on the player's half, §19.7). Reward offer, ticker and `stats.merges` unchanged; `onMerge` carries `area` ('board' | 'hand' | 'temp'). Bots plan the elite like any owned unit.
- **Now (client)**: `gameLogic.mergeTarget(priv, chessId)` mirrors `mergeTile`; a shop or reward card armed for a merge-completing buy lights the target tile in gold (`ShopBar onArm` → `screens/game.js` highlight group `mergeTile`); the 可晋升 tag says 精锐干员将出现在作战区原位置 or 精锐干员将进入整备区; an elite card never merges (`completesChessMerge` refuses `isGolden`, `mergeProgress` reports 0 copies: no pips, tag or tile); `render/promote.js promotionsOf` recognises a merge in setPrep and the elite plays `fx.promote` (gold pillar, shockwave, hex ring, streaks from the other copies) instead of `fx.deploy` (`view.debug.promotions`).
- **Checks**: `audit.js` checks every merge — the tile is `mergeTile` of the deployed copies legal for the elite, with that copy's facing; the deploy count did not grow; otherwise the elite is not on the board, except the no-room fallback. `tools/matchrun --check` sweeps are clean.
- Normative lines rewritten: §2 (`promote.js`), §6.2 (the economy line and the temp-overflow paragraph), §9 (the promotion cue).
diff --git a/docs/META.md b/docs/META.md
index 1960484d..cc0e8024 100644
--- a/docs/META.md
+++ b/docs/META.md
@@ -12,7 +12,7 @@ server/match/
(data/tuning.json, §3.1)
pool.js SharedPool (copies per base chess, across players), per-match bans, copy-weighted rolls
board.js placement legality from the stage legend on the deploy field (own board / boss half), slot helpers,
- deployment order
+ reading order (boardOrder), a merge's elite tile in deployment order (mergeTile)
bondsMeta.js bond counting modes, tiers, 调和 / 独行 / 助力 / 绝技, layers
effectsMeta.js MetaRegistry + EffectDispatcher + the handler ctx (this document, §2)
builtinMeta.js engine built-ins (consume-on-equip items, Arts, EffectRefs used by 机变 defaults)
@@ -342,7 +342,7 @@ Every handler method is `(ctx, ev)`; `ev` is shared by all handlers of one dispa
| `onDestroy` | an item was destroyed (player / replaced) | `{ item, holder, reason }` |
| `onLayers` | bond layers were added (prep or battle gains) | `{ bondId, from, to, reason }` (milestones: 维多利亚 25, 远见 10, 奇迹 100 …); `to` ≤ 999 (`BOND_LAYER_CAP`) — a gain at the cap dispatches nothing, so the milestones stop with the count |
-Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in deployment order, then hand)
+Dispatch order per player: `global` → `band` → `bond` (data order) → garrisons (board in reading order — top row first, `board.js boardOrder` —, then hand)
→ equipped items → EffectRefs (insertion order). `onPrice` runs the priced chess's own 特质 first (购买价格为N sets the
price that 远见's discount and the strategies' caps then act on). Every call is isolated with try/catch (the error is
logged once and counted in `match.dispatcher.errors`); nested dispatches are capped at depth 6.
@@ -511,7 +511,7 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
* **Merge**: 3 normal copies (风丸 2) anywhere (board/hand/temp) → 1 elite (the incoming copy, then temp, hand, board
copies are consumed). Where it goes (PRTS 卫戍协议/帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,
则发送至作战区对应位置)", the user's playtest #6 follow-up): when a consumed copy stood on the board, onto that copy's
- tile with its facing — of several, the one that deploys first (row desc, then col asc; `board.js mergeTile`,
+ tile with its facing — of several, the one that deploys first (col asc, then row desc; `board.js mergeTile`,
[ASSUMED]); a 突变细胞 carrier is destroyed before its gain, so its freed tile is no copy's (`transformChess`, DESIGN
§21.1); a tile the elite may not use (a stale terrain change) is skipped. It replaces a deployed copy, so the deploy count never
grows (no BOARD_FULL), and as a deployment its manually deployable summons join the hand (`grantTokensFor`, the
diff --git a/docs/PLAYING.md b/docs/PLAYING.md
index 3f7851d4..7c0ad16c 100644
--- a/docs/PLAYING.md
+++ b/docs/PLAYING.md
@@ -18,7 +18,7 @@
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。选的时候可以点左侧决策顺序下方的「查看禁用盟约与干员」,再看一遍「确认本局信息」里的盟约(置灰、禁用人数)和本局禁用干员:只能查看,倒计时照常走,底部也显示轮到谁和剩余秒数;点「关闭」、点窗口外面或按 Esc 关闭,轮到下一位时会自动关闭。 |
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场(如教鞭的法术大师A2·多轮战术特训)。各回合的悬赏决策按官方对局截图来出:第 3 回合是 6 张「接下来两场作战」的悬赏(3 张 1 资金、2 张 2 资金、1 张 3 资金);第 9 回合是各首领的「下场作战」悬赏和「源石虫·特训」;绝境 / 终极的第 11 回合多半是悬赏决策(也可能是机密商店或战术决策),其中的悬赏通常有 1 只特异巨兽,其余是各阵营的「下场战斗」悬赏。击倒悬赏敌人的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。**机密商店**的 6 件装备都免费,和官方一样可能出现两件相同的装备(各算一张卡,可以分别被选走);第 11 回合(绝境 / 终极)的机密商店按官方:2 件 VI 阶、至少 1 件 V 阶和 1 枚盟约之币,标准 / 险境较早回合的机密商店各阶装备都可能出现。**战术决策**也和官方一样可能出现两张相同的卡(官方截图里出现过两张「补给」;各算一张卡,可以分别被选走,每张只生效一次,两人各选一张全队卡则效果叠加);第 11 回合(绝境 / 终极)的战术决策按官方截图只出盟誓、驰援、列装 / 财富 / 补给 / 整备 / 升华等增益卡,不出削弱敌人的卡和战场演变卡。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
-| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
+| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
| 联防 | 见第 5 节。 |
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
@@ -55,7 +55,7 @@
- **购买点两次**:第一次点商店卡片只选中(卡片抬起并打开详情,底部出现「确认购买 · 再次点击」),再点这张卡片的**任意位置**(包括金色的确认条)才购买;点别处或按 Esc 取消。升级调度中心、晋升奖励同样两次;策略或装备带来的免费挑选(如凯瑟琳【定向投放】升级后刷新的 3 件装备、娜仁图亚【见者有份】的 2 件装备、寻呼模块的 3 名干员)也一样出现在商店卡片的位置,左侧标题写着它的来源,装备显示为装备卡(图标、名称、说明、FREE),选 1 个免费获得;同时有多个免费挑选时依次出现,标题下和「稍后」后的提示按钮会写还有几项(如「+1」)。不能购买时(例如已准备就绪)点卡片只打开详情。
- **公共卡池**(同盟):每名干员的副本数有限,I–VI 阶分别 12 / 14 / 18 / 16 / 8 / 5 份,所有玩家共用;持有的干员(棋盘 + 整备区)占用副本,商店里展示的不占用。一名干员被大家拿光后就刷不出来了;淘汰玩家的干员会还回卡池。
- **位置上限**:棋盘最多部署 **8** 名;整备区 **10** 格;整备区满时新获得的单位(合成的精锐、奖励、退回的装备等)进入**临时整备区**(整备区前面的 5 格)。临时整备区有单位时**不能准备就绪**,棋盘上会用红色虚线框标出这一排并说明规则:把它们放进整备区或战场、配发或使用,或者出售 / 销毁。休整期中进入临时整备区的单位,若到**本休整期结束**时仍留在那里就会被自动处理(干员返还卡池、装备销毁);作战、结算期间或准备就绪之后才进入的(例如战斗奖励、结算时合成的精锐),会保留到**下一个休整期**结束,你一定有机会处理它们。
-- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——从上到下、从左到右);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
+- **晋升精锐**:3 名同名干员(无论是否部署)自动合成**精锐**(等级、技能、模组大幅提升;风丸只需 2 名),精锐不会再合成。如果被合成的干员里有**已经部署在战场上**的,精锐会直接出现在**那名干员的位置**(保持原来的朝向,不多占部署位;有两三名都在场上时,取最先部署的那名——最左一列优先,同一列从上到下);否则精锐进入整备区(满了进入临时整备区)。原先配发的装备都退回整备区,需要重新配发;被合成干员的召唤物随之移除,场上的精锐会把自己的召唤物送进整备区。点选一张能合成的商店卡片时,精锐将出现的那一格会亮起金色边框,同时打开的详情卡里也会写明去向(「可晋升:精锐干员将出现在作战区原位置 / 将进入整备区」;详情卡会自动停靠在不遮挡这一格的一侧)。在回合开始或结算时因策略、奖励等完成的合成,精锐出现时同样会播放金色的晋升特效。
- **晋级奖励**:每次晋升后获得一次特殊招募:从 3 名 **高一阶**(最高 VI)、**互不相同**的随机干员中**免费**挑 1 名(该阶剩余干员不足 3 名时由低一阶补足)。它不能刷新、不能冻结、本回合结束失效。
- 每回合商店会自动刷新未冻结的格子。
- **「本回合首次主动刷新」按每名干员各自计算**(拉普兰德):她在本回合遇到的第一次主动刷新生效,在整备区里也算——先刷新、再买下拉普兰德,下一次刷新她仍会叠层;同一回合内用已经生效过的拉普兰德合成的精锐,本回合不再生效。
@@ -70,7 +70,7 @@
- **召唤物手动摆放**:把带可部署召唤物的干员放上棋盘后,它的召唤物会进入整备区(赫默的医疗探机、巫恋的诅咒娃娃、凯瑟琳的 2 个支援装置、浊心斯卡蒂的海嗣、伺夜的狼群、缪尔赛思的流形;同一干员的召唤物叠在一格,不占部署位),像干员一样拖到棋盘上并选方向。凯瑟琳的支援装置给**它面朝的那一格**上的干员加屏障,所以要放在干员旁边、朝向干员。**战术家的援军**(伺夜的狼群、缪尔赛思的流形,也就是选战术点)和官方一样**只能放在召唤者的攻击范围内**:拖动时只有这些格子会亮起;之后在原地给战术家换朝向时,不在新攻击范围里的援军会退回整备区(还在范围里的保持不动;整备区和临时整备区都满、援军无处可退时,这次换朝向不生效并提示「整备区已满」)。撤退或出售干员时它的召唤物一起移除;**移动干员到别的格子时(包括把召唤物拖到干员身上交换位置),它已摆放的召唤物会退回整备区**,需要重新摆放。整备区满时退回的召唤物进入临时整备区,休整期结束还没处理的会在下一回合重新回到整备区。
- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
-- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
+- 开战时干员按**逐列从左到右、每列从上到下**的顺序部署(最左一列最先,与阿戈尔吞噬「更靠左和靠上」的顺序相同),召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、最右一列靠下的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放(例外:深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放,余的二技能「厚礼上宾」改为周围两格(技能范围)内有敌人时就释放,这是本作有意与官方不同的地方;深巡一技能仍要受到伤害);执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开,落在她前方 2–4 格的范围(官方「范围2-4」)内的地面敌人和她阻挡的敌人身上。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
diff --git a/docs/SIM.md b/docs/SIM.md
index da3a7c98..5ba0838e 100644
--- a/docs/SIM.md
+++ b/docs/SIM.md
@@ -59,9 +59,10 @@ b.fieldMeta(); // { fieldId, kind, rect, stageId, units: Uni
```
Construction creates every ally unit (not yet deployed) and installs content (kits, then domain modules). The first
-`step()` (or `b.start()`) spawns stage crates, deploys all board units for free — per player the **operators top→bottom
-then left→right** (right boss side: right→left in field columns), **then the summon pieces** the same way (PRTS
-卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物"; a skill's summon — 赫默's 医疗探机, 巫恋's
+`step()` (or `b.start()`) spawns stage crates, deploys all board units for free — per player the **operators by column, the
+left column first, top to bottom within a column** (right boss side: from its own left, the highest field column),
+**then the summon pieces** the same way (PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署。优先部署干员,随后为召唤物", a
+scan down each column — the 阿戈尔 devour's "更靠左和靠上"; row-major until 0.1.3; a skill's summon — 赫默's 医疗探机, 巫恋's
诅咒娃娃 — included, once, free: content/tokens.js `dockSkillSummons` / `releaseSkillSummon`, user playtest #6) — except a
summon piece content flags `deferDeploy` (one its owner's loadout does not make, e.g. 赫默 on S1; with
`SKILL_SUMMON_START_DEPLOY` off also a skill's summon, which then waits on its reserved tile until the skill gives
diff --git a/docs/research/00-INDEX.md b/docs/research/00-INDEX.md
index 7544374d..b1aaccf1 100644
--- a/docs/research/00-INDEX.md
+++ b/docs/research/00-INDEX.md
@@ -82,7 +82,7 @@ The visible chess per tier are 16/17/19/22/19/19. The item slot uses the same ti
| Deploy cap | **8** (`characterLimit`); **9** with 人事部文档. Summons don't use a slot (PRTS token pages of the pool's hand summons: 部署占用数 0; user playtest #6). | DATA/VERIFIED |
| Hand (整备区) | **10 regular + 5 temporary**, shared by operators, items and summon stacks. Full ⇒ no buy or withdraw. Passive overflow goes to temp; **temp blocks Ready**; temp is wiped at the next phase. Board↔hand swap is allowed when full. | VERIFIED |
| Equipment | 2 per operator (board **or** hand). Locked; 3rd = replace, and the old one is destroyed. 2 identical normal items (equipped or not) merge into golden. | VERIFIED |
-| Deployment | Free, at t=0, order **top→bottom then left→right** (right-side boss player mirrored). Ranged units may use melee tiles. | VERIFIED |
+| Deployment | Free, at t=0, order **从上到下>从左到右** = down each column, the columns left to right (right-side boss player mirrored; research 01 §4.3). Ranged units may use melee tiles. | VERIFIED |
| DP | Start **10**, **+1/s**, cap **99**. Used only for auto-redeploy (original tile, respawn timer done, DP ≥ `cost`). | DATA/VERIFIED |
| Skill auto-cast | Default: SP ready + about to attack/heal + an enemy in the **initial** range. ~~`skillTriggerDataList` rows with `skillIndex` 0 = skill 1 only (TANK S1 → on damage: only 古米 and 灰毫 this season)~~ — Superseded by user playtest #6 (DESIGN §20): the class rows apply to **every MANUAL skill** of the class and never to an AUTO skill (PRTS 卫戍协议/帮助 names whole classes; the 阵法术师 row must cover 薄绿's default S2, a phalanx that never attacks with its skill off); a MANUAL skill with its own 技能范围 (not an attack-range change) uses SKILL_RANGE; automatic operations have a 3 s cooldown. charId rows per 03. The TANK row came with 下半 (上半 had none); 深巡 S2, 雷蛇 S2, 号角 S2 / S3 and 灰毫 S1 / S2 deliberately cast with an enemy in range instead (the owner's decision, DESIGN §21.29); 余 S2 too, on its x-1 (SKILL_RANGE, the owner, 2026-10-04, DESIGN §22.10). | VERIFIED (class-row reading superseded; six 重装 skills deviate on purpose) |
| Speed | 2× forced, no manual control | VERIFIED |
diff --git a/docs/research/01-core-data.json b/docs/research/01-core-data.json
index 6c42f7b6..40db5a4d 100644
--- a/docs/research/01-core-data.json
+++ b/docs/research/01-core-data.json
@@ -9562,7 +9562,7 @@
"promotion": {
"eliteGoesTo": "hand; when a consumed copy was deployed, that copy's board position (PRTS 卫戍协议/帮助 '发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)'; user-confirmed after playtest #6)",
"eliteTileAmongSeveralDeployed": {
- "rule": "the consumed deployed copy that deploys first (board row desc, then col asc; mirrored on the right boss half), with its facing",
+ "rule": "the consumed deployed copy that deploys first (board col asc, then row desc — the deployment order since 0.1.3; mirrored on the right boss half), with its facing",
"assumed": true
},
"equipmentReturnsTo": "hand",
diff --git a/docs/research/01-core-rules.md b/docs/research/01-core-rules.md
index 341571d0..eccc2500 100644
--- a/docs/research/01-core-rules.md
+++ b/docs/research/01-core-rules.md
@@ -213,8 +213,8 @@ Boss HP scaling:
Each player fights their own copy of the round level independently. There is no PvP.
- **Deployment:** automatic, paying DP (start 10, +1/s). **Redeploy also costs DP** [COMM].
- **Deploy order:**
- - 上半: left→right, bottom→top.
- - 下半: top→bottom, then left→right. The last-placed unit has the highest taunt.
+ - 上半: left→right, bottom→top (PRTS "从左到右>从下到上": along each row, the rows from the bottom).
+ - 下半: PRTS 卫戍协议/帮助 "按从上到下>从左到右的顺序部署" (from 2026-03-14): a scan down each column (top first), the columns from left to right — the order PRTS gives the 阿戈尔 devour ("从最先部署(更靠左和靠上的)的【阿戈尔】干员开始") and act-2 videos show (BV1rVwjziEQt, BV1pGAPzrEpZ). The remake read it as top row first until 0.1.3. The last-placed unit has the highest taunt.
- **Skills:** fire automatically. Units with summons deploy the summon on first deployment regardless of SP [COMM].
- **Perfect clear (完美作战):** every enemy killed before the time limit, none reached the objective.
- **Bond stacking:**
@@ -843,11 +843,11 @@ Local copies are in `scratchpad/gd/extra/critic/` (`bw_盟约.txt`, `bw_sweep.tx
| new | – | At **combat start, all unfrozen shop contents are cleared** ("进入作战期后,栏上若未处于冻结状态则清空全部物资"). | VERIFIED |
| 4.2 / 8 | bench 10 + temp area | **Hand (手牌区/整备区) = 10 regular + 5 temporary slots**, shared by operators, **items** and hand-deployable **summons**. Several summons of one owner stack into 1 slot; summons of different owners don't stack.
When the regular hand is full, every action that would add a card is refused: buy, and withdraw from the board. Exception: a purchase that completes a 3-merge nets −1 card, so allow it.
Passive gains (merge result, band/trait/item grants, items returned by promotion) overflow into the temp slots. **While temp is non-empty the player cannot start combat.** Temp is wiped when the next phase starts.
A board↔hand swap is allowed even with a full hand. Cards fill hand slots **right→left**. | VERIFIED |
| 5.2 | – | **Items cannot be sold** ("多余的装备无法进行出售,可以销毁"). They can only be destroyed for 0 funds. Operators sell for 1 from board or hand. | VERIFIED |
-| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:
1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.
2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (top→bottom, then left→right — the official deploy order) with its facing; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows. A 突变细胞 carrier is destroyed before its new operator is gained (official footage, DESIGN §21.1), so a merge that operator completes never uses the carrier's tile.
3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.
4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
+| 7 | promotion reward [COMM] | Confirmed (PRTS 帮助, BWIKI, official intro). On acquiring the 3rd copy:
1. The 3rd copy is not created, and the 2 owned copies are destroyed wherever they are.
2. **1 elite goes to the hand — or, when a consumed copy was deployed, to that copy's board position** (PRTS 帮助 "发送1名【精锐】状态的该干员至手牌区(若消耗已部署至作战区的干员,则发送至作战区对应位置)"; the user confirmed it first-hand after playtest #6 — this row used to read "not to a board tile", a misreading of the same sentence). Their equipment returns to the hand ("干员晋级后已配发装备会回收至整备区,需要重新配置"). [ASSUMED] where the text is silent: with two or three deployed copies the elite takes the tile of the one that deploys first (the left column first, top to bottom — the official deploy order, §4.3) with its facing; the copies' summons leave and the elite's own summon card joins the hand (§战斗部署 "如果部署的干员拥有可手动部署的附属召唤物,则该召唤物会立刻加入手牌区"); the deploy count never grows. A 突变细胞 carrier is destroyed before its new operator is gained (official footage, DESIGN §21.1), so a merge that operator completes never uses the carrier's tile.
3. The shop then temporarily shows **3 operators of tier min(shopLevel+1, 6) at price 0**. The official intro shows "II 0 ×3" after a tier-I merge at L1 and "III 0 ×3" at L2.
4. The player takes **1**. The offer cannot be refreshed or frozen and expires at round end. | VERIFIED (elite to the copy's board position: PRTS + the user); the tile among several deployed copies ASSUMED |
| 2 | BAND_CHECK "3 random + reroll" [ASSUMED] | **All unlocked bands are shown.** In co-op, players pick **one at a time in a random order** shown on the left ("以随机顺序进行策略轮选"). Each co-op player may **skip once**. The step lasts 50 s; on timeout the band is 华法琳. **Duplicates are allowed:** the official illustration shows 2 teammates' picks still un-greyed in the grid. [ASSUMED] 12 s per turn inside the 50 s step. | VERIFIED except the per-turn timer |
| 2 | banned bonds "1 core + 1 add-on" [ASSUMED] | Replaced by the verified rule in **A2**. | VERIFIED |
| 1, 12 | one ×0.7/×0.8 multiplier per mode; `aceffect_enemy_*` mapping [ASSUMED] | Replaced by the **per-round table in A3**. | VERIFIED (PRTS, user-sourced) |
-| 4.3 | "automatic, paying DP" | At battle start **all board units deploy for free**, one by one in order: 下半 top→bottom, then left→right; right-side players in the boss round are mirrored.
**DP starts at 10, +1/s, cap 99.** These are the round-level `options`. `initialCost` 0 in `m0X` is the terrain file, not the round level.
DP is only spent on **automatic redeploy**: the unit returns to its original tile when its respawn timer is done, the tile is free, and DP ≥ `cost`.
Precisely (PRTS 卫戍协议/帮助 §作战阶段 单位部署, checked after player report F5): the unit comes back on the tile where its 倒地干员 lies ("原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"). That is its original tile unless it was moved off it (a 突袭 jump, 乌尔比安's anchor). No ally may deploy on that tile ("倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止"). One knocked out on another piece's initial position goes back to its own ("若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置").
Deploy effects do not fire during prep placement. Battle speed is forced to **2×**. | VERIFIED |
+| 4.3 | "automatic, paying DP" | At battle start **all board units deploy for free**, one by one in order: 下半 "从上到下>从左到右" — down each column, the columns left to right (§4.3); right-side players in the boss round are mirrored.
**DP starts at 10, +1/s, cap 99.** These are the round-level `options`. `initialCost` 0 in `m0X` is the terrain file, not the round level.
DP is only spent on **automatic redeploy**: the unit returns to its original tile when its respawn timer is done, the tile is free, and DP ≥ `cost`.
Precisely (PRTS 卫戍协议/帮助 §作战阶段 单位部署, checked after player report F5): the unit comes back on the tile where its 倒地干员 lies ("原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"). That is its original tile unless it was moved off it (a 突袭 jump, 乌尔比安's anchor). No ally may deploy on that tile ("倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止"). One knocked out on another piece's initial position goes back to its own ("若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置").
Deploy effects do not fire during prep placement. Battle speed is forced to **2×**. | VERIFIED |
| 4.3 | leaked/timeout | When `maxPlayTime` ends, the battle stops. Every enemy not killed (still alive, or reached the goal) is "未击倒" and feeds 联防/LP. [ASSUMED] `maxPlayTime` is **game-time** seconds, so the client at 2× shows it ticking twice as fast. Last spawns land at 15/38/73 s against limits of 45/55/115 s, which leaves the time needed to kill walkers. | VERIFIED + ASSUMED unit |
| 3 | solo prep 300 s | 下半 solo has **no timer** on prep or 机变 ("休整期及机变阶段没有时间限制…24小时内随时返回"). Treat `normalPhaseTime` 300 and `specialPhaseTime` 150 as an optional "timed solo" setting. | VERIFIED |
| 4.4 | 联防 partner rule "fewest leaks" | Meaningless, because helpers are perfect by definition. There are **at most 2 helpers**: "所有防卫成功的玩家(最多2名)". Default choice: highest LP, then seat index [ASSUMED]. 1 helper uses `escaped_single` (8 units); 2 helpers use `escaped_multi` (20). | VERIFIED cap |
diff --git a/docs/research/03-operators.md b/docs/research/03-operators.md
index 712f5c55..3b2d4d9c 100644
--- a/docs/research/03-operators.md
+++ b/docs/research/03-operators.md
@@ -1980,7 +1980,7 @@ Consequences for the current pool:
- Withdrawing the owner removes its summons from the board.
- Tokens marked `DEFAULT` in `shopStateTokenDict` are these placeable pieces: 医疗探机, 诅咒娃娃, 狼群, 斯卡蒂的海嗣, 流形. `HIDDEN` tokens exist only in battle.
- [ASSUMED] Summons do **not** use one of the 8 deploy slots, as in the base game. Keep this as a config flag.
-- **Battle start:** every board unit deploys for free, in order **top→bottom, then left→right** (下半). In the boss round the order is mirrored for the right-side player. After that, knocked-out units redeploy automatically on their tile when the respawn timer is done and DP ≥ `cost` (DP starts at 10, +1/s, cap 99).
+- **Battle start:** every board unit deploys for free, in order **从上到下>从左到右** (下半): down each column, the columns left to right (research 01 §4.3). In the boss round the order is mirrored for the right-side player. After that, knocked-out units redeploy automatically on their tile when the respawn timer is done and DP ≥ `cost` (DP starts at 10, +1/s, cap 99).
- **Ranged units may stand on melee tiles** ("所有行动内远程干员可部署在近战位"). Melee units may **not** use ranged-only tiles (HIGH `h`) [ASSUMED standard] — except the branches whose own trait reads 可以放置于远程位 (钩索师 歌蕾蒂娅 / 崖心, 推击手 见行者; PRTS 新人入门 "可部署在高台和地面"). That line is the branch trait, not a 部署效果 like the Y-module 教官's talent "可以额外部署在远程位" (the 帮助: "携带Y模组的教官仍无法部署至高台位"), so it still applies in prep — the remake's reading [ASSUMED], GitHub issue #32 item 4, DESIGN §22.6. Maps: see 05.
### C3. Pool membership per match
diff --git a/docs/research/09-ux-official.md b/docs/research/09-ux-official.md
index 7b937c3d..8058f522 100644
--- a/docs/research/09-ux-official.md
+++ b/docs/research/09-ux-official.md
@@ -67,7 +67,7 @@ Evidence was extracted to the scratchpad `research2/`: guide pages, video frames
| Moving a deployed unit | Drag the unit to any legal tile, **including its own tile**. The same diamond opens. The original stays visible until confirmed; cancelling leaves the unit and its facing unchanged. Re-orienting in place = drag the unit onto its own tile and swipe a new direction. | WIKI + ASSUMED (own-tile case) |
| Tap a deployed unit | Opens the selection diamond (underframe) around the unit and shows its **attack range tiles** on the board (orange). Buttons on the diamond: **撤退** (red runner icon, upper-left; unit returns to the bench) and **出售 +1** (coin badge, upper-right). The left side shows the detail card: portrait, tier, LV, ATK/DEF/RES/block/cost, an "攻击范围" mini-grid, bond icons, and tabs 特质/技能/特性/天赋/装备. You cannot rotate from this menu. | DATA + WIKI |
| Default facing | Every player-initiated placement goes through the wheel, so there is no silent default. Server-created board units (effects such as 外勤医疗 or 预备干员, bots, and back-compat) default to **RIGHT**, toward the gates. | ASSUMED |
-| Persistence | Facing is stored with the position and survives round changes. At combat start units auto-deploy on their tiles **with their facing**, in order **top→bottom, then left→right** (盟约下). | DATA (positions) + COMM (order) |
+| Persistence | Facing is stored with the position and survives round changes. At combat start units auto-deploy on their tiles **with their facing**, in order **从上到下>从左到右** (盟约下): down each column, the columns left to right (research 01 §4.3). | DATA (positions) + COMM (order) |
| Melee, ranged, medic | All use the same wheel. The range preview uses the unit's own rangeGrid (heal range for medics). Blocking and "front" logic use the facing vector. | DATA |
| Summons (tokens) | Each summon is its own bench card ("干员及其召唤物会自动加入整备区"), placed and moved freely with the same wheel. Retreating the owner also retreats its summons. | WIKI |
| Equipment | Dragged onto a unit (board or bench). No direction step. A third item opens the equip-replace dialog (`UseEquipUp { charChessInstId, equipChessInstId, isChangeEquip, unloadInstId }`). | DATA |
diff --git a/public/js/ui/gameLogic.js b/public/js/ui/gameLogic.js
index 93d1d9c8..b5a25cb7 100644
--- a/public/js/ui/gameLogic.js
+++ b/public/js/ui/gameLogic.js
@@ -676,7 +676,8 @@ export function mergeProgress(priv, chessId, getChess = () => null) {
/**
* Where the elite appears when gaining one more normal copy of `chessId` completes a merge now (PRTS 卫戍协议/帮助
* "若消耗已部署至作战区的干员,则发送至作战区对应位置"; mirror of server board.js mergeTile / PlayerState._mergeChess): the
- * board tile of the deployed copy that deploys first (row desc, then col asc) — `{ row, col, dir }` — or null (no
+ * board tile of the deployed copy that deploys first (col asc, then row desc: by column from the left, top to bottom —
+ * Battle.start) — `{ row, col, dir }` — or null (no
* merge — an elite card never merges, see mergeProgress — or no copy is deployed: the elite goes to the hand). The
* copies stand on legal tiles, and the elite is the same operator, so the tile needs no legality check here.
* @param {any} priv
@@ -692,7 +693,7 @@ export function mergeTarget(priv, chessId, getChess = () => null) {
const board = (Array.isArray(priv?.board) ? priv.board : []).filter((p) => p?.kind === 'chess' && !p.golden && Number.isInteger(p.row) && Number.isInteger(p.col)
&& !String(p.id).endsWith('_b') && (p.id === base || getChess(p.id)?.baseId === base));
if (!board.length) return null;
- board.sort((a, b) => b.row - a.row || a.col - b.col);
+ board.sort((a, b) => a.col - b.col || b.row - a.row);
return { row: board[0].row, col: board[0].col, dir: board[0].dir || 'RIGHT' };
}
diff --git a/server/match/PlayerState.js b/server/match/PlayerState.js
index 7f6a8513..8c2b7dc1 100644
--- a/server/match/PlayerState.js
+++ b/server/match/PlayerState.js
@@ -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 ("干员晋级后已配发装备会回收至整备区";
diff --git a/server/match/board.js b/server/match/board.js
index 340eb589..12733cde 100644
--- a/server/match/board.js
+++ b/server/match/board.js
@@ -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;
}
diff --git a/server/sim/Battle.js b/server/sim/Battle.js
index 5b52e929..a518307a 100644
--- a/server/sim/Battle.js
+++ b/server/sim/Battle.js
@@ -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
diff --git a/server/sim/content/kits/tier3.js b/server/sim/content/kits/tier3.js
index 25a6c6ea..0248b628 100644
--- a/server/sim/content/kits/tier3.js
+++ b/server/sim/content/kits/tier3.js
@@ -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'));
diff --git a/server/sim/content/tokens.js b/server/sim/content/tokens.js
index 7d3b1a9c..8bc322b3 100644
--- a/server/sim/content/tokens.js
+++ b/server/sim/content/tokens.js
@@ -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);
diff --git a/test/content/enemies_bosses.test.js b/test/content/enemies_bosses.test.js
index 5f8494de..d10f37bc 100644
--- a/test/content/enemies_bosses.test.js
+++ b/test/content/enemies_bosses.test.js
@@ -321,7 +321,8 @@ for (const key of ['enemy_1203_sfhu', 'enemy_1203_sfhu_2']) {
// 持续时间和效果独立计算"; "普通攻击对目标对及目标周围半径1.0范围内的所有我方单位造成法术普通伤害" (0.1.3: the blast took the attack
// radius 2 and left a steam zone; the attack hit its target only)
test(`${nm(key)}: death spawns 4 茶器; the blast (r 1.25, ATK arts) gives each unit hit one ${tb(key, 'DeadBoom.duration')} s layer of ASPD ${tb(key, 'DeadBoom.attack_speed')}; its attacks splash r 1.0`, () => {
- const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 8 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
+ // t_gun deploys last (column 6, below t_wall): the attack's target, t_wall2 and t_wall within 1.0 of it
+ const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_wall', row: 11, col: 6 }, { chessId: 't_wall2', row: 10, col: 5 }], hooks: ['damaged'], captureNoisy: true });
h.step();
const e = put(h, key, [10, 7]);
h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === e && c.dmg.isAttack), 10);
@@ -1372,7 +1373,8 @@ test(`${nm('enemy_1050_lslime')}: 4 targets, burning DoT on hit, ASPD up below h
// 目标造成物理普通伤害,对溅射目标造成物理溅射伤害") — community report #14: until 0.1.3 the target took a full attack and the 4
// orthogonal neighbours 100 % ATK (no diagonal), DEF down for 3 s
test(`${nm('enemy_1500_skulsr')}: an unblocked attack is a grenade — 26 % ATK on the target and on its 8 surrounding tiles, DEF −50 % for 5 s; blocked a plain hit; ATK up below half`, () => {
- const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 6 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
+ // t_wall3 is the one in range (√5): the target; t_wall2 on its diagonal, t_wall two tiles away
+ const h = arena({ units: [{ chessId: 't_wall', row: 11, col: 3 }, { chessId: 't_wall2', row: 12, col: 4 }, { chessId: 't_wall3', row: 11, col: 5 }], captureNoisy: true, hooks: ['statusApplied', 'damaged'] });
h.step();
const e = put(h, 'enemy_1500_skulsr', [10, 7]);
h.runUntil(() => e.stats.attacks >= 1, 10);
diff --git a/test/content/kits_t6.test.js b/test/content/kits_t6.test.js
index d7773f95..7364a317 100644
--- a/test/content/kits_t6.test.js
+++ b/test/content/kits_t6.test.js
@@ -298,6 +298,10 @@ test('余: S3 fire wall adds burn to allied arts damage crossing it and clears c
enemies: [{ key: 'enemy_dummy', pos: [10, 6] }, { key: 'enemy_shooter', pos: [11, 8] }],
seed: 11,
});
+ // the shooter aims at 优等生 behind the wall: a taunt, since 余 (his column further right) now deploys last — the
+ // latest deployed, the shooter's pick at equal taunt (deployment by column since 0.1.3)
+ h.step();
+ h.b.addBuff(h.unit('chess_char_1_15_a'), { key: 'test:taunt', mods: { taunt: 1 }, persist: true });
const u = h.unit('chess_char_6_03_a');
assert.ok(h.runUntil(() => u.skill.active, 10));
h.run(15);
diff --git a/test/docs-consistency.test.js b/test/docs-consistency.test.js
index c4fb0657..e2c1c2a4 100644
--- a/test/docs-consistency.test.js
+++ b/test/docs-consistency.test.js
@@ -515,7 +515,8 @@ test('user playtest #6 follow-up: a merge consuming a deployed copy puts the eli
assert.equal(promo.eliteTileAmongSeveralDeployed?.assumed, true);
for (const [name, text] of [['research 01', R01], ['00-INDEX', INDEX], ['META', META], ['SIM', SIM], ['PlayerState', PS]]) assert.match(text.replace(/\s+|\/\/\s/g, ''), official, `${name} quotes PRTS`);
assert.match(INDEX, /to \*\*that copy's board position\*\*/);
- assert.match(META, /of several, the one that deploys first \(row desc, then col asc; `board\.js mergeTile`/);
+ // the deployment order is by column since 0.1.3 (Battle.start; DESIGN §23.f4), so is the merge's tile among several copies
+ assert.match(META, /of several, the one that deploys first \(col asc, then row desc; `board\.js mergeTile`/);
assert.ok(!/it takes a freed\s+board tile of a consumed copy only when the hand and temp are both full/.test(META), 'META: the old fallback-only wording is gone');
assert.ok(!/only with the hand and temp both full does it take a/.test(PS), 'PlayerState header: the old fallback-only wording is gone');
assert.match(PLAYING, /精锐会直接出现在\*\*那名干员的位置\*\*/);
diff --git a/test/match/hardening.test.js b/test/match/hardening.test.js
index e087d30c..ef14c9d6 100644
--- a/test/match/hardening.test.js
+++ b/test/match/hardening.test.js
@@ -88,7 +88,7 @@ test('merge with a full hand and a full temp: the elite takes its deployed copy\
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true }, 'a purchase that completes a merge is allowed with a full hand');
const elite = [...ps.board.values()].find((p) => p.id === m.gd.goldenIdOf(id));
assert.ok(elite, 'the elite stands on a consumed copy\'s tile');
- assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the deploy order)');
+ assert.equal(ps.find(elite.uid).key, `${t1[0]},${t1[1]}`, 'the copy that deploys first (legalTileFor walks the top row left to right: on one row, the deploy order)');
assert.equal(elite.poolCopies, 3, 'it holds all three copies');
assert.deepEqual(elite.items.map((x) => x.id).sort(), ['chess_item_1_01_e_a', 'chess_item_2_04_e_a'], 'returned items are kept on it');
checkInvariants(m);
diff --git a/test/match/merge.test.js b/test/match/merge.test.js
index fc52d78c..8ef83b1b 100644
--- a/test/match/merge.test.js
+++ b/test/match/merge.test.js
@@ -116,7 +116,8 @@ test('merge from two board copies with a full hand: the elite takes the copy tha
stock(m, ps, id);
assert.equal(ps.deployCount, 2);
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
- // deploy order = row desc, then col asc: (9,3) before (9,4) — [ASSUMED] the copy the battle deploys first
+ // deploy order = by column from the left (col asc, then row desc; Battle.start): (9,3) before (9,4) — [ASSUMED] the copy
+ // the battle deploys first
const elite = ps.board.get('9,3');
assert.ok(elite && elite.id === DATA.chess[id].goldenId, 'the elite stands on (9,3)');
assert.equal(elite.dir, 'DOWN', 'with the facing of the copy that stood there');
@@ -358,10 +359,10 @@ test('a merge completed during SETTLE (突变细胞) keeps its reward offer for
if (holderTile) assert.ok(!ps.board.has(holderTile), `${label}: the carrier's tile is empty`);
if (deployedTiles.length) {
// PRTS 卫戍协议/帮助: a merge consuming a deployed copy sends the elite to that copy's tile — outside PREP too; of
- // several, the first in deploy order (row desc, col asc)
+ // several, the first in deploy order (col asc, then row desc — by column from the left, Battle.start)
const map = ps.deployMap();
const pos = positionClass(m.gd.chess(elite.id));
- const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
+ const legal = deployedTiles.map((k) => k.split(',').map(Number)).filter(([r, c]) => canPlace(map, pos, r, c)).sort((a, b) => a[1] - b[1] || b[0] - a[0]);
assert.ok(legal.length, `${label}: a legal deployed tile exists`);
assert.equal(loc.area, 'board', `${label}: the elite stands on the board`);
assert.equal(loc.key, `${legal[0][0]},${legal[0][1]}`, `${label}: on the first deployed tile in deploy order`);
diff --git a/test/match/playtest6-elite-to-board.test.js b/test/match/playtest6-elite-to-board.test.js
index 467595e4..9b901f25 100644
--- a/test/match/playtest6-elite-to-board.test.js
+++ b/test/match/playtest6-elite-to-board.test.js
@@ -86,11 +86,12 @@ test('buy: one deployed copy + one hand copy → the elite takes the deployed co
m.dispose();
});
-test('buy: two deployed copies → the copy that deploys first (row desc, then col asc) gives its tile and facing, in either placement order', () => {
+test('buy: two deployed copies → the copy that deploys first (col asc, then row desc: by column from the left) gives its tile and facing, in either placement order', () => {
for (const order of ['first-placed-first', 'first-placed-last']) {
const { m, ps } = prep({ seed: 42 });
const [id] = meleeTier1(m);
- // two legal tiles, the first of them in deploy order
+ // two legal tiles, the first of them in deploy order (legalTileFor walks the top row left to right: on one row, the
+ // deploy order)
const t1 = legalTileFor(m, ps, id);
const t2 = legalTileFor(m, ps, id, new Set([tileKey(...t1)]));
const [early, late] = order === 'first-placed-first' ? [t1, t2] : [t2, t1];
@@ -101,7 +102,7 @@ test('buy: two deployed copies → the copy that deploys first (row desc, then c
stock(m, ps, id);
assert.deepEqual(m.handle('p_0', { t: 'g.buy', slot: 0 }), { ok: true });
const want = mergeTile([t1, t2].map((t) => ({ key: tileKey(...t) })));
- assert.equal(want.key, tileKey(...t1), 'legalTileFor walks the deploy order');
+ assert.equal(want.key, tileKey(...t1), 'the first in deploy order');
const elite = ps.board.get(want.key);
assert.ok(elite && m.gd.isGolden(elite.id), `${order}: the elite on ${want.key}`);
assert.equal(elite.dir, want.key === tileKey(...early) ? 'UP' : 'LEFT', `${order}: the facing of the copy that stood there`);
diff --git a/test/sim/battle.test.js b/test/sim/battle.test.js
index d5d18a18..1710c324 100644
--- a/test/sim/battle.test.js
+++ b/test/sim/battle.test.js
@@ -13,25 +13,27 @@ const approx = (a, b, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps, `${a} ≈
const guard = (o = {}) => chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 300, blockCnt: 2 }, skill: null, ...o });
const walker = (o = {}) => enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1, ...o });
-test('initial deployment order: top→bottom then left→right; right boss side right→left', () => {
+test('initial deployment order: by column from the left, top to bottom within a column (PRTS 从上到下>从左到右 as a scan); the right boss side from its own left', () => {
+ // PRTS 卫戍协议/帮助 §作战阶段 "按从上到下>从左到右的顺序部署" (since 2026-03-14; act 1: "从左到右>从下到上") — down each column,
+ // the columns from the left: the order PRTS gives the 阿戈尔 devour ("更靠左和靠上"). Until 0.1.3: the top row first
const h = makeBattle({
- defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }) } },
- units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }],
+ defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }), d: guard({ id: 'd' }), e: guard({ id: 'e' }) } },
+ units: [{ chessId: 'a', row: 9, col: 5 }, { chessId: 'b', row: 12, col: 7 }, { chessId: 'c', row: 12, col: 3 }, { chessId: 'd', row: 10, col: 4 }, { chessId: 'e', row: 12, col: 5 }],
content: 'none',
});
h.step();
- assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'b', 'd', 'a']);
+ assert.deepEqual(h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.defId), ['c', 'd', 'e', 'a', 'b']);
assert.equal(h.hooksOf('battleStart').length, 1);
const hb = makeBattle({
kind: 'boss',
- defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }) } },
- players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }] }],
+ defs: { chess: { a: guard({ id: 'a' }), b: guard({ id: 'b' }), c: guard({ id: 'c' }) } },
+ players: [{ playerId: 'R1', side: 'R', colOffset: 8, units: [{ uid: 1, chessId: 'a', row: 10, col: 3 }, { uid: 2, chessId: 'b', row: 10, col: 6 }, { uid: 3, chessId: 'c', row: 12, col: 6 }] }],
content: 'none',
});
hb.step();
const order = hb.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.defId, c.unit.tileR, c.unit.tileC, c.unit.facing]);
- // board row 10 → boss row 3; mirrored cols: 3 → 17, 6 → 14; right→left in field coords
- assert.deepEqual(order, [['a', 3, 17, -1], ['b', 3, 14, -1]]);
+ // board rows 10 / 12 → boss rows 3 / 5; mirrored cols: 3 → 17, 6 → 14; its own left first = the highest field column
+ assert.deepEqual(order, [['a', 3, 17, -1], ['c', 5, 14, -1], ['b', 3, 14, -1]]);
});
test('DP: starts at 10, +1/s, cap 99; dead operator redeploys after respawnTime when DP ≥ cost', () => {
diff --git a/test/sim/combat.test.js b/test/sim/combat.test.js
index fac05af5..b7085e9c 100644
--- a/test/sim/combat.test.js
+++ b/test/sim/combat.test.js
@@ -543,7 +543,7 @@ test('ranged enemies attack allies in radius: blocker → taunt → latest deplo
chess: { t_a: guard({ id: 't_a', stats: { maxHp: 1e6, atk: 0 } }), t_b: guard({ id: 't_b', stats: { maxHp: 1e6, atk: 0, tauntLevel: taunt } }) },
enemies: { enemy_caster: enemyRec({ key: 'enemy_caster', hp: 1e6, speed: 1, atk: 100, range: 2.5, dmgType: 'arts' }) },
},
- // deploy order: top→bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
+ // deploy order: one column, top to bottom, so t_b (row 11) deploys before t_a (row 10) ⇒ t_a is the latest deployed
units: [{ chessId: 't_a', row: 10, col: 8 }, { chessId: 't_b', row: 11, col: 8 }],
enemies: [{ key: 'enemy_caster', pos: [10, 10] }], content: 'none', autoFinish: false,
});
diff --git a/test/sim/playtest5-targeting.test.js b/test/sim/playtest5-targeting.test.js
index af48c7b5..b7660e61 100644
--- a/test/sim/playtest5-targeting.test.js
+++ b/test/sim/playtest5-targeting.test.js
@@ -256,16 +256,16 @@ function lastTarget(h, enemy) {
return ev ? h.b.unitById(ev[2]) : null;
}
-test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the bottom row, rightmost (从上到下 > 从左到右)', () => {
- const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
+test('#5 unblocked ranged enemies: highest taunt, then the latest deployed — the rightmost column, its bottom (从上到下 > 从左到右: down each column, the columns left to right)', () => {
+ const units = [[12, 5], [11, 3], [9, 4], [9, 7], [10, 8], [12, 8]].map(([row, col], i) => ({ chessId: 't_wall', row, col, uid: i + 1 }));
const h = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h.step();
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => [c.unit.tileR, c.unit.tileC]);
- assert.deepEqual(order, [[12, 5], [11, 3], [10, 8], [9, 4], [9, 7]]);
+ assert.deepEqual(order, [[11, 3], [9, 4], [12, 5], [9, 7], [12, 8], [10, 8]]);
h.run(1.2);
const e = h.enemy('enemy_t_shooter');
const t = lastTarget(h, e);
- assert.deepEqual([t.tileR, t.tileC], [9, 7], 'the last deployed');
+ assert.deepEqual([t.tileR, t.tileC], [10, 8], 'the last deployed');
// a taunt level beats deployment order
const h2 = arena({ units, enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h2.step();
@@ -279,7 +279,7 @@ test('#5 the initial deployment puts every operator before the summons (PRTS 卫
const h = arena({ units: [{ chessId: 't_wall', row: 12, col: 3, uid: 1 }, { chessId: 't_wall', row: 9, col: 8, uid: 2 }, tok], enemies: [{ key: 'enemy_t_shooter', pos: [10.5, 6] }] });
h.step();
const order = h.hooksOf('deploy').filter((c) => c.initial).map((c) => c.unit.uid);
- assert.deepEqual(order, [1, 2, 3], 'operators top→bottom, then the summon');
+ assert.deepEqual(order, [1, 2, 3], 'the operators (by column from the left), then the summon');
const t = h.unit(3);
assert.ok(t.aggroSeq > h.unit(1).aggroSeq && t.aggroSeq > h.unit(2).aggroSeq);
h.run(1.2);
diff --git a/test/ui/playtest6-elite.test.js b/test/ui/playtest6-elite.test.js
index d4764dd6..edf384c6 100644
--- a/test/ui/playtest6-elite.test.js
+++ b/test/ui/playtest6-elite.test.js
@@ -56,11 +56,13 @@ describe('gameLogic.mergeTarget (mirror of server board.js mergeTile)', () => {
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4, 'UP')], hand: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'UP' });
assert.deepEqual(mergeTarget({ board: [onBoard(A, 10, 4)], temp: [inHand(A, 2)] }, A, getChess), { row: 10, col: 4, dir: 'RIGHT' }, 'temp copies count');
});
- test('two deployed copies → the one that deploys first: row desc, then col asc', () => {
+ test('two deployed copies → the one that deploys first: col asc, then row desc (by column from the left, Battle.start)', () => {
const priv = { board: [onBoard(A, 9, 2, 'DOWN'), onBoard(A, 11, 7, 'LEFT')], hand: [] };
- assert.deepEqual(mergeTarget(priv, A, getChess), { row: 11, col: 7, dir: 'LEFT' });
+ assert.deepEqual(mergeTarget(priv, A, getChess), { row: 9, col: 2, dir: 'DOWN' }, 'the left column, though lower (row-major until 0.1.3)');
const same = { board: [onBoard(A, 10, 8, 'UP'), onBoard(A, 10, 3, 'DOWN')], hand: [] };
assert.deepEqual(mergeTarget(same, A, getChess), { row: 10, col: 3, dir: 'DOWN' });
+ const column = { board: [onBoard(A, 9, 4, 'UP'), onBoard(A, 12, 4, 'LEFT')], hand: [] };
+ assert.deepEqual(mergeTarget(column, A, getChess), { row: 12, col: 4, dir: 'LEFT' }, 'one column: the top first');
});
test('elites and other operators are ignored; 风丸 merges with one deployed copy', () => {
const priv = { board: [{ ...onBoard(golden(A), 12, 2), golden: true }, onBoard('chess_char_1_02_a', 12, 3), onBoard(A, 9, 9)], hand: [inHand(A, 5)] };