feedback5: 乌尔比安 knocked out while his S3 has moved him lies and redeploys on his deployment tile, not on the anchor tile — the owner's decision of 2026-10-07 on community report 28 「乌尔比安3技能期间死亡会留在3技能位移后的位置复活,应回到初始部署位复活」, a deliberate deviation from PRTS's "where it fell" for this 【移动】 only; golden: unchanged (the corpus moves him 23 times and knocks him out 6 times, never while moved)

- server/sim/units.js `downAtHome` (false), cleared by every deployment (battle/deploy.js _deploy).
- kits/ops/chess_char_5_05-ulpia.js sets it once the S3 【移动】 has put him on the anchor tile; onEnd clears it when the
  skill ends with him standing (the 【返回】); knocked out, it stays for the body.
- sim/battle/tiles.js _layBody: a unit holding it lies on its home tile (his board placement, where the 从不混淆的方向
  stood) when that tile is in the rect and free — else where it fell [ASSUMED], the PRTS exception's one attempt; the
  timed redeploy, 不屈's and CB1's 复活 (M3茧甲 / 埃芒加德: at once, free) all come back there (restTile), b.snap
  `down` names it. Every other moved operator (a 突袭 jump, other kits' 【移动】) keeps "where it fell".
- Test: test/sim/feedback5-ulpia-revive-home.test.js (3) — knocked out during S3 he lies at home, the anchor tile free,
  and redeploys there; revived at home by M3茧甲; control: after the 【返回】 and for a 突袭 member the PRTS rule holds. The
  first two fail on the parent (he lay and stood up on (10,7)); the third fails there only on the missing field.
- Docs: DESIGN §25.17.3 (the decision, marked as the owner's deviation; the §25 intro's decisions of 2026-10-07),
  docs/SIM.md §1, DESIGN §5 (design/engine.md DP / knocked-out line), docs/PLAYING.md 作战 row.

Gate: non-browser suites 5024 / 5024, npm run golden ok, test/golden.test.js 9 / 9, lint 0 errors, check:imports ok,
typecheck ok.
This commit is contained in:
sganggs committed 2026-10-07 04:04:41 +08:00
1 parent 9ca52ba798
commit 9738bb829a
9 files changed
+128 -14

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@
| 机变 | 部分回合开始前出现:同盟共 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 队友)的同盟模拟不限时。所有人「准备就绪」即提前开战,全员就绪前可取消。 |
- **特殊地形点一下就有说明**:战场上的**活性源石**、**沼泽**、**排气格栅**、**深水区**,以及**红门 / 蓝门 / 传送点**,点一下就会弹出它自己的说明卡——名称、机制,以及**这一关自己的数值**(比如战场#04 的活性源石:每秒 70 点真实伤害、攻击力 +20%、攻击速度 +20、持续 300 秒)。休整期和作战期都能点。普通地板 / 道路 / 高台点了不会弹东西,所以点一下仍然只是「取消选中、关掉卡片」。
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
| 作战 | 每人在自己的战场各自作战(战斗在你的浏览器里模拟,2 倍速)。干员按**逐列从左到右、每列从上到下**的顺序(官方「从上到下>从左到右」:最左一列最先)自动部署并消耗部署费用(初始 10,+1/秒)。时限 45–115 秒(实际秒数,即屏幕上的倒计时),时间到仍存活的敌人算漏怪。有敌人进入蓝门时,顶栏的目标生命值会**立即**显示扣除后的数值(红字 + 「−N」,每回合至多 10 点),结算时确定;联防阶段标注「联防中」,队友拦下的部分最后不扣。左侧队伍栏同样显示队友的待扣生命值。被击倒的干员**留在倒下的格子上**(倒地姿势、略微变灰),之后也在这一格再部署(与官方一样;倒在其他干员或召唤物初始位置上的,会回到自己的初始位置躺下,召唤物已经消失也一样;唯一的例外是乌尔比安:三技能把他移到船锚处期间被击倒时,他回到自己的部署位躺下、再部署——这是项目维护者 2026-10-07 应社区反馈有意做出的改动,与官方「原地倒下」不同);因自身效果强制退场的干员(史尔特尔「余烬」结束、耀骑士临光 2 技能结束等)同样倒在原地、在原地再部署;倒地干员所在的格子**不会被占用**,突袭盟约的再部署、援军、召唤物都会避开它。头上的圆圈是再部署倒计时:绿色圆环 + 秒数;变成黄色「DP」表示部署费用不够。血条下方的一行是**元素损伤**(与原版一致:元素图标 + 白条):干员为带图标的彩色圆盘,敌人为小号纯色圆盘,右侧白条是剩余元素值,像血条一样从右往左减少,白条耗尽的那一下就是爆条(被大额元素损伤一次打掉一截时白条也会一次少一截,例如掠海漂移体每击约为 25%–50%);爆条后进入爆发冷却(灼燃 / 神经 / 侵蚀 10 秒,敌方侵蚀 8 秒,凋亡 15 秒),这时白条变成**元素颜色**(神经青绿、侵蚀蓝、灼燃橙红、凋亡灰紫)并在冷却期间慢慢回满、图标闪烁——彩色条表示已经爆条、正在承受爆发效果,不是还剩的元素值;期间所有元素都不会累积也无法被治疗,结束后清零。元素爆发造成的是**无来源伤害**,不吃干员的增伤、穿透和装备加成(例如深巡打海嗣时灼燃爆发固定 7000),击杀仍记在爆条的干员名下;元素脆弱只提高元素伤害,不加快元素条;脆弱不影响元素伤害。已被击杀的敌人不会再被附加元素损伤。 |
| 联防 | 见第 5 节。 |
| 结算 | 扣 LP、发放悬赏资金、结算层数;LP ≤ 0 的玩家淘汰(其干员回到公共卡池)。 |
+3 -1
View File
@@ -90,7 +90,9 @@ timer done, tile free, DP ≥ cost), 不屈's, 阿戈尔's — comes back there
初始位置,则在被击倒后,尝试返回其自身的初始位置": one that fell on another board piece's home (a 突袭 member after its jump,
乌尔比安 off his anchor; the piece on the field or not — a summon leaves its home free only once it has expired or been
killed) 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. "倒地干员所在地块视为可部署,但所有我方单位在此处的部署
where it fell for the `kill` / `death` handlers. One deliberate deviation, the owner's decision of 2026-10-07 (community
report 28): 乌尔比安 knocked out while his S3 has moved him lies on his deployment tile (his home) wherever he fell, when it
is free (`Unit.downAtHome`, set by his kit for that 【移动】, cleared by the return and by every deployment; DESIGN §25.17.3). "倒地干员所在地块视为可部署,但所有我方单位在此处的部署
行为将被阻止": `downOn(r, c)` — `_deploy` (redeploys, the 突袭 landing, summons), `spawnDevice` and `relocate` refuse that
tile and `isReservedTile` reports it, so every automatic picker skips it. The rule covers every 退场 (GitHub #60): an
operator forced out by its own effects (`retreat` reason `'retreat'`: 史尔特尔's 余烬, 耀骑士临光 S2, 骑士戒律 + 竞技旗,
+1 -1
View File
@@ -118,7 +118,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
### 5.5 Combat rules (engine)
- **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.23; `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.
- **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, and 乌尔比安 knocked out while his S3 has moved him always on his deployment tile when it is free (the owner's deviation of 2026-10-07, `Unit.downAtHome`, §25.17.3) — 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 × the blocker's 阻挡半径倍率 for blockFly units vs flyers (起飞; mod `blockRadiusScale` = 倍率 − 1 — PRTS 游戏数据基础 "飞行阻挡半径 = 0.8944 × 阻挡半径倍率", 凯尔希·思衡托 +0.23), 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). An enemy holding 不可阻挡 (PRTS 异常效果 BLOCK_FREE 「无法阻挡/被阻挡,自动解除阻挡」) — `unblockable` (恐惧, 诱导, enemy abilities), 浮空, and 沉睡 (SLEEPING = 无法行动+无敌+不可阻挡) — is never blocked and is released at once: a sleeping enemy frees its blocker's slot, stays where it is and, once awake, is blocked again only by a blocker with room (§24.9). 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; 「同时攻击阻挡的所有敌人」 — 强攻手 / 重剑手 / 推击手 and the skills worded so, `hitAllBlocked` — up to the block count, never fewer than 1, since 0.2.0: PRTS 分支特性信息 "普通攻击最大目标数等于阻挡数(不会低于1)"), 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).
- **Healers** (`dmgType heal`): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.json `abnormal`, `noHeal`; §20.3; 史尔特尔 during 余烬, §22.7), and never the enemies they block — also while blocking (§21.17); do nothing otherwise (SP still regenerates).
+21 -7
View File
@@ -34,7 +34,7 @@ second batch of community reports of 2026-10-06, one section per stream: CB2, he
CB3, operator kits and triggers (砾's double hit, 乌尔比安's S3 landing behind the 联防 red gate, the 「受到攻击时」
counters on every enemy damage instance, a 突袭 member's jump after an in-place 复活, 银灰's sound; 铃兰's trigger,
琳琅诗怀雅's champagne, the 突袭 玛恩纳, 归溟幽灵鲨 devoured and 荒芜拉普兰德 / 叙拉古 already answered; 乌尔比安's revive
tile a question) → §25.17; CB4, enemies, bosses, waves, movement (the 突袭 landing on the 临时整备区 row and the
tile on his deployment tile, the owner's deviation) → §25.17; CB4, enemies, bosses, waves, movement (the 突袭 landing on the 临时整备区 row and the
other half of the 联防 map, a silenced 萨卡兹枯朽战士's death pollution, 萨卡兹穿刺手's speed after a stun and its first
hit, 自制投石机 blocked on the 排气格栅; the briefing types' 7–9 waves, 新硎's 断刃, a freed prisoner in 联防 and 鸭爵's two
swaps in one wave already answered; the moving leaders' routes checked; 野鬃, 锏's pulls, 昆图斯's footprint and the leaders' attack rate not
@@ -58,8 +58,10 @@ lock (§25.13.2); a newly merged elite 拉普兰德 counts as a new 拉普兰德
alive at the fight's start, adopting PR #209 and replacing 「保持固定血量」 (§25.13.4); flying units at 1.3 tiles and the two
model quirks of PR #211 (§25.13.5); the bounty pays whenever its enemy dies (relayed with the community
reports, §25.14.3); 2026-10-07 — the in-match shortcuts rebindable in 设置 (§25.15); a language is a file in the
language folder, and the language pack is the first type of a general content-pack mechanism (§25.20); 烛煌's 绝处重燃
before 坚固维式重锤's lock and the official tier cap of 寻呼模块 / 简易通讯机 kept as they are (§25.21.1, §25.21.10).
language folder, and the language pack is the first type of a general content-pack mechanism (§25.20); 乌尔比安 knocked
out during his S3 comes back on his deployment tile, a deliberate deviation from PRTS's "where it fell" (§25.17.3);
烛煌's 绝处重燃 before 坚固维式重锤's lock and the official tier cap of 寻呼模块 / 简易通讯机 kept as they are (§25.21.1,
§25.21.10).
### 25.1 Refactor and tooling: the big files split, golden results, lint, types, import boundaries (refactor plan; owner's decision of 2026-10-04: no web framework) — `server/http/`, `server/match/match/`, `server/match/player/`, `server/sim/battle/`, `content/enemies/`, `kits/ops/`, `public/js/ui/gameLogic/`, `render/fx/`, `render/app/`, `screens/game/`, `tools/golden.mjs`, `tools/check-imports.mjs`, `eslint.config.js`, `jsconfig.json`
@@ -1218,7 +1220,7 @@ on the 0.2.0 tree with a script first (where the report could be older, on the v
against the official data (character_table, skill_table, activity_table → act2autochess, audio_data, ArknightsGameData's
buff templates, the local client's charpacks and skill prefabs) and PRTS (备注 included). Five are real and fixed, one
commit each with its golden scenarios named; five were already answered or are not bugs (pinned by tests); one is a
question for the owner.
question for the owner (settled on 2026-10-07: §25.17.3).
#### 25.17.1 「砾的平A是比较特殊的双判,是两次50%伤害倍率(不是攻击倍率)」 (item 16.5): two instances of 50 % 伤害倍率 per normal attack — `sim/ai.js resolveHit`, `sim/professions.js` (`hitDmgMul`), `kits/ops/chess_char_2_12-gravel.js`
@@ -1252,7 +1254,7 @@ question for the owner.
`test/sim/feedback5-ulpia-own-board.test.js`; `test/sim/facing-invariance.test.js` counts every tile of its open field
as the player's own (a board-position rule, like the deployment order it already avoids).
#### 25.17.3 「乌尔比安3技能期间死亡会留在3技能位移后的位置复活,应回到初始部署位复活」 (item 28): the sources say where he fell — a question for the owner, no change
#### 25.17.3 「乌尔比安3技能期间死亡会留在3技能位移后的位置复活,应回到初始部署位复活」 (item 28): back to his deployment tile — the owner's decision of 2026-10-07, a deliberate deviation from the sources — `kits/ops/chess_char_5_05-ulpia.js`, `sim/battle/tiles.js _layBody`, `sim/units.js downAtHome`
- **Checked**: knocked out during S3 he lies on the landing tile and comes back there — the rule of every operator moved
off its board tile since 0.1.1 (§21.24, `Battle._layBody`): PRTS 卫戍协议/帮助 「原地留下一个“倒地干员”…自动部署至该位置」,
@@ -1260,8 +1262,20 @@ question for the owner.
landing tile. PRTS 乌尔比安 S3 备注 ④ (the 【返回】 at the skill's end) says nothing of a knock-out, and the client's S3
templates (`ulpia_s3_*`: the 【移动】 respawn clips and flags) carry no other rule. With §25.17.2 he can no longer fall
behind the red gate.
- **Question for the owner**: keep "where he fell" (the sources) or send him back to his deployment tile (the report)?
- Golden: unchanged.
- **Decision**: asked whether to keep "where he fell" (the sources) or send him back to his deployment tile (the report),
the owner chose the deployment tile (the owner's decision of 2026-10-07) — **a deliberate deviation** from PRTS's rule,
for his S3 only: every other operator moved off its tile (a 突袭 jump, the 【移动】 of other kits) still lies and comes
back where it fell.
- **Now**: his kit sets `Unit.downAtHome` once the S3 【移动】 has put him on the anchor tile; the 【返回】 (the skill over
with him standing) and every deployment clear it. Knocked out while it holds, `Battle._layBody` lays him on his home
tile — his deployment tile, where the 从不混淆的方向 stood (gone with the skill) — and he redeploys there (timed, 不屈, or at
once by CB1's 复活, §25.21.3); b.snap `down` names that tile, so the client draws him there.
- [ASSUMED]: his deployment tile taken (a unit or another body on it) → where he fell, the one attempt of the PRTS
exception above; a 突袭 member's jump tile before the S3 is not his deployment tile (his board home is).
- Golden: unchanged — the corpus moves him 23 times (roster-015, four solo boss fields) and knocks him out 6 times,
never while moved (instrumented). Tests: `test/sim/feedback5-ulpia-revive-home.test.js` (3: knocked out during S3 he
lies and redeploys at home, revived at home by M3茧甲 — both fail before —, and the control: after the 【返回】 and for a
突袭 member the PRTS rule holds). Docs: docs/SIM.md §1, DESIGN §5 (design/engine.md), docs/PLAYING.md.
#### 25.17.4 「退行的潮涌者的攻击无法触发菲莱2技能的受击反击(排查其他类似技能的干员)」 (item 30): the 「受到攻击时」 counters answer every damage instance from an enemy — `kits/shared/tier1.js byEnemyAttack`, `kits/ops/chess_char_3_06-philae.js` and the kits below
+1
View File
@@ -33,6 +33,7 @@ export class BattleDeploy {
u.hidden = false;
u.body = null;
u.countdown = null; // a countdown summon's content starts its new life in the deploy hook (content/tokens.js startCountdown)
u.downAtHome = false; // a new deployment: a later knock-out lies where it falls again (Battle._layBody)
u.x = C0; u.y = R0; u.tileR = R0; u.tileC = C0;
u.blocking = [];
u.deploySeq = ++this._deploySeq;
+6 -4
View File
@@ -105,15 +105,17 @@ export class BattleTiles {
* gone: a summon only leaves its home free once it has expired or been killed, so a removed one must count too), 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.
* knock-out]. Only an operator moved off its board tile — a 突袭 jump, 乌尔比安's anchor — can fall elsewhere. One
* deliberate deviation, the owner's decision of 2026-10-07 (community report 28): a unit whose content holds
* `downAtHome` — 乌尔比安 moved by his S3 (kits/ops/chess_char_5_05-ulpia.js) — goes back to its home tile, its
* deployment tile, wherever it fell (that home taken: where it fell, as above). 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.kind === 'op' || a.kind === 'token') && a.homeR === r && a.homeC === c)) return;
if (!u.downAtHome && !this.allyUnits.some((a) => a !== u && a.uid != null && (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];
}
@@ -16,6 +16,8 @@ export default {
// (projectile_range); moves onto the anchor tile when deployable (a tile of his own board) and not his own (a 从不混淆的方向
// marker keeps his tile)
// and returns at skill end — both 【移动】, i.e. free redeploys (Battle.moveRedeploy; the return empties his SP).
// Knocked out while moved, he lies and redeploys on his deployment tile (Unit.downAtHome — the owner's decision of
// 2026-10-07, a deliberate deviation from PRTS's "where it fell", DESIGN §25.17.3).
// T1 本性的坚守: heal 100 (160 below 50 %) on every hit taken. T2 血脉的哺养: per kill +120 max HP / +30 ATK (×9),
// other Abyssal Hunters +50 %. Module (elite): healing received ×1.2.
// S1 必须促成的接触 (instant): the anchor lands on the best enemy of the skill range (beyond his own range, unblocked
@@ -104,6 +106,11 @@ export default {
// no exit) that keeps the running skill — "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数"; the rest of
// his buffs are kept too (owner's deviation, DESIGN §22.3). The marker is deployed after the move (备注 ③)
if (!battle.moveRedeploy(unit, dest[0], dest[1]) || !unit.alive || !unit.skill?.active) return;
// knocked out while moved, he lies and redeploys on his deployment tile (Battle._layBody) — the owner's decision
// of 2026-10-07 (community report 28 「乌尔比安3技能期间死亡…应回到初始部署位复活」), a deliberate deviation from
// PRTS's "where it fell" (帮助 「原地留下一个“倒地干员”」), for this 【移动】 only; the return or his next
// deployment clears it
unit.downAtHome = true;
const marker = battle.spawnToken(unit, tokenId, home[0], home[1], { untargetable: true, kit: { skill: null, trait: { noAttack: true } } });
unit.mem.anchorHome = { r: home[0], c: home[1], marker };
battle.fx('teleport', { x: unit.x, y: unit.y, id: unit.id, fromX, fromY });
@@ -111,6 +118,8 @@ export default {
onEnd({ battle, unit }) {
const h = unit.mem.anchorHome;
unit.mem.anchorHome = null;
// the skill over with him standing: no longer moved by it (knocked out, the flag stays for Battle._layBody)
if (unit.alive) unit.downAtHome = false;
if (!h) return;
if (h.marker && h.marker.alive) battle.retreat(h.marker, { reason: 'expired', permanent: true });
if (unit.alive && unit.deployed) {
+4
View File
@@ -84,6 +84,10 @@ export class Unit {
// a countdown summon's life on the field ({ from, until } battle times; content/tokens.js startCountdown, cleared by
// every deployment): its bar shows the time left (snapshot.js unitTuple), its HP never moves (无敌 + 禁疗)
this.countdown = null;
// knocked out, the unit lies — and redeploys — on its home tile instead of where it fell (Battle._layBody) while content
// holds this: 乌尔比安 moved by his S3 (the owner's decision of 2026-10-07, a deviation from PRTS's "where it fell");
// every deployment clears it (battle/deploy.js _deploy)
this.downAtHome = false;
this.trait = {}; // profession runtime state
this.stats = { dmg: 0, kills: 0, heal: 0, taken: 0, attacks: 0 };
this.hidden = false; // enemies inside a DISAPPEAR segment
@@ -0,0 +1,82 @@
// test/sim/feedback5-ulpia-revive-home.test.js — community report of 2026-10-06 (item 28) 「乌尔比安3技能期间死亡会留在3技能位移后
// 的位置复活,应回到初始部署位复活」 and the owner's decision of 2026-10-07: 乌尔比安 knocked out while his S3 必须开辟的通路 has
// moved him onto the anchor tile lies — and comes back — on his deployment tile. A deliberate deviation from the PRTS rule
// every other operator keeps (PRTS 卫戍协议/帮助 「原地留下一个“倒地干员”…满足再部署条件时…自动部署至该位置」: where it fell,
// Battle._layBody, DESIGN §21.24), narrowly for that 【移动】 (Unit.downAtHome, set by kits/ops/chess_char_5_05-ulpia.js).
// Run: node --test test/sim/feedback5-ulpia-revive-home.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, chessRec, checkInvariants } from '../helpers/battleHarness.js';
const ULPIA = 'chess_char_5_05_a';
const DUMMY = { enemy_anchor_dummy: enemyRec({ key: 'enemy_anchor_dummy', hp: 1e9, speed: 0, mass: 0, atk: 0 }) };
const kill = (b, u) => b.dealDamage(null, u, { amount: 1e9, type: 'true' });
/** The tile b.snap's `down` entry of `u` names ([id, respawnAt, respawnTime, state, row, col]): where the client draws it. */
const downTile = (b, u) => (b.snapshot().down || []).find((d) => d[0] === u.id)?.slice(4) ?? null;
/** 乌尔比安 on (10, 3) of the flat field casts S3 at an enemy parked on (10, 7): he moves onto the anchor tile. */
function movedByS3({ items } = {}) {
const h = makeBattle({
stageId: 'flat', seed: 5, autoFinish: false, timeLimit: 300, defs: { enemies: DUMMY },
units: [{ chessId: ULPIA, row: 10, col: 3, ...(items ? { items } : null) }],
});
h.step();
const u = h.unit(ULPIA);
const home = [u.tileR, u.tileC];
h.spawn('enemy_anchor_dummy', { pos: [10, 7] });
h.step();
u.skill.gainSp(999, 'test');
if (!u.skill.active) assert.ok(u.skill.activate('test'), 'S3 cast');
h.run(0.2);
assert.ok(u.alive && u.skill.active, 'S3 runs');
const landed = [u.tileR, u.tileC];
assert.deepEqual(home, [10, 3]);
assert.deepEqual(landed, [10, 7], 'moved onto the anchor tile');
return { h, u, home, landed };
}
test('knocked out during S3, 乌尔比安 lies on his deployment tile and redeploys there, not on the anchor tile (until 0.2.0: where he fell)', () => {
const { h, u, home, landed } = movedByS3();
kill(h.b, u);
assert.ok(h.b.isDown(u), 'knocked out');
assert.deepEqual(h.b.restTile(u), home, 'he lies on his deployment tile');
assert.deepEqual(downTile(h.b, u), home, 'the client draws him there');
assert.equal(h.b.isReservedTile(...home), true, 'his deployment tile is held for him');
assert.equal(h.b.isReservedTile(...landed), false, 'the anchor tile is free');
assert.equal(h.b.allyUnits.filter((a) => a.kind === 'token' && a.alive).length, 0, 'the 从不混淆的方向 marker is gone with the skill');
assert.ok(h.runUntil(() => u.alive, 120), 'he redeploys');
assert.deepEqual([u.tileR, u.tileC], home, 'on his deployment tile');
assert.equal(u.downAtHome, false, 'a new deployment: the next knock-out follows the rule again');
checkInvariants(h.b);
assert.deepEqual(h.b.errors.map((e) => `${e.label} ${e.message}`), []);
});
test('revived by M3茧甲 during S3 (CB1: a knock-out and an immediate free redeploy where he lies), he stands on his deployment tile', () => {
const { h, u, home } = movedByS3({ items: ['chess_item_4_12_e_a'] });
kill(h.b, u);
assert.ok(u.alive, 'revived at once');
assert.deepEqual([u.tileR, u.tileC], home, 'on his deployment tile');
checkInvariants(h.b);
});
test('narrow: after S3 has ended (and taken him back) and for any other moved operator, a knock-out keeps the PRTS rule — where it fell', () => {
// S3 over: the 【返回】 brings him home, the flag is gone
const { h, u, home } = movedByS3();
assert.ok(h.runUntil(() => !u.skill.active, 40), 'S3 ends');
assert.deepEqual([u.tileR, u.tileC], home, 'back home (【返回】)');
assert.equal(u.downAtHome, false);
// a 突袭 member that jumped next to an enemy and fell there lies there (feedback1f-downed-tile.test.js has the full rule)
const rec = chessRec({ id: 'r_mover', bonds: ['raidShip'], profession: 'WARRIOR', skill: null });
const g = makeBattle({
stageId: 'flat', seed: 5, autoFinish: false, timeLimit: 120, bonds: { raidShip: { count: 1, active: true, tier: 1, layers: 0 } },
defs: { chess: { r_mover: rec }, enemies: DUMMY }, units: [{ chessId: 'r_mover', row: 12, col: 3 }],
enemies: [{ key: 'enemy_anchor_dummy', pos: [10, 8] }],
});
const m = g.unit('r_mover');
assert.ok(g.runUntil(() => m.alive && m.deployed && (m.tileR !== 12 || m.tileC !== 3), 30), 'the member jumped');
const fell = [m.tileR, m.tileC];
kill(g.b, m);
assert.deepEqual(g.b.restTile(m), fell, 'it lies where it fell');
checkInvariants(g.b);
});