feedback1f WV: a knocked-out operator keeps its tile; 突袭 never lands on it
Player report F5 after 0.1.0: "突袭盟约的干员会移动到濒死干员的尸体上". Real. A knocked-out operator left the occupancy map at once and only its home tile stayed reserved. A 突袭 member that fell after its jump lay on the landing tile, which nothing reserved, so the next member's jump (same target, same scoring) landed on it; and a member's timed redeploy came back on its home where a teammate that had jumped there lay. Real matches (six real 突袭 members, 战场#07 / #08, R6 / R9 / R12, 96 battles): 6 placements on a body (3 jumps, 3 home redeploys) before, 0 of 526 jumps after. Official (PRTS 卫戍协议/帮助, 作战阶段 / 单位部署): - "干员退场后…原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员 并自动部署至该位置" - "倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止" - "若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其 自身的初始位置" Now (sim/Battle.js): - A knocked-out operator lies on `unit.body`: the tile it fell on, or its own home when it fell on another board piece's home and its home is free (one attempt at the knock-out, assumed). x / y / tileR / tileC keep where it fell, so the kill / death handlers still act there. - Every redeploy comes back there (restTile): the timed one, 不屈's and 阿戈尔's. A retreated operator still comes back on its home. - downOn(r, c): _deploy (redeploys, the 突袭 landing, summons), spawnDevice and relocate refuse the tile for every ally of the field; isReservedTile reports it, so every automatic picker skips it (突袭 raidTile, tactical points, summon / device pickers, 维娜's lions). The kit helpers that kept their own home-tile loop (tier1 summonTileFree, tier3 / tier4 freeTile) go through isReservedTile. - b.snap `down` entries gain the body tile: [id, respawnAt, respawnTime, state, row, col]. DOWN_STATE.WAIT_TILE becomes a safeguard only. Tests: test/sim/feedback1f-downed-tile.test.js (real Match / PlayerState / BattleSpec runs with real 突袭 chess, the 突袭 jump, the engine refusals, the return home); downelem and bonds_addon follow the official rule.
This commit is contained in:
1 parent
60ed4fdab9
commit
5a3d562b75
12 files changed
+374
-71
No files matched your search
+1
-1
@@ -18,7 +18,7 @@
|
||||
| 选择策略 | 40 名策略(分队长)决定**初始目标生命值(20–45)**和一条永久规则。同盟:随机顺序轮流选,**每人 30 秒**,右上角和「决策中」显示同一个倒计时;AI 队友立即选完;队友已选的不能重复,每人可**跳过一次**(排到最后);超时自动选你**当前选中**的策略(已被队友选走则选华法琳)。独立:自由选择、不限时;同盟模拟只有你一名玩家时也不限时。 |
|
||||
| 机变 | 部分回合开始前出现:同盟共 6 张卡轮流各取 1 张(首位 30 秒,其余 16 秒;只有你一名玩家时不限时),独立 3 选 1。卡片包括装备、资金、干员、盟约层数、悬赏等;**悬赏决策**为你的作战添加悬赏敌人,卡上用蓝字标出持续多久:「下场作战」/「接下来两场作战」的只出现一场或两场,之后不再出现;官方的「多轮悬赏」(原文写「之后的每场作战」)在这里也按「接下来两场作战」处理,只出现两场。击倒它的人(联防中的队友也算)获得资金;右侧效果栏会写「还剩 N 场作战」。每张卡左上是图标和名称,下面写着完整效果(与详情卡相同的官方描述,手机上也显示);已被选走的卡变暗,右上角显示选走它的玩家头像(悬停可看名字)。选卡要**点两次**:第一次只选中(金框 +「确认选择 · 再次点击」),再点同一张或点右上的「确认选择」才生效;点别的卡换选,按 Esc 或点空白处取消。 |
|
||||
| 休整期 | 购买、摆阵、配装备、冻结 / 刷新、升级调度中心;同盟有倒计时(65–215 秒,随回合增加);独立模拟、以及只有你一名玩家(只带 AI 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在原地**(倒地姿势、略微变灰),头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够,红色「!」表示原位被占。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**从上到下、从左到右**的顺序自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下);倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
|
||||
| 联防 | 见第 5 节。 |
|
||||
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
|
||||
|
||||
|
||||
+21
-7
@@ -77,6 +77,17 @@ field — units deploy one after another with a fixed delay from the battle star
|
||||
enemy tile index → allies (skill tick, attack) → projectiles → auto-redeploys → boss pool sync → `tick` hook →
|
||||
release hooks/timers of units removed this tick (§1.4) → `time += TICK` → end checks.
|
||||
|
||||
**Knocked-out operators** (PRTS 卫戍协议/帮助 §作战阶段 单位部署; player report F5 after 0.1.0): "干员退场后…原地留下一个
|
||||
“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置" — an operator knocked out (`isDown`) lies on the tile it
|
||||
fell on (`unit.body`, `Battle._layBody`; b.snap `down` carries it) and its redeploy — the timed one (`_checkRedeploys`:
|
||||
timer done, tile free, DP ≥ cost), 不屈's, 阿戈尔's — comes back there (`restTile`); "若干员被击倒的位置为其他干员或召唤物的
|
||||
初始位置,则在被击倒后,尝试返回其自身的初始位置": one that fell on another board piece's home (a 突袭 member after its jump,
|
||||
乌尔比安 off his anchor) lies on its own home instead when that is free (else it stays [ASSUMED: one attempt]); x / y /
|
||||
tileR / tileC keep where it fell for the `kill` / `death` handlers. "倒地干员所在地块视为可部署,但所有我方单位在此处的部署
|
||||
行为将被阻止": `downOn(r, c)` — `_deploy` (redeploys, the 突袭 landing, summons), `spawnDevice` and `relocate` refuse that
|
||||
tile and `isReservedTile` reports it, so every automatic picker skips it. A withdrawn operator that is not down (a
|
||||
retreat) comes back on its home tile.
|
||||
|
||||
### 1.1 Coordinates (PlayerBattleInput units)
|
||||
|
||||
`units[].row/col` are **board coordinates** (rows 9–12, cols 2–10) unless `abs: true` (or player
|
||||
@@ -277,7 +288,8 @@ Guarantees content can rely on (pinned by `test/sim/robustness.test.js`):
|
||||
unit rows/cols are coerced to integers (junk ⇒ unit skipped); `spawnEnemy` mods must be finite ≥ 0 (else ×1), the spawn
|
||||
point is clamped into the rect; `spawnDevice`/`spawnToken`/`relocate` accept only integer in-rect free tiles
|
||||
(`spawnToken` returns null and leaves nothing behind when the tile is busy, `relocate` only moves living deployed allies,
|
||||
`redeploy {tile}` refuses a non-integer / out-of-rect / occupied tile without falling back to the home tile);
|
||||
`redeploy {tile}` refuses a non-integer / out-of-rect / occupied tile — or one a knocked-out operator lies on —
|
||||
without falling back to the home tile);
|
||||
non-finite `hp`/`stats`/`duration`/`extend`/`addAmmo`/`addCharge`/`addLayers`/`addCoins`/`spCostMul` values are ignored;
|
||||
aggregated stats that overflow (stacked `*Mul`) fall back to base values; a revive written by a `kill` handler is
|
||||
clamped to max HP (NaN/≤ 0 ⇒ the unit dies with hp 0); projectiles with non-finite coordinates are re-aimed at their
|
||||
@@ -298,7 +310,7 @@ Guarantees content can rely on (pinned by `test/sim/robustness.test.js`):
|
||||
|
||||
`Unit` fields: `id, side ('ally'|'enemy'), kind ('op'|'token'|'enemy'|'device'), defId, def (normalised), name, ownerId
|
||||
(playerId), uid (board piece), ownerUnit (tokens), x, y (floats; x = col, y = row), tileR, tileC (allies), homeR, homeC
|
||||
(board tile: auto-redeploys land there), dir ('UP'|'RIGHT'|'DOWN'|'LEFT'; getters `fwd` = forward vector, `facing` =
|
||||
(board tile: a retreated operator's redeploys land there; a knocked-out one's on its `body` tile, §1), dir ('UP'|'RIGHT'|'DOWN'|'LEFT'; getters `fwd` = forward vector, `facing` =
|
||||
horizontal sign ±1 for sprites only), hp, alive, deployed, removed, hidden, base {…}, buffs[],
|
||||
rangeKeys / rangeKeySet (current range, absolute tile keys `r × 21 + c`), baseRangeKeys (initial range, §7.1),
|
||||
extraRangeKeys (content extra targets, `battle.setExtraRange`), blocking[] (allies), blockedBy (enemies), motion
|
||||
@@ -560,8 +572,9 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
| `spawnToken(ownerUnit | playerId, tokenId, row, col, { def, stats, hp, duration, untargetable, dir, kit, force, anySource })` | field tiles; def from data/tokens.json `variants[ownerChessId]` for the owner unit's selected skill / module (`tokenDef`); `dir` defaults to the owner unit's (else the player's: RIGHT, mirrored side LEFT; a legacy `facing` ±1 is still read); returns the token or null (tile busy; or the owner runs a **non-default** skill that does not produce the token — `producesToken` — unless `anySource`: kit install hooks written for the default skill run under every skill). `spawnDevice(key, row, col, { …, dir })` likewise |
|
||||
| `tokenDef(tokenId, ownerUnit | chessId)`, `producesToken(ownerUnit, tokenId)` | the token def a summon of that owner gets — `getToken(id, owner.defId, owner.def.loadout)`, exact even when two players of one field give the same chess different loadouts (prefer it over an id-only `battle.data.getToken(id, unit.defId)` for summon stats / blackboards); whether the owner's loadout makes the token (DATA.md §14 `sources` has 'skill' or 'talent'; true when the data does not tell: no own variant, player-owned summons) |
|
||||
| `spawnEnemy(enemyKey, { routeIndex, route, pos, mods, tag, sourcePlayerId, ownerPlayerId, bounty, countInTotal, def })` | returns the enemy, or null past `MAX_ALIVE_ENEMIES`; `def` = inline record (enemies.json shape or normalised) for keys missing from data |
|
||||
| `spawnDevice(key, row, col, { hp, obstacle, blockCnt, name, def, res, atk, bat, aspd })`, `setObstacle(r, c, on)` | obstacles re-path enemies; spawnDevice returns null outside the rect or on a living unit |
|
||||
| `isReservedTile(r, c)` | true when a living unit stands there or it is the home tile of an ally piece that has not deployed yet / waits to redeploy — summon tile pickers must skip these (`findTacticalPoint` does) |
|
||||
| `spawnDevice(key, row, col, { hp, obstacle, blockCnt, name, def, res, atk, bat, aspd })`, `setObstacle(r, c, on)` | obstacles re-path enemies; spawnDevice returns null outside the rect, on a living unit or on a knocked-out operator's tile |
|
||||
| `isReservedTile(r, c)` | true when a living unit stands there or it is the rest tile (`restTile`) of an ally piece that has not deployed yet / waits to redeploy — the tile a knocked-out operator lies on, else the home tile — every automatic picker (the 突袭 landing tile, tactical points, summon / device tiles) must skip these (`findTacticalPoint` does) |
|
||||
| `downOn(r, c, except?)`, `restTile(u)` | the knocked-out operator lying on a tile (no ally deploys / moves there: `_deploy`, `spawnDevice`, `relocate`); the tile a withdrawn ally comes back on — its `body` tile when knocked out, else its home (§1) |
|
||||
| `addProjectile({ from, target | to:{x,y}, speed, onHit(ctx), visual, source, hitDead })` | homing; fizzles if the target dies unless `hitDead` |
|
||||
| `allySelectable(ally, by)`, `alliesFor(by, ownerId?)` | may an ability of ally `by` select `ally` — never a 孤立 unit (炎佑, 从不混淆的方向) but `by` itself (PRTS 选择器: "若掩码中孤立为1,且选择器的阵营与目标为友好关系,则不可选中"); `allies()` without the 孤立 ones — content picks ally targets (buffs, auras, 全场 talents, heal picks) through these, `alliesInGrid` too |
|
||||
| `unitsInGrid(unit, grid, {side, extend})`, `alliesInGrid(unit)`, `enemiesInRadius(x, y, r, centre?)`, `alliesInRadius(x, y, r, ownerId?)` | grid offsets are relative to facing RIGHT, rotated by `unit.dir`; enemies by their body (§2 hit areas: a huge enemy on every tile it occupies / within `r` of its rectangle; `centre` = splash around a target, a 中点判定 by position) |
|
||||
@@ -571,7 +584,7 @@ or guard with a per-unit flag while dealing it. When the guard trips, the logged
|
||||
| `getPlayer(playerId)` | `{ playerId, seat, side, colOffset, mirror, dir (default unit direction: RIGHT, mirrored side LEFT), facing (its sign), bonds (live copy, layers updated by addLayers), bandId, playerEffects, lpForBoss, dp, units }` |
|
||||
| `mapTile(ps, row, col, abs?)` / `mapDir(ps, dir, abs?)` | board → field tile / direction of a player (the FA right-side mirror) |
|
||||
| `addDp(playerId, n)`, `retreat(unit, {reason, permanent})`, `relocate(unit, r, c)` | |
|
||||
| `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); `tile: [r, c]` lands on that tile once (home unchanged — later redeploys use the board tile; refused when off-rect or occupied, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 revive in place |
|
||||
| `redeploy(unit, { free=true, tile, keepSp })` | immediate (re)deployment of a dead/retreated ally (full HP, `deploy {initial:false}`); `free: false` pays `base.cost` DP (refused without it); without `tile` it lands on the unit's rest tile (`restTile`: where a knocked-out operator lies, else home); `tile: [r, c]` lands on that tile once (home unchanged; refused when off-rect, occupied or a knocked-out operator's tile, no fallback); `keepSp` keeps SP/charges (保留技力), restored before `deploy` fires — 突袭 raids, 阿戈尔 / 不屈 revives where the unit lies |
|
||||
| `refreshRange(unit)`, `setExtraRange(unit, keys)`, `rangeChanged(unit)` | rebuild the ranges after changing `unit.rangeGrid` (流形 copies); extra targetable tiles (absolute keys; merged into every later rebuild until set again; `null` clears; never in `baseRangeKeys`): 蕾缪安 wanted, 维娜 S3 |
|
||||
| `push(enemy, force, {from, dir, fixed, fixedAngle, inward, effect})`, `pull(enemy, force, {to, center, stop})`, `pullToFront(enemy, unit, force)`, `forceLevel(enemy, force)`, `pushDistance(enemy, force, {effect})` | the official 位移 (PRTS 游戏数据基础 §重量公式 / 推与拉; user playtest #6 item 14): 受力等级 = 力度 (微小力 −1, 小力 0, 中力 1, 较大力 2, 大力 3 …) − current 重量等级 (massLevel, 失重 counts). Push distance per level (`constants.js PUSH_TILES`, PRTS 推与拉's 弹道 column): ≤ −3 → 0, −2 → 0.12, −1 → 0.44, 0 → 1.7, 1 → 2.14, 2 → 2.96, ≥ 3 → 3.53 tiles; `effect` = a 特效 push (`PUSH_TILES_EFFECT`: −2 → 0.085, −1 → 0.374, 0 → 1.562, 1 → 1.987, 2 → 2.773, ≥ 3 → 3.331 — 见行者 S1 / S2, `PUSH_EFFECT_SKILLS`; every other pusher uses the 弹道 column [ASSUMED]); radial (away from `from`) unless `dir` (directional: 推击手, 朝部署方向 — > 45° off or < 0.25 tile ⇒ radial and level −2; `fixed` waives both (圣聆初雪 S1), `fixedAngle` only the angle (见行者 S2)); `inward` = a push towards `from` (薄绿 S2), stopping at the 急停 radius. Pull: level ≥ 0 → to `to` / the 急停 radius around `center`, −1 → 35 % of the way, −2 → 0.03, ≤ −3 → 0; `pullToFront` aims at the 拉力起点 0.5 tile ahead of the unit with the 急停 radius 0.6708 around it (never moves an enemy the unit itself blocks — nor does a pull towards an ally's centre, e.g. the 流形 S3 pulse) |
|
||||
| `displace(enemy, {x, y}, tiles)` | the raw mover behind push / pull: along passable tiles (by `motion`: FLY in the rect, else ground-passable), no weight rule; nothing for 失衡免疫 (`noDisplace`: 近地悬浮, 浮空) and leaders; unblocks + re-paths |
|
||||
@@ -947,10 +960,11 @@ Unknown subprofessions fall back to the profession default (test `professions.te
|
||||
|
||||
- `snapshot()` → `{ fieldId, t, units: [[id, x, y, hp, maxHp, sp, spMax, flags, anim]], dp, killed, total, dps?, boss?, down?, elem? }`.
|
||||
`sp/spMax` show remaining duration/ammo as a draining bar while a timed skill is active. Units in DIE state stay 0.8 s.
|
||||
`down: [[id, respawnAt, respawnTime, state]]` (only when non-empty) = knocked-out operators waiting to redeploy
|
||||
`down: [[id, respawnAt, respawnTime, state, row, col]]` (only when non-empty) = knocked-out operators waiting to redeploy
|
||||
(`Battle.isDown(u)`: reason `'killed'` or `FORCED_EXIT` (§1.1 carryState `down`), deployed at least once, a finite
|
||||
respawn timer; `state` = constants.js
|
||||
`DOWN_STATE`: 0 counting, 1 timer done / DP short, 2 timer done / own tile taken); `elem: [[id, element, fill,
|
||||
`DOWN_STATE`: 0 counting, 1 timer done / DP short, 2 timer done / its tile taken — a safeguard: no ally deploys on a
|
||||
knocked-out operator's tile, §1; `row, col` = the tile it lies on and comes back on, `unit.body`); `elem: [[id, element, fill,
|
||||
cooldownEnd, cooldown]]` (only when non-empty) = `elementView` of every unit with a gauge or a running 爆发冷却 (§3).
|
||||
`fieldMeta()` lists the knocked-out operators too (a client joining mid-battle shows them; DESIGN §18.3).
|
||||
- `drainEvents()` tuples: `['spawn', UnitInfo]` (first appearance), `['deploy', id]` (every (re)deploy), `['atk', src, tgt, projKind]`
|
||||
|
||||
+91
-20
@@ -10,6 +10,9 @@
|
||||
// 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`.
|
||||
// A knocked-out operator 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 卫戍协议/帮助 §作战阶段 单位部署;
|
||||
// `_layBody`, `downOn`, `restTile`, `isReservedTile`; docs/SIM.md §1).
|
||||
// Tick order: scheduled callbacks → spawns → DP → buffs → enemies (attack, move, block) → enemy index →
|
||||
// allies (skill tick, attack) → projectiles → redeploys → boss sync → `tick` hook → release hooks of removed units →
|
||||
// time += TICK → end checks. A forceEnd() requested mid-step ends the step after the current phase (docs/SIM.md §1.4).
|
||||
@@ -854,11 +857,14 @@ export class Battle {
|
||||
const k = R0 * COLS + C0;
|
||||
const occ = this._occ[k];
|
||||
if (occ && occ !== u && occ.alive && occ.deployed) { this.log(`tile ${R0},${C0} occupied; ${u} not deployed`); return false; }
|
||||
// "倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止" (PRTS 卫戍协议/帮助 §作战阶段 单位部署)
|
||||
if (this.downOn(R0, C0, u)) { this.log(`a knocked-out operator lies on ${R0},${C0}; ${u} not deployed`); return false; }
|
||||
const first = u.deploySeq === 0;
|
||||
u.alive = true;
|
||||
u.deployed = true;
|
||||
u.removed = false;
|
||||
u.hidden = false;
|
||||
u.body = null;
|
||||
u.x = C0; u.y = R0; u.tileR = R0; u.tileC = C0;
|
||||
u.blocking = [];
|
||||
u.deploySeq = ++this._deploySeq;
|
||||
@@ -940,6 +946,7 @@ export class Battle {
|
||||
if (reason === 'killed') { const pp = this._pp(unit.ownerId); if (pp) pp.deaths++; }
|
||||
const mul = unit.persist.redeployMul * unit.s.redeployMul;
|
||||
unit.respawnAt = this.time + Math.max(0, unit.base.respawnTime * mul);
|
||||
if (this.isDown(unit)) this._layBody(unit); // before `die` / `death`: the body's tile is final for them
|
||||
} else {
|
||||
unit.removed = true;
|
||||
}
|
||||
@@ -993,10 +1000,11 @@ export class Battle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately redeploy a dead (or retreated) ally. opts:
|
||||
* Immediately redeploy a dead (or retreated) ally on its rest tile (restTile: where a knocked-out operator lies, else
|
||||
* its home tile). opts:
|
||||
* free=true no DP cost (false: pays `base.cost`, refused when the player lacks the DP)
|
||||
* tile=[r,c] land on this in-rect tile instead of the home tile (the home stays the tile of later redeploys);
|
||||
* refused (false) when the tile is outside the rect or a living unit stands there — no fallback
|
||||
* tile=[r,c] land on this in-rect tile instead (the home stays the board tile); refused (false) when the tile is
|
||||
* outside the rect, a living unit stands there or another knocked-out operator lies there — no fallback
|
||||
* keepSp keep the SP / charges the unit had (保留技力): restored before the `deploy` hook fires
|
||||
* Returns true when the unit was deployed (full HP, `deploy {initial:false}` fires).
|
||||
*/
|
||||
@@ -1006,10 +1014,14 @@ export class Battle {
|
||||
if (tile != null) {
|
||||
if (!Array.isArray(tile) || !Number.isInteger(tile[0]) || !Number.isInteger(tile[1]) || !this.grid.inRect(tile[0], tile[1])) return false;
|
||||
at = [tile[0], tile[1]];
|
||||
} else {
|
||||
const rest = this.restTile(unit);
|
||||
if (rest[0] !== unit.homeR || rest[1] !== unit.homeC) at = rest;
|
||||
}
|
||||
const k = at ? at[0] * COLS + at[1] : unit.homeR * COLS + unit.homeC;
|
||||
const occ = this._occ[k];
|
||||
if (occ && occ.alive && occ !== unit) return false;
|
||||
if (at && this.downOn(at[0], at[1], unit)) return false;
|
||||
const ps = this.getPlayer(unit.ownerId);
|
||||
const cost = unit.base.cost;
|
||||
let paid = 0;
|
||||
@@ -1025,18 +1037,24 @@ export class Battle {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic redeploys (DESIGN §5.5): a withdrawn operator whose timer is done comes back on its rest tile — a knocked-out
|
||||
* one where it lies ("满足再部署条件时,移除场上的该倒地干员并自动部署至该位置", PRTS 卫戍协议/帮助) — when that tile is
|
||||
* free and its player has the DP.
|
||||
*/
|
||||
_checkRedeploys() {
|
||||
for (const u of this.allyUnits) {
|
||||
if (u.alive || u.removed || u.kind !== 'op') continue;
|
||||
if (this.time + 1e-9 < u.respawnAt) continue;
|
||||
const k = u.homeR * COLS + u.homeC;
|
||||
const occ = this._occ[k];
|
||||
const [r, c] = this.restTile(u);
|
||||
const occ = this._occ[r * COLS + c];
|
||||
if (occ && occ.alive && occ !== u) continue;
|
||||
if (this.downOn(r, c, u)) continue;
|
||||
const ps = this.getPlayer(u.ownerId);
|
||||
const cost = u.base.cost;
|
||||
if (!ps || ps.dp + 1e-9 < cost) continue;
|
||||
ps.dp = Math.max(0, ps.dp - cost);
|
||||
this._deploy(u, { initial: false });
|
||||
this._deploy(u, { initial: false, tile: r === u.homeR && c === u.homeC ? null : [r, c] });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1709,10 +1727,12 @@ export class Battle {
|
||||
* the tile impassable.
|
||||
*/
|
||||
spawnDevice(key, row, col, opts = {}) {
|
||||
// Devices are ally-side units on a field tile: never outside the rect, never on top of a living unit.
|
||||
// Devices are ally-side units on a field tile: never outside the rect, never on top of a living unit or of a
|
||||
// knocked-out operator (downOn).
|
||||
if (!Number.isInteger(row) || !Number.isInteger(col) || !this.grid.inRect(row, col)) return null;
|
||||
const occ = this._occ[row * COLS + col];
|
||||
if (occ && occ.alive) return null;
|
||||
if (this.downOn(row, col)) return null;
|
||||
const hp = fin(opts.hp, 100);
|
||||
const bat = fin(opts.bat, 1), aspd = fin(opts.aspd, 100);
|
||||
const def = {
|
||||
@@ -1771,7 +1791,7 @@ export class Battle {
|
||||
|
||||
addProjectile(p) { return this.projectiles.add(p); }
|
||||
|
||||
/** Move an ally to another tile (keeps state). */
|
||||
/** Move an ally to another tile (keeps state); never onto a living unit or a knocked-out operator (downOn). */
|
||||
relocate(unit, r, c) {
|
||||
// only a living, deployed ally moves, and only onto an integer tile of this field (a dead unit left in _occ
|
||||
// would later "block" from a tile it no longer stands on once it redeploys at home)
|
||||
@@ -1779,6 +1799,7 @@ export class Battle {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || !this.grid.inRect(r, c)) return false;
|
||||
const k = r * COLS + c;
|
||||
if (this._occ[k] && this._occ[k] !== unit && this._occ[k].alive) return false;
|
||||
if (this.downOn(r, c)) return false;
|
||||
const ok = unit.tileR * COLS + unit.tileC;
|
||||
if (this._occ[ok] === unit) this._occ[ok] = null;
|
||||
this.releaseBlocked(unit);
|
||||
@@ -1911,19 +1932,64 @@ export class Battle {
|
||||
}
|
||||
|
||||
/**
|
||||
* A tile a summon must not take: a living unit stands on it, or it is the home tile of an ally piece that has not
|
||||
* deployed yet or waits to redeploy (a token parked there would keep that unit off the field for the whole battle).
|
||||
* A tile no automatic placement may take (the 突袭 landing tile, tactical points, summon / device tiles): a living
|
||||
* unit stands on it, or it is the rest tile (restTile) of an ally piece that has not deployed yet or waits to
|
||||
* redeploy — the tile a knocked-out operator lies on ("倒地干员所在地块…所有我方单位在此处的部署行为将被阻止", PRTS
|
||||
* 卫戍协议/帮助 §作战阶段 单位部署), else its home tile (a token parked there would keep that unit off the field).
|
||||
*/
|
||||
isReservedTile(r, c) {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || r < 0 || r >= ROWS || c < 0 || c >= COLS) return true;
|
||||
const u = this._occ[r * COLS + c];
|
||||
if (u && u.alive) return true;
|
||||
for (const a of this.allyUnits) {
|
||||
if (!a.alive && !a.removed && a.homeR === r && a.homeC === c && (a.kind === 'op' || a.kind === 'token')) return true;
|
||||
if (a.alive || a.removed || (a.kind !== 'op' && a.kind !== 'token')) continue;
|
||||
const [tr, tc] = this.restTile(a);
|
||||
if (tr === r && tc === c) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The knocked-out operator (isDown) lying on (r, c), other than `except`, or null. Official (PRTS 卫戍协议/帮助 §作战阶段
|
||||
* 单位部署): "干员退场后…原地留下一个“倒地干员”…满足再部署条件时,移除场上的该倒地干员并自动部署至该位置" and "倒地干员所在
|
||||
* 地块视为可部署,但所有我方单位在此处的部署行为将被阻止" — its tile (`body`, _layBody) takes no other ally: `_deploy`
|
||||
* (initial deploys, redeploys, the 突袭 landing, summons), `spawnDevice` and `relocate` refuse it.
|
||||
*/
|
||||
downOn(r, c, except = null) {
|
||||
for (const a of this.allyUnits) {
|
||||
if (a === except || !this.isDown(a)) continue;
|
||||
const [br, bc] = a.body ?? [a.tileR, a.tileC];
|
||||
if (br === r && bc === c) return a;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The tile a withdrawn ally comes back on (redeploy, _checkRedeploys): a knocked-out operator's body tile — where it
|
||||
* fell, or its home (_layBody) — else its home tile.
|
||||
*/
|
||||
restTile(u) {
|
||||
return this.isDown(u) ? (u.body ? [u.body[0], u.body[1]] : [u.tileR, u.tileC]) : [u.homeR, u.homeC];
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a knocked-out operator lies (it redeploys there): the tile it fell on, except — "若干员被击倒的位置为其他干员或
|
||||
* 召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置" (PRTS 卫戍协议/帮助) — a tile that is another board piece's
|
||||
* home (a piece with a board uid: the 初始位置 is the prep placement), where it goes back to its own home tile when
|
||||
* that is in the rect and free (isReservedTile: no living unit, no other body, no other waiting piece's tile). It
|
||||
* stays where it fell when its home is taken [ASSUMED: one attempt, at the knock-out]. Only an operator moved off its
|
||||
* board tile — a 突袭 jump, 乌尔比安's anchor — can fall elsewhere. Sets `u.body` (b.snap `down` carries it); x / y /
|
||||
* tileR / tileC keep where it fell, so the `kill` / `death` handlers (被击倒时 effects) still act there.
|
||||
*/
|
||||
_layBody(u) {
|
||||
const r = u.tileR, c = u.tileC, hr = u.homeR, hc = u.homeC;
|
||||
u.body = [r, c];
|
||||
if (r === hr && c === hc) return;
|
||||
if (!this.allyUnits.some((a) => a !== u && a.uid != null && !a.removed && (a.kind === 'op' || a.kind === 'token') && a.homeR === r && a.homeC === c)) return;
|
||||
if (!this.grid.inRect(hr, hc) || this.isReservedTile(hr, hc)) return;
|
||||
u.body = [hr, hc];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tactical point (战术点) for tactician reinforcements: a free (isReservedTile) walkable ground tile of the unit's
|
||||
* initial range (baseRangeKeys), on an enemy ground path first (groundPathTiles — where a player would put the
|
||||
@@ -2103,8 +2169,9 @@ export class Battle {
|
||||
|
||||
/**
|
||||
* Compact full snapshot of this field (DESIGN §8.2 b.snap), plus (only when non-empty):
|
||||
* down: [[id, respawnAt, respawnTime, state]] — knocked-out operators waiting to redeploy on their own tile
|
||||
* (isDown): the game time their respawn timer ends, its length (s) and constants.js DOWN_STATE;
|
||||
* down: [[id, respawnAt, respawnTime, state, row, col]] — knocked-out operators waiting to redeploy (isDown): the
|
||||
* game time their respawn timer ends, its length (s), constants.js DOWN_STATE and the tile they lie on (and
|
||||
* come back on: _layBody — where they fell, or their home);
|
||||
* elem: [[id, element, fill, cooldownEnd, cooldown]] — the element gauge each unit shows (damage.js elementView).
|
||||
*/
|
||||
snapshot() {
|
||||
@@ -2125,7 +2192,7 @@ export class Battle {
|
||||
let down = null;
|
||||
for (const u of this.allyUnits) {
|
||||
if (!this.isDown(u)) continue;
|
||||
(down || (down = [])).push([u.id, r2(u.respawnAt), r2(Math.max(0, u.respawnAt - u.deathAt)), this._downState(u)]);
|
||||
(down || (down = [])).push([u.id, r2(u.respawnAt), r2(Math.max(0, u.respawnAt - u.deathAt)), this._downState(u), ...this.restTile(u)]);
|
||||
}
|
||||
if (down) snap.down = down;
|
||||
let elem = null;
|
||||
@@ -2139,20 +2206,24 @@ export class Battle {
|
||||
}
|
||||
|
||||
/**
|
||||
* A knocked-out operator waiting to redeploy on its own tile (DESIGN §5.5: after its respawn time, when the tile is
|
||||
* free and DP ≥ cost): killed — or entering the battle knocked out (FORCED_EXIT, 联防) — not withdrawn, not removed
|
||||
* for good, after it was deployed. The client keeps its model on the field knocked down with a redeploy countdown
|
||||
* (b.snap `down`, render/units.js); summons, devices and enemies simply leave.
|
||||
* A knocked-out operator waiting to redeploy on the tile it lies on (DESIGN §5.5: after its respawn time, when the
|
||||
* tile is free and DP ≥ cost; _layBody, downOn): killed — or entering the battle knocked out (FORCED_EXIT, 联防) —
|
||||
* not withdrawn, not removed for good, after it was deployed. The client keeps its model on that tile knocked down
|
||||
* with a redeploy countdown (b.snap `down`, render/units.js); summons, devices and enemies simply leave.
|
||||
*/
|
||||
isDown(u) {
|
||||
return !!u && u.side === 'ally' && u.kind === 'op' && !u.alive && !u.removed && (u.removeReason === 'killed' || u.removeReason === FORCED_EXIT)
|
||||
&& u.deploySeq > 0 && Number.isFinite(u.respawnAt);
|
||||
}
|
||||
|
||||
/** constants.js DOWN_STATE of a down operator: its timer runs, or it waits for its tile / the DP (_checkRedeploys). */
|
||||
/**
|
||||
* constants.js DOWN_STATE of a down operator: its timer runs, or it waits for its tile / the DP (_checkRedeploys). No
|
||||
* ally may take the tile it lies on (downOn), so WAIT_TILE is a safeguard only.
|
||||
*/
|
||||
_downState(u) {
|
||||
if (this.time + 1e-9 < u.respawnAt) return DOWN_STATE.COUNTING;
|
||||
const occ = this._occ[u.homeR * COLS + u.homeC];
|
||||
const [r, c] = this.restTile(u);
|
||||
const occ = this._occ[r * COLS + c];
|
||||
if (occ && occ.alive && occ !== u) return DOWN_STATE.WAIT_TILE;
|
||||
const ps = this.getPlayer(u.ownerId);
|
||||
return !ps || ps.dp + 1e-9 < u.base.cost ? DOWN_STATE.WAIT_DP : DOWN_STATE.COUNTING;
|
||||
|
||||
@@ -25,13 +25,16 @@
|
||||
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready) with no enemy in range → "保留技力立即再部署"
|
||||
// next to the most advanced ground enemy, on a free tile its position may be deployed on
|
||||
// (grid.canStand: never the 深水区 — player report #3 after 0.1.0, members dropped into 战场#08's
|
||||
// pool after an enemy wading in it): a real redeployment (retreat + free redeploy on the
|
||||
// landing tile, full HP, `deploy` fires — 部署时 traits such as 史尔特尔, 突袭手雷, 卡西米尔, 叙拉古)
|
||||
// with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP +(base + per·L)
|
||||
// until it leaves the field; later redeploys use its board tile again;
|
||||
// pool after an enemy wading in it; Battle.isReservedTile: never a tile a knocked-out operator
|
||||
// lies on — player report F5, members landed on a fallen teammate): a real redeployment (retreat +
|
||||
// free redeploy on the landing tile, full HP, `deploy` fires — 部署时 traits such as 史尔特尔,
|
||||
// 突袭手雷, 卡西米尔, 叙拉古) with its SP / charges kept (engine redeploy tile + keepSp); ATK/HP
|
||||
// +(base + per·L) until it leaves the field; knocked out after a jump it lies where it fell and
|
||||
// comes back there (the engine's rest tile, Battle._layBody — PRTS 卫戍协议/帮助 "原地留下一个
|
||||
// “倒地干员”…自动部署至该位置"; its own home when it fell on another board piece's home);
|
||||
// L ≥ power_bond_stack_cnt: every operator ASPD +power_attack_speed
|
||||
// 不屈 indomShip ground operator knocked out → p = min(1, base + per·L) immediate free redeploy; tier 2: every
|
||||
// operator on the field +sp SP
|
||||
// 不屈 indomShip ground operator knocked out → p = min(1, base + per·L) immediate free redeploy where it lies
|
||||
// (the engine's rest tile); tier 2: every operator on the field +sp SP
|
||||
// 协防 emptyShip all operators phys/arts taken ×(1 − damage_resistance); members dealt ×damage_scale_normal
|
||||
// (elite ×damage_scale_extra)
|
||||
// 独行 soloShip the member(s) ATK +atk, HP +max_hp, +sp SP on every deploy
|
||||
@@ -289,8 +292,9 @@ function raidPoll(battle, st) {
|
||||
|
||||
/**
|
||||
* "保留技力立即再部署": retreat the unit and redeploy it for free on (r, c) with its SP / charges kept (engine redeploy
|
||||
* `tile` + `keepSp`: restored before the `deploy` handlers run; the board tile stays the home of later redeploys).
|
||||
* Should the landing fail, the unit goes straight back to its home tile (never stranded off the field).
|
||||
* `tile` + `keepSp`: restored before the `deploy` handlers run; the board tile stays its home). Should the landing fail
|
||||
* (raidTile skips taken, reserved and body tiles, so only content refusing it), the unit goes straight back to its home
|
||||
* tile (never stranded off the field).
|
||||
* Returns 'raid' | 'home' | false.
|
||||
*/
|
||||
function raidRedeploy(battle, u, r, c) {
|
||||
|
||||
@@ -400,9 +400,10 @@ function installEgir(battle, pid, bb, members) {
|
||||
battle.on('battleStart', () => devour(battle, pid, bb, members), { once: true });
|
||||
if (!reached(battle, pid, 'egirShip', bb.power_bond_char_cnt)) return;
|
||||
// 5: "前3名【阿戈尔】干员首次被击倒时立刻复活" — PRTS: the knocked-out unit's next deployment has 0 redeploy time and
|
||||
// 0 cost, i.e. it IS knocked out (被击倒 triggers, 克莱门莎, 幽灵鲨 … fire) and redeploys at once on the tile it was
|
||||
// knocked out on (engine redeploy `tile`: a raid-relocated member comes back where it fell, later redeploys use its
|
||||
// board tile) with full HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古). Death priority 11: before 不屈 (10),
|
||||
// 0 cost, i.e. it IS knocked out (被击倒 triggers, 克莱门莎, 幽灵鲨 … fire) and redeploys at once where it lies (the
|
||||
// engine's rest tile, Battle._layBody: the tile it was knocked out on — a raid-relocated member comes back where it
|
||||
// fell —, or its own home when it fell on another board piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full
|
||||
// HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古). Death priority 11: before 不屈 (10),
|
||||
// whose redeploy "also consumes a 复活 charge" — with this order the charge is always the one used, same outcome.
|
||||
const memberSet = new Set(members);
|
||||
const max = Math.max(0, Math.floor(num(bb.max_free_respawn_cnt, 0)));
|
||||
@@ -412,7 +413,7 @@ function installEgir(battle, pid, bb, members) {
|
||||
if (c.reason !== 'killed' || !memberSet.has(u) || st.knocked.has(u)) return;
|
||||
st.knocked.add(u);
|
||||
if (st.revives >= max || u.alive || u.removed) return;
|
||||
if (!battle.redeploy(u, { free: true, tile: [u.tileR, u.tileC] }) && !battle.redeploy(u, { free: true })) return;
|
||||
if (!battle.redeploy(u, { free: true })) return;
|
||||
st.revives++;
|
||||
S.fxOn(battle, 'revive', u, 'bond:egirShip', 'respawn', { n: st.revives });
|
||||
}, { priority: 11 });
|
||||
|
||||
@@ -181,16 +181,12 @@ export function makeZone(battle, caster, { x, y, radius, duration, interval = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (a summon there would stop that operator from redeploying until it leaves).
|
||||
* 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 a board unit that has not deployed yet / waits to redeploy — a summon
|
||||
* there would stop that operator from redeploying until it leaves).
|
||||
*/
|
||||
export function summonTileFree(battle, r, c) {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c) || battle.unitAt(r, c)) return false;
|
||||
for (const u of battle.allyUnits) {
|
||||
if (u.alive || u.removed || u.kind === 'device') continue;
|
||||
if (u.homeR === r && u.homeC === c) return false;
|
||||
}
|
||||
return true;
|
||||
return Number.isInteger(r) && Number.isInteger(c) && battle.grid.inRect(r, c) && !battle.isReservedTile(r, c);
|
||||
}
|
||||
|
||||
/** First free tile around `unit` (Chebyshev ring 1, front first — offsets rotated by its direction) where `ok(r, c)` holds. */
|
||||
|
||||
@@ -125,14 +125,12 @@ function textNum(text, re, fallback) {
|
||||
return /^\d+(\.\d+)?$/.test(m[1]) ? +m[1] : (CN_NUM[m[1]] ?? fallback);
|
||||
}
|
||||
/**
|
||||
* A tile a summon may take: inside the field, nobody on it, and 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 — that unit would never deploy; a dead operator must be able to come back).
|
||||
* 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).
|
||||
*/
|
||||
function freeTile(battle, r, c) {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c) || battle.unitAt(r, c)) return false;
|
||||
for (const u of battle.allyUnits) if (!u.alive && !u.removed && u.kind !== 'device' && u.homeR === r && u.homeC === c) return false;
|
||||
return true;
|
||||
return Number.isInteger(r) && Number.isInteger(c) && battle.grid.inRect(r, c) && !battle.isReservedTile(r, c);
|
||||
}
|
||||
/** Walkable ground tile a melee summon can stand on. */
|
||||
const groundTile = (battle, r, c) => battle.grid.groundPassable(r, c) && battle.grid.canStand(r, c);
|
||||
|
||||
@@ -119,10 +119,13 @@ const reveal = (battle, enemies) => { for (const e of enemies) if (e.s.flags.ste
|
||||
function shove(battle, e, from, force) {
|
||||
return e && e.alive ? battle.push(e, num(force, 0), { from }) : 0;
|
||||
}
|
||||
/** Free tile for a summon/device: in the rect, standable, empty and not the home tile of any ally (dead ones redeploy there). */
|
||||
/**
|
||||
* Free tile for a summon/device: in the rect, standable, not reserved (Battle.isReservedTile: empty, no knocked-out
|
||||
* operator lying there) and not the home tile of any ally (dead ones redeploy there).
|
||||
*/
|
||||
function freeTile(battle, r, c, { ranged = false, ground = false } = {}) {
|
||||
if (!Number.isInteger(r) || !Number.isInteger(c) || !battle.grid.inRect(r, c)) return false;
|
||||
if (battle.unitAt(r, c)) return false;
|
||||
if (battle.isReservedTile(r, c)) return false;
|
||||
if (!battle.grid.canStand(r, c, { ranged })) return false;
|
||||
if (ground && !battle.grid.groundPassable(r, c)) return false;
|
||||
for (const u of battle.allyUnits) if (!u.removed && u.homeR === r && u.homeC === c && u.kind !== 'device') return false;
|
||||
|
||||
@@ -43,6 +43,9 @@ export class Unit {
|
||||
this.tileC = init.tileC ?? Math.round(this.x);
|
||||
this.homeR = this.tileR;
|
||||
this.homeC = this.tileC;
|
||||
// knocked-out operators (Battle.isDown): [r, c] of the tile the body lies on and the unit comes back on — where it
|
||||
// fell, or its home (Battle._layBody); x / y / tileR / tileC keep where it fell
|
||||
this.body = null;
|
||||
// deploy direction (sim/dir.js; allies: from the board piece, default RIGHT); `facing` is its derived horizontal
|
||||
// sign (sprite flip only) — a legacy `init.facing` ±1 still maps to RIGHT / LEFT
|
||||
this.dir = normDir(init.dir ?? init.facing);
|
||||
|
||||
@@ -307,7 +307,7 @@ test('突袭: idle member relocates next to a ground enemy (SP kept) with ATK/HP
|
||||
checkInvariants(h2.b);
|
||||
});
|
||||
|
||||
test('突袭: the jump is a redeployment — deploy fires (部署时 effects), full HP, SP kept; later redeploys use the board tile', () => {
|
||||
test('突袭: the jump is a redeployment — deploy fires (部署时 effects), full HP, SP kept; knocked out there, it comes back there', () => {
|
||||
const bb = bondBb('raidShip');
|
||||
const defs = { chess: { r_m: chessRec({ id: 'r_m', bonds: ['raidShip'], skill: { spCost: 40, initSp: 0, duration: 10 } }) }, enemies: DUMMY };
|
||||
let spAtJump = null;
|
||||
@@ -329,10 +329,15 @@ test('突袭: the jump is a redeployment — deploy fires (部署时 effects), f
|
||||
close(u.hp, u.s.maxHp, 'fresh deployment: full HP');
|
||||
close(u.s.maxHp, 2000 * (1 + bb.base_max_hp), 'raid HP bonus');
|
||||
assert.equal(h.result?.().perPlayer?.p1?.deaths ?? 0, 0, 'not counted as a death');
|
||||
// knocked out later: the auto-redeploy goes back to its board tile, without the raid bonus
|
||||
// knocked out later: it lies on its landing tile and comes back there ("原地留下一个“倒地干员”…满足再部署条件时…
|
||||
// 自动部署至该位置", PRTS 卫戍协议/帮助 — player report F5 after 0.1.0), without the raid bonus; its home stays the
|
||||
// board tile
|
||||
const landed = [u.tileR, u.tileC];
|
||||
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
assert.deepEqual([u.tileR, u.tileC], landed, 'lies where it fell');
|
||||
h.b.redeploy(u);
|
||||
assert.deepEqual([u.tileR, u.tileC], [12, 3], 'home tile unchanged');
|
||||
assert.deepEqual([u.tileR, u.tileC], landed, 'redeployed where it lay');
|
||||
assert.deepEqual([u.homeR, u.homeC], [12, 3], 'home tile unchanged');
|
||||
close(u.s.maxHp, 2000, 'bonus gone after leaving the field');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
+14
-12
@@ -1,6 +1,6 @@
|
||||
// test/sim/downelem.test.js — user playtest #4 items 8 and 9, sim side (server/sim/Battle.js snapshot / fieldMeta,
|
||||
// damage.js elementView): b.snap `down` lists the knocked-out operators waiting to redeploy on their own tile (the
|
||||
// client keeps them on the field knocked down with a redeploy countdown) and `elem` the element gauge each unit shows
|
||||
// damage.js elementView): b.snap `down` lists the knocked-out operators waiting to redeploy on the tile they lie on (the
|
||||
// client keeps them there knocked down with a redeploy countdown) and `elem` the element gauge each unit shows
|
||||
// (PRTS 元素: the fullest gauge, its 爆发冷却) — entries, states, and what is left out. User playtest #5 item 2: an
|
||||
// operator entering a battle knocked out (联防 carryState `down`, constants.js FORCED_EXIT) is down the same way.
|
||||
|
||||
@@ -16,7 +16,7 @@ const op = (id, stats = {}) => chessRec({ id, profession: 'WARRIOR', stats: { at
|
||||
const TOKEN = { name: '测试召唤物', stats: { maxHp: 100, atk: 0, def: 0, res: 0, blockCnt: 0, cost: 0, respawnTime: 0 } };
|
||||
const downOf = (h, id) => (h.snapshot().down || []).find((d) => d[0] === id) || null;
|
||||
|
||||
test('a knocked-out operator is `down` until it redeploys: its timer, then waiting for its tile / the DP; then back', () => {
|
||||
test('a knocked-out operator is `down` until it redeploys: its timer, then waiting for the DP (no ally takes its tile); then back', () => {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_a: op('t_a'), t_b: op('t_b') } }, units: [{ chessId: 't_a', row: 9, col: 5 }, { chessId: 't_b', row: 10, col: 5 }],
|
||||
content: 'none', autoFinish: false, timeLimit: 120, flags: { dpInit: 0, dpPerSec: 1, dpMax: 99 },
|
||||
@@ -31,18 +31,20 @@ test('a knocked-out operator is `down` until it redeploys: its timer, then waiti
|
||||
approx(d[1], Math.round((t0 + 6) * 100) / 100, 0.011);
|
||||
approx(d[2], 6, 0.011);
|
||||
assert.equal(d[3], DOWN_STATE.COUNTING);
|
||||
assert.deepEqual(d.slice(4), [9, 5], 'the tile it lies on (player report F5 after 0.1.0)');
|
||||
assert.ok(h.b.fieldMeta().units.some((u) => u.id === a.id), 'a client joining now gets its UnitInfo');
|
||||
h.run(6.5);
|
||||
assert.equal(downOf(h, a.id)[3], DOWN_STATE.WAIT_DP, 'timer done, 6.5 DP < cost 12');
|
||||
const tok = h.b.spawnToken('p1', 'token_test', 9, 5, { def: TOKEN });
|
||||
assert.ok(tok && tok.alive, 'a summon stands on its tile');
|
||||
assert.equal(downOf(h, a.id)[3], DOWN_STATE.WAIT_TILE);
|
||||
h.run(7);
|
||||
assert.ok(!a.alive, 'no redeploy while its tile is taken');
|
||||
assert.equal(downOf(h, a.id)[3], DOWN_STATE.WAIT_TILE);
|
||||
h.b.retreat(tok, { reason: 'expired', permanent: true });
|
||||
h.step();
|
||||
assert.ok(a.alive && a.deployed, 'redeployed once the tile is free (DP 13.5 ≥ 12)');
|
||||
// "倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止" (PRTS 卫戍协议/帮助): WAIT_TILE (its tile taken)
|
||||
// is a safeguard only — no summon (or any ally) is deployed on the tile it lies on
|
||||
assert.equal(h.b.spawnToken('p1', 'token_test', 9, 5, { def: TOKEN }), null, 'no summon on its tile');
|
||||
assert.equal(h.b.isReservedTile(9, 5), true);
|
||||
h.run(5);
|
||||
assert.ok(!a.alive, 'no redeploy while the DP is short (11.5 < 12)');
|
||||
assert.equal(downOf(h, a.id)[3], DOWN_STATE.WAIT_DP);
|
||||
h.run(1);
|
||||
assert.ok(a.alive && a.deployed, 'redeployed on its tile once the DP is there (12.5 ≥ 12)');
|
||||
assert.deepEqual([a.tileR, a.tileC], [9, 5]);
|
||||
assert.equal(downOf(h, a.id), null);
|
||||
assert.equal(h.snapshot().down, undefined);
|
||||
assert.ok(h.eventsOf('deploy').some((e) => e[1] === a.id));
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// Player report F5 after the 0.1.0 release: "突袭盟约的干员会移动到濒死干员的尸体上" — a 突袭 member jumping
|
||||
// ("保留技力立即再部署至一名地面敌人周围") landed on the tile of a knocked-out (濒死) operator lying there, and an
|
||||
// operator's timed redeploy came back on its home tile although another operator lay knocked out on it.
|
||||
// Official (PRTS 卫戍协议/帮助 §作战阶段 单位部署):
|
||||
// "干员退场后,将返回隐藏的待部署区,并原地留下一个“倒地干员”以供查看信息,满足再部署条件时,移除场上的该倒地干员并
|
||||
// 自动部署至该位置。"
|
||||
// "倒地干员所在地块视为可部署,但所有我方单位在此处的部署行为将被阻止。"
|
||||
// "若干员被击倒的位置为其他干员或召唤物的初始位置,则在被击倒后,尝试返回其自身的初始位置。"
|
||||
// So a knocked-out operator lies where it fell (on its own home tile when it fell on another board piece's home and its
|
||||
// own is free), comes back on that tile, and no ally deployment (a 突袭 landing, a redeploy, a summon, a device, a
|
||||
// move) takes it meanwhile.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { makeMatch } from '../match/harness.js';
|
||||
import { buildBattleSpec, createBattleFromSpec } from '../../server/sim/spec.js';
|
||||
import { inRange } from '../../server/sim/content/support/index.js';
|
||||
|
||||
const RAID = { raidShip: { count: 2, active: true, tier: 1, layers: 0 } };
|
||||
const raider = (id) => chessRec({ id, bonds: ['raidShip'], profession: 'WARRIOR', skill: null });
|
||||
const plain = (id) => chessRec({ id, bonds: [], profession: 'WARRIOR', skill: null });
|
||||
const tileOf = (u) => [u.tileR, u.tileC];
|
||||
const kill = (b, u) => b.dealDamage(null, u, { amount: 1e9, type: 'true' });
|
||||
/** The b.snap `down` entry of `u` ([id, respawnAt, respawnTime, state, row, col]) or null. */
|
||||
const downEntry = (b, u) => (b.snapshot().down || []).find((d) => d[0] === u.id) ?? null;
|
||||
/** Where the client draws the knocked-out `u`: the tile its `down` entry names (a 4-element entry: where it fell). */
|
||||
const bodyOf = (b, u) => { const d = downEntry(b, u); return d && d.length >= 6 ? d.slice(4) : tileOf(u); };
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// the player scenario on the real product path
|
||||
|
||||
/**
|
||||
* The normal field of a solo player at PREP of `round` (seed `seed`, stage `stageId`) with exactly `pieces`
|
||||
* ([chessId, row, col]) on the board: the real Match, PlayerState, chess data and round wave, built like
|
||||
* Match._ccField (the BattleSpec the browser and the server both build their Battle from).
|
||||
*/
|
||||
function realField({ pieces, round, seed, stageId }) {
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed }).start();
|
||||
const ps = h.ps('p_0');
|
||||
assert.ok(h.drive(() => { if (ps.lp > 0 && ps.lp < 300) ps.lp = 300; return h.m.phase === 'PREP' && h.m.round === round; }), `PREP R${round}`);
|
||||
h.setStage(stageId);
|
||||
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
ps.hand.fill(null);
|
||||
for (const [id, r, c] of pieces) ps.board.set(`${r},${c}`, ps.newPiece('chess', id));
|
||||
ps.recompute();
|
||||
const spec = buildBattleSpec({ ...h.m._normalOpts(ps), battleId: 'feedback1f', fieldId: 'n:p_0', kind: 'normal', content: h.m.battleContent });
|
||||
return createBattleFromSpec(spec, h.m.ds, { quiet: true, recordEvents: false });
|
||||
}
|
||||
|
||||
// Six 突袭 members (宴 休谟斯 斯卡蒂 瑕光 伊内丝 缄默德克萨斯 史尔特尔 仇白 耀骑士临光 — real chess) on 战场#07 / 战场#08 (下半)
|
||||
// against the real round waves: on 0.1.0 each run put a member on a knocked-out operator — a 突袭 landing on the body
|
||||
// of a member that fell after its own jump, or a member's timed redeploy on its home tile where a teammate lay that
|
||||
// had jumped there and fallen.
|
||||
const RUNS = [
|
||||
{ stageId: 'act2autochess_m03', round: 12, seed: 4, pieces: [['chess_char_4_04_a', 12, 2], ['chess_char_4_16_a', 10, 2], ['chess_char_5_07_a', 12, 5], ['chess_char_6_15_a', 10, 5], ['chess_char_6_17_a', 9, 4], ['chess_char_1_18_a', 11, 6]] },
|
||||
{ stageId: 'act2autochess_m04', round: 6, seed: 3, pieces: [['chess_char_3_12_a', 12, 3], ['chess_char_4_04_a', 11, 3], ['chess_char_4_16_a', 10, 3], ['chess_char_5_07_a', 9, 3], ['chess_char_6_15_a', 12, 4], ['chess_char_6_17_a', 11, 4]] },
|
||||
{ stageId: 'act2autochess_m04', round: 12, seed: 1, pieces: [['chess_char_2_09_a', 12, 3], ['chess_char_3_05_a', 11, 3], ['chess_char_3_12_a', 10, 3], ['chess_char_4_04_a', 9, 3], ['chess_char_4_16_a', 12, 4], ['chess_char_5_07_a', 11, 4]] },
|
||||
{ stageId: 'act2autochess_m04', round: 12, seed: 1, pieces: [['chess_char_2_09_a', 12, 2], ['chess_char_3_05_a', 10, 2], ['chess_char_3_12_a', 12, 5], ['chess_char_4_04_a', 10, 5], ['chess_char_4_16_a', 9, 4], ['chess_char_5_07_a', 11, 6]] },
|
||||
{ stageId: 'act2autochess_m04', round: 12, seed: 4, pieces: [['chess_char_4_04_a', 12, 3], ['chess_char_4_16_a', 11, 3], ['chess_char_5_07_a', 10, 3], ['chess_char_6_15_a', 9, 3], ['chess_char_6_17_a', 12, 4], ['chess_char_1_18_a', 11, 4]] },
|
||||
];
|
||||
|
||||
test('F5 real match: no 突袭 landing, redeploy or summon ever takes the tile of a knocked-out operator; it comes back where it lies', () => {
|
||||
let jumps = 0, downs = 0, comebacks = 0;
|
||||
const bad = [];
|
||||
for (const run of RUNS) {
|
||||
const b = realField(run);
|
||||
const tag = `${run.stageId} R${run.round} seed ${run.seed}`;
|
||||
const lay = new Map(); // knocked-out operator → the tile it lies on
|
||||
b.on('death', (c) => {
|
||||
const u = c.unit;
|
||||
if (c.reason !== 'killed' || u.kind !== 'op' || !b.isDown(u)) return;
|
||||
downs++;
|
||||
const body = bodyOf(b, u);
|
||||
lay.set(u, body);
|
||||
// it lies where it fell — on its own home when it fell on another board piece's home and its own is free
|
||||
const other = b.allyUnits.find((a) => a !== u && a.uid != null && !a.removed && a.homeR === u.tileR && a.homeC === u.tileC);
|
||||
const homeFree = !b.allyUnits.some((a) => a !== u && ((a.alive && a.tileR === u.homeR && a.tileC === u.homeC) || (!a.alive && !a.removed && b.restTile(a).join() === `${u.homeR},${u.homeC}`)));
|
||||
if (other && (u.tileR !== u.homeR || u.tileC !== u.homeC) && homeFree && body.join() !== `${u.homeR},${u.homeC}`) bad.push(`${tag} t=${b.time.toFixed(1)}: ${u.name} lies on ${other.name}'s home ${body} (its own ${u.homeR},${u.homeC} is free)`);
|
||||
else if (!other && body.join() !== tileOf(u).join()) bad.push(`${tag} t=${b.time.toFixed(1)}: ${u.name} fell on ${tileOf(u)} but lies on ${body}`);
|
||||
});
|
||||
b.on('deploy', (c) => {
|
||||
const u = c.unit;
|
||||
if (c.initial || u.side !== 'ally') return;
|
||||
if (u.kind === 'op' && (u.tileR !== u.homeR || u.tileC !== u.homeC) && !lay.has(u)) jumps++;
|
||||
for (const [d, t] of lay) {
|
||||
if (d === u || d.alive) continue;
|
||||
if (t[0] === u.tileR && t[1] === u.tileC) bad.push(`${tag} t=${b.time.toFixed(1)}: ${u.name} deployed on ${t} where ${d.name} lies knocked out`);
|
||||
}
|
||||
if (lay.has(u)) {
|
||||
const t = lay.get(u);
|
||||
lay.delete(u);
|
||||
comebacks++;
|
||||
if (t[0] !== u.tileR || t[1] !== u.tileC) bad.push(`${tag} t=${b.time.toFixed(1)}: ${u.name} lay on ${t} but redeployed on ${tileOf(u)}`);
|
||||
}
|
||||
});
|
||||
while (!b.finished && b.time < 400) b.step();
|
||||
}
|
||||
assert.deepEqual(bad, []);
|
||||
// the scenario: members jumped, fell and came back
|
||||
assert.ok(jumps >= 20 && downs >= 10 && comebacks >= 5, `jumps ${jumps}, knocked out ${downs}, redeploys ${comebacks}`);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// the rule, step by step (synthetic chess, flat stage)
|
||||
|
||||
test('F5 突袭: a member never lands on a knocked-out member\'s body; the fallen member comes back on the tile it lies on', () => {
|
||||
const defs = {
|
||||
chess: { r_a: raider('r_a'), r_b: raider('r_b') },
|
||||
enemies: { enemy_f5_dummy: enemyRec({ key: 'enemy_f5_dummy', hp: 1e7, speed: 0 }), enemy_f5_bait: enemyRec({ key: 'enemy_f5_bait', hp: 2500, speed: 0 }) },
|
||||
};
|
||||
// A idles from the start; B kills the bait in front of it first (~5 s), so it becomes idle later than A
|
||||
const h = makeBattle({
|
||||
defs, bonds: RAID, units: [{ chessId: 'r_a', row: 12, col: 3 }, { chessId: 'r_b', row: 10, col: 3 }],
|
||||
enemies: [{ key: 'enemy_f5_dummy', pos: [9, 8] }, { key: 'enemy_f5_bait', pos: [10, 4] }],
|
||||
});
|
||||
const A = h.unit('r_a'), B = h.unit('r_b');
|
||||
assert.ok(h.runUntil(() => A.tileR !== 12 || A.tileC !== 3, 12), 'A jumped');
|
||||
const L = tileOf(A);
|
||||
assert.deepEqual([B.tileR, B.tileC], [10, 3], 'B still busy / idle at home');
|
||||
kill(h.b, A);
|
||||
assert.ok(h.b.isDown(A), 'A knocked out');
|
||||
assert.deepEqual(downEntry(h.b, A).slice(4), L, 'A lies where it fell: b.snap down carries that tile');
|
||||
assert.equal(h.b.isReservedTile(L[0], L[1]), true, 'its tile is reserved for it');
|
||||
assert.equal(h.b.isReservedTile(12, 3), false, 'its home is not (only the body\'s tile blocks)');
|
||||
assert.ok(h.runUntil(() => B.tileR !== 10 || B.tileC !== 3, 20), 'B jumped');
|
||||
assert.notDeepEqual(tileOf(B), L, 'B did not land on A\'s body');
|
||||
assert.ok(inRange(B, h.enemy('enemy_f5_dummy')), 'B still lands next to the enemy, with it in range');
|
||||
// "满足再部署条件时,移除场上的该倒地干员并自动部署至该位置"
|
||||
assert.ok(h.runUntil(() => A.alive, 30), 'A redeployed after its timer');
|
||||
assert.deepEqual(tileOf(A), L, 'on the tile it lay on');
|
||||
assert.deepEqual([A.homeR, A.homeC], [12, 3], 'its home (the board tile) is unchanged');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('F5 engine: every ally placement refuses the tile a knocked-out operator lies on — redeploy, summon, device, move', () => {
|
||||
const defs = { chess: { d_a: plain('d_a'), d_b: plain('d_b'), d_c: plain('d_c') } };
|
||||
const h = makeBattle({ defs, units: [{ chessId: 'd_a', row: 12, col: 3 }, { chessId: 'd_b', row: 10, col: 3 }, { chessId: 'd_c', row: 11, col: 5 }] });
|
||||
h.step();
|
||||
const b = h.b, A = h.unit('d_a'), B = h.unit('d_b'), C = h.unit('d_c');
|
||||
// A moved away like a 突袭 jump, then knocked out there
|
||||
b.retreat(A, { reason: 'raid' });
|
||||
assert.ok(b.redeploy(A, { free: true, tile: [9, 8] }));
|
||||
kill(b, A);
|
||||
assert.ok(b.isDown(A));
|
||||
assert.deepEqual(tileOf(A), [9, 8]);
|
||||
assert.equal(b.isReservedTile(9, 8), true);
|
||||
b.retreat(B, { reason: 'raid' });
|
||||
assert.equal(b.redeploy(B, { free: true, tile: [9, 8] }), false, 'a redeploy onto the body is refused');
|
||||
assert.ok(b.redeploy(B, { free: true }), 'B back home');
|
||||
assert.deepEqual(tileOf(B), [10, 3]);
|
||||
assert.equal(b.spawnToken('p1', 'token_10028_vigil_wolf', 9, 8), null, 'a summon onto the body is refused');
|
||||
assert.equal(b.spawnDevice('f5_device', 9, 8, { hp: 100 }), null, 'a device onto the body is refused');
|
||||
assert.equal(b.relocate(C, 9, 8), false, 'moving an operator onto the body is refused');
|
||||
assert.deepEqual(tileOf(C), [11, 5]);
|
||||
// a tactician at (9,6) whose range holds (9,8) — the nearer tile — and (9,9)
|
||||
assert.equal(b.findTacticalPoint({ baseRangeKeys: [9 * 21 + 8, 9 * 21 + 9], tileR: 9, tileC: 6, dir: 'RIGHT' })?.join(), '9,9', 'no tactical point on the body');
|
||||
// its own redeploy comes back there ("自动部署至该位置")
|
||||
assert.ok(b.redeploy(A, { free: true }));
|
||||
assert.deepEqual(tileOf(A), [9, 8]);
|
||||
assert.equal(b.isReservedTile(9, 8), true, 'standing there now');
|
||||
checkInvariants(b);
|
||||
});
|
||||
|
||||
test('F5 "若干员被击倒的位置为其他干员或召唤物的初始位置…尝试返回其自身的初始位置": a body on another piece\'s home goes back to its own', () => {
|
||||
const defs = { chess: { h_a: plain('h_a'), h_b: plain('h_b') } };
|
||||
const setup = () => {
|
||||
const h = makeBattle({ defs, units: [{ chessId: 'h_a', row: 12, col: 3 }, { chessId: 'h_b', row: 10, col: 3 }] });
|
||||
h.step();
|
||||
const b = h.b, A = h.unit('h_a'), B = h.unit('h_b');
|
||||
b.retreat(A, { reason: 'raid' });
|
||||
assert.ok(b.redeploy(A, { free: true, tile: [11, 6] }), 'A away from its home');
|
||||
b.retreat(B, { reason: 'raid' });
|
||||
assert.ok(b.redeploy(B, { free: true, tile: [12, 3] }), 'B on A\'s home (free: A is away)');
|
||||
return { h, b, A, B };
|
||||
};
|
||||
{
|
||||
const { h, b, A, B } = setup();
|
||||
let fellAt = null;
|
||||
b.on('death', (c) => { if (c.unit === B) fellAt = [B.tileR, B.tileC]; });
|
||||
kill(b, B);
|
||||
assert.ok(b.isDown(B));
|
||||
assert.deepEqual(fellAt, [12, 3], 'the death handlers (被击倒时 effects) still see where it fell');
|
||||
assert.deepEqual(downEntry(b, B).slice(4), [10, 3], 'B lies on its own home, not on A\'s (b.snap down)');
|
||||
assert.deepEqual(b.restTile(B), [10, 3]);
|
||||
assert.equal(b.isReservedTile(12, 3), false, 'A\'s home stays free');
|
||||
assert.equal(b.isReservedTile(10, 3), true);
|
||||
// A knocked out too: it lies where it is and comes back there
|
||||
kill(b, A);
|
||||
assert.deepEqual(downEntry(b, A).slice(4), [11, 6], 'A fell on no other piece\'s home: it stays');
|
||||
assert.ok(h.runUntil(() => A.alive && B.alive, 40), 'both redeploy');
|
||||
assert.deepEqual(tileOf(B), [10, 3]);
|
||||
assert.deepEqual(tileOf(A), [11, 6]);
|
||||
checkInvariants(b);
|
||||
}
|
||||
{
|
||||
// its own home taken (A moved onto it): the body stays where it fell [ASSUMED: one attempt, at the knock-out]
|
||||
const { b, A, B } = setup();
|
||||
b.retreat(A, { reason: 'raid' });
|
||||
assert.ok(b.redeploy(A, { free: true, tile: [10, 3] }), 'A on B\'s home');
|
||||
kill(b, B);
|
||||
assert.deepEqual(downEntry(b, B).slice(4), [12, 3], 'B stays on the tile it fell on');
|
||||
assert.equal(b.isReservedTile(12, 3), true);
|
||||
checkInvariants(b);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user