feedback5 CB3: an in-place 复活 (M3茧甲, 埃芒加德) starts a deploy-timed skill again, so a 突袭 member with one jumps at once after the revive instead of waiting out the 10 s idle (community report of 2026-10-06, item 44)

Official: PRTS 盟约记录 备注 — "复活" is 退场 + a 0-time / 0-cost redeploy, i.e. a new deployment, after which a deploy-timed
skill ("部署后…秒内") runs again; a 突袭 member counts such a running window as 技能就绪 (bonds/addon/battle.js raidPoll,
GitHub #49 / #109) and jumps when nothing is in its range. Cause: the remake revives in place (fatal hook) — her window, over
by then, stayed over, and only the idle trigger was left (measured: 缄默德克萨斯 S3 jumped 10.00 s after an M3茧甲 / 埃芒加德
revive, 0.23 s after 不屈's real redeploy). Now items/battle.js revivedInPlace also restarts an activateOnDeploy skill on the
next tick: activated anew when its window had ended, its window back to the full duration when it still ran. The SP reset
and the other 部署时 effects of PRTS's form stay unmodelled (§21.21).

Golden: 1 scenario — roster-012: the elite 缄默德克萨斯 is revived in place three times (101 s, 105 s, 113 s) and her S3 runs
again each time (checked instrumented). Tests: test/sim/feedback5-raid-revive.test.js (3; the M3茧甲 / 埃芒加德 jump and the
window cases fail on the parent, 不屈 is the unchanged control). Docs: docs/SIM.md and docs/design/engine.md (fatal hook).
This commit is contained in:
sganggs committed 2026-10-07 02:33:10 +08:00
1 parent 5877800a67
commit 3d472e2304
5 files changed
+121 -24

No files matched your search

+1 -1
View File
@@ -774,7 +774,7 @@ registration order. `battle.off(handle)` / `battle.off(name, fn)` / `battle.offO
| `damaged` | `{ source, target, amount, type, dmg, credit }` | after application (`amount` may be 0 when shielded); element fills too (with their source); 无来源: `source` null, `credit` set |
| `hpDamage` | `{ source, target, amount, dmg, credit }` | a damage instance after shields, before the HP loss (§4; not a 流失): lower `amount` only (a raise is ignored) — HP floors / reductions ordered after a barrier (kits/ops/op-huang.js, op-zuole.js) |
| `heal` | `{ source, target, amount, opts }` | mutable `amount` |
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, kit savers, 不死 / 复活 items, 埃芒加德; 不屈 is a `death` hook). Fired by every HP loss of a unit without a boss pool — hits of any type, element bursts, 无来源 damage, `loseHp` 流失. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤 — once per deployment: `items/battle.js deploymentOf`, a key every deploy changes and an in-place 复活 changes too; one battle-level hook holds the running windows (`holdsUndying`), so a window outlasts a lend, DESIGN §21.21 — the lock `PRIO_REVIVE` −100 after the substitutes (−100, registered first), the running windows `PRIO_UNDYING_HELD` −99 before them: a 傀儡师 holding 不死 does not switch, PRTS 分支特性信息 傀儡师 "未持有不死的情况下", DESIGN §22.11) → 淬羽赫默 S3's 不死 (−100.5: PRTS 衍生生命 "无畏者协议提供的不死效果仅在目标没有不死效果时才会触发"; its running windows held at −99 like the hammer's) → items' 复活 (M3茧甲, `PRIO_RESPAWN` −101: PRTS "复活" acts on a knock-out, which a 不死 prevents) → 埃芒加德 (−110); both 复活 revive in place and call `revivedInPlace` (a new deployment for the lock) |
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — set `prevented` (substitutes, kit savers, 不死 / 复活 items, 埃芒加德; 不屈 is a `death` hook). Fired by every HP loss of a unit without a boss pool — hits of any type, element bursts, 无来源 damage, `loseHp` 流失. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤 — once per deployment: `items/battle.js deploymentOf`, a key every deploy changes and an in-place 复活 changes too; one battle-level hook holds the running windows (`holdsUndying`), so a window outlasts a lend, DESIGN §21.21 — the lock `PRIO_REVIVE` −100 after the substitutes (−100, registered first), the running windows `PRIO_UNDYING_HELD` −99 before them: a 傀儡师 holding 不死 does not switch, PRTS 分支特性信息 傀儡师 "未持有不死的情况下", DESIGN §22.11) → 淬羽赫默 S3's 不死 (−100.5: PRTS 衍生生命 "无畏者协议提供的不死效果仅在目标没有不死效果时才会触发"; its running windows held at −99 like the hammer's) → items' 复活 (M3茧甲, `PRIO_RESPAWN` −101: PRTS "复活" acts on a knock-out, which a 不死 prevents) → 埃芒加德 (−110); both 复活 revive in place and call `revivedInPlace` (a new deployment for the lock, and a deploy-timed skill — `activateOnDeploy` — runs again from the next tick: started anew, or its window back to full; PRTS's 复活 is 退场 + a 0-time / 0-cost redeploy) |
| `dollSwitch` | `{ unit, reason, done }` | content switches a 傀儡师 to its <替身> now (归溟幽灵鲨 S2 "技能结束后立刻切换为<替身>": no lethal HP loss); its trait does it unless it already is one or is not on the field, and sets `done` |
| `dollSwap` | `{ unit, form }` | a 傀儡师 starts a switch — to its <替身> (`form` `'doll'`) or back to its <本体> (`null`); not when it is knocked out as the 替身 (不屈 rolls on it: "切换<替身>与<本体>时") |
| `kill` | `{ killer, victim }` | victim HP reached 0 (a handler may revive by restoring HP) |
+1 -1
View File
@@ -100,7 +100,7 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
| `hit` | `{ source, target, dmg }` | before mitigation; mutate `dmg` (DamageInfo) |
| `damaged` | `{ source, target, amount, type, dmg }` | after application |
| `heal` | `{ source, target, amount }` | before application (mutable amount) |
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — fired by every HP loss of a unit without a boss pool (hits of any type, element bursts, 无来源 damage, `loseHp`); set `prevented`. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤, once per deployment) `PRIO_REVIVE` −100 → items' 复活 (M3茧甲) `PRIO_RESPAWN` −101 → 埃芒加德 −110; the two 复活 revive in place and open a new deployment for the lock (§21.21); a running 坚固 window's hook sits at `PRIO_UNDYING_HELD` −99, before a 傀儡师's switch to its 替身 (−100, ahead of the lock — a 本体 holding 不死 does not switch, §22.11) |
| `fatal` | `{ unit, source, credit, dmg, amount, prevented }` | HP would reach 0 — fired by every HP loss of a unit without a boss pool (hits of any type, element bursts, 无来源 damage, `loseHp`); set `prevented`. Order: kits' own savers (10 … −60) → items' 不死 (坚固维式重锤, once per deployment) `PRIO_REVIVE` −100 → items' 复活 (M3茧甲) `PRIO_RESPAWN` −101 → 埃芒加德 −110; the two 复活 revive in place and open a new deployment for the lock (§21.21) and for a deploy-timed skill (`activateOnDeploy`, started again); a running 坚固 window's hook sits at `PRIO_UNDYING_HELD` −99, before a 傀儡师's switch to its 替身 (−100, ahead of the lock — a 本体 holding 不死 does not switch, §22.11) |
| `kill` | `{ killer, victim }` | victim hp reached 0; a `killed` ability that keeps the enemy alive (a knock-out into a 重生 / form change, §21.4) hides it from every later handler, the kill count, credit and the bounty |
| `death` | `{ unit }` | unit removed (ops may redeploy later); the `die` event and the death hooks fire only on removal |
| `skillStart` / `skillEnd` | `{ unit, skill }` | |
+20 -2
View File
@@ -246,8 +246,26 @@ function hammerState(battle, rt, u) {
*/
export function deploymentOf(u) { return `${u.deploySeq}:${u.mem.revives | 0}`; }
/** An in-place 复活 (M3茧甲, 埃芒加德) happened: a new deployment for the once-per-deployment lock (deploymentOf). */
export function revivedInPlace(u) { if (u && u.mem) u.mem.revives = (u.mem.revives | 0) + 1; }
/**
* An in-place 复活 (M3茧甲, 埃芒加德) happened. PRTS's 复活 is 退场 + a 0-time / 0-cost redeploy (PRTS 盟约记录 备注) — a new
* deployment, of which the remake carries: the once-per-deployment lock (deploymentOf) and a deploy-timed skill ("部署后…秒内",
* SkillSpec `activateOnDeploy`: 缄默德克萨斯 S1–S3, 宴 S2, 斯卡蒂 S2, 砾's barrier …) running again — started anew when its window
* had ended, its window back to the full duration when it still ran (the next tick, out of the damage pipeline that set the
* revive off). Community report of 2026-10-06 (item 44): a 突袭 member counts such a running window as 技能就绪
* (bonds/addon/battle.js raidPoll), and after an in-place revive it waited out the 10 s idle time instead of jumping at once
* as after a redeploy. The SP reset and the other 部署时 effects of PRTS's form stay unmodelled (DESIGN §21.21).
*/
export function revivedInPlace(u) {
if (!u || !u.mem) return;
u.mem.revives = (u.mem.revives | 0) + 1;
const sk = u.skill;
if (!sk || sk.noSkill || !sk.spec || !sk.spec.activateOnDeploy || !sk.battle) return;
const seq = u.deploySeq, n = u.mem.revives;
sk.battle.after(0, () => {
if (!u.alive || !u.deployed || u.deploySeq !== seq || u.mem.revives !== n || u.skill !== sk) return;
if (sk.active) { if (sk.duration > 0) sk.timeLeft = sk.duration; } else sk.activate('deploy', { free: true });
}, { owner: u });
}
/**
* Does `u` hold 坚固维式重锤's 不死 right now — a window started in this deployment that has not run out? The window lives
+20 -20
View File
@@ -720,8 +720,8 @@
"round": 10,
"seed": 1517795683,
"waveId": "act1autochess_h03",
"time": 162.6,
"ticks": 4878,
"time": 161.57,
"ticks": 4847,
"reason": "cleared",
"killed": 99,
"total": 100,
@@ -730,21 +730,21 @@
"rngDraws": 187,
"errors": {"count":0,"kinds":[]},
"players": {
"p1": {"killed":99,"total":100,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":983940,"bossDmg":0,"heal":22157,"deaths":7,"layerGains":{"preciShip":8}}
"p1": {"killed":99,"total":100,"perfect":false,"leaks":1,"leaksUncounted":0,"coins":10,"dmg":988651,"bossDmg":0,"heal":21802,"deaths":7,"layerGains":{"preciShip":8}}
},
"units": [
["p1",1,"op","chess_char_4_17_b",1,"uniequip_002_hsguma",3046.73,1,7,0,237,466,0,20,0],
["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,9,4,57534,2092,2092,32,7],
["p1",2,"op","chess_char_4_18_b",1,"uniequip_002_mudrok",6104,1,9,5,65239,2337,2337,37,8],
["p1",3,"op","chess_char_4_11_b",1,"uniequip_002_cathy",2644,1,124,0,2731,3521,0,3,1],
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,104,4,45091,4236,0,45,3],
["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2169,1,122,10,105064,5538,0,27,3],
["p1",6,"op","chess_char_4_16_b",2,"uniequip_002_texas2",1619,1,113,3,4767,5745,362,24,3],
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",0,0,104,5,40856,4236,0,40,3],
["p1",5,"op","chess_char_4_22_b",2,"uniequip_002_svrash",2219,1,122,9,108386,5488,0,27,3],
["p1",6,"op","chess_char_4_16_b",2,"uniequip_002_texas2",1619,1,113,4,16046,5745,362,23,5],
["p1",7,"op","chess_char_4_10_b",1,"uniequip_002_aroma",1664,1,4,0,0,0,0,0,0],
["p1",8,"op","chess_char_4_12_b",2,"uniequip_002_glady",2232,1,5,2,6911,557,557,14,2],
["p1",9,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,11,20,187170,2441,0,92,38],
["p1",10,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1576,1,13,44,488221,7756,0,57,23],
["p1",11,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,6,12,86215,1419,0,28,5],
["p1",12,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1534,1,14,0,0,9049,19146,96,2],
["p1",8,"op","chess_char_4_12_b",2,"uniequip_002_glady",2232,1,5,1,5724,557,557,13,2],
["p1",9,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,11,20,179263,2491,0,91,36],
["p1",10,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1526,1,13,43,487394,7656,0,58,23],
["p1",11,"op","chess_char_4_20_b",2,"uniequip_002_fartth",0,0,6,12,82773,1419,0,27,5],
["p1",12,"op","chess_char_4_21_b",1,"uniequip_002_plosis",1484,1,14,0,0,8599,18546,100,2],
["p1",13,"token","token_10041_cathy_catsld",null,null,100,1,17,0,0,0,0,0,null],
["p1",14,"token","token_10041_cathy_catsld",null,null,100,1,16,0,0,0,0,0,null],
["-","~1","device","trap_1105_accrate",null,null,100,0,1,0,0,0,0,0,null],
@@ -760,19 +760,19 @@
"enemy_1183_mlasrt": [1,1,0,988,7488],
"enemy_1207_sfji_2": [1,1,0,0,16128],
"enemy_1208_msfji_2": [4,4,0,0,120],
"enemy_1267_nhpbr": [45,45,0,12545,233280],
"enemy_1267_nhpbr_2": [24,24,0,8150,152064],
"enemy_1267_nhpbr": [45,45,0,12777,233280],
"enemy_1267_nhpbr_2": [24,24,0,7400,152064],
"enemy_1270_nhstlk": [1,1,0,0,13824],
"enemy_1270_nhstlk_2": [15,15,0,6817,276480],
"enemy_1270_nhstlk_2": [15,15,0,7030,276480],
"enemy_1325_cbgpro_2": [1,1,0,0,5299],
"enemy_1425_lrcmra_2": [1,1,0,718,28800],
"enemy_1525_blkswb": [1,0,1,11702,37611],
"enemy_1525_blkswb": [1,0,1,11702,42321],
"enemy_2050_smsha": [1,1,0,1902,46080]
},
"hooks": [1,122,4878,469,469,1417,1418,521,107,110,87,86,0,15601,1813,20,100,1,1,122,1,0,1],
"events": {"atk":626,"bounty":5,"deploy":19,"die":109,"dmg":1410,"engage":10,"fx":241,"heal":213,"layer":1,"leak":1,"skill":175,"spawn":117,"status":929},
"fx": {"beam":6,"blink":2,"displace":4,"dodge":1,"explode":4,"explosion":23,"featherArrow":5,"flame":3,"frostNova":38,"garrison":1,"garrisonGrant":1,"healField":2,"overclock":1,"palsy":1,"pull":2,"revive":4,"riposte":1,"rockslide":7,"shieldBreak":8,"slash":23,"summon":1,"swordRain":9,"swordStorm":3,"telegraph":4,"thorns":3,"tornado":2,"tornadoPulse":10,"truesilver":3,"zone":69},
"snaps": "a9c24b01 43e8c4cb 9f57bc24 f59bb440 6678caa3 a8a8f019 00aa512d 0d1483f9 2a3527a2 03e0de1a 3bc882ff 0bd6525a bdfd0551 2a029828 bba7125a e653cf29 998295f6"
"hooks": [1,122,4847,476,476,1436,1435,526,107,110,88,87,0,16368,1779,26,100,1,1,121,1,0,3],
"events": {"atk":634,"bounty":5,"deploy":19,"die":109,"dmg":1425,"engage":10,"fx":269,"heal":217,"layer":1,"leak":1,"skill":177,"spawn":117,"status":920},
"fx": {"beam":6,"blink":2,"displace":4,"dodge":3,"explode":5,"explosion":23,"featherArrow":5,"flame":3,"frostNova":36,"garrison":1,"garrisonGrant":1,"healField":2,"overclock":1,"palsy":1,"pull":2,"revive":4,"riposte":3,"rockslide":8,"shieldBreak":10,"slash":24,"summon":1,"swordRain":29,"swordStorm":5,"telegraph":3,"thorns":3,"tornado":2,"tornadoPulse":10,"truesilver":3,"zone":69},
"snaps": "a9c24b01 43e8c4cb 9f57bc24 f59bb440 6678caa3 a8a8f019 00aa512d 0d1483f9 2a3527a2 03e0de1a 8e417627 3f822e54 df66a000 eac29e3c 8acf1281 0f15b00b 8758da2f"
},
"roster-013": {
"about": "pass 0 band 5: chess_char_4_23_b chess_char_5_08_a chess_char_5_01_a chess_char_5_05_a chess_char_5_06_a chess_char_5_07_a chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b chess_char_5_02_a chess_char_5_03_a chess_char_5_10_a",
+79
View File
@@ -0,0 +1,79 @@
// test/sim/feedback5-raid-revive.test.js — community report of 2026-10-06 (item 44) 「突袭盟约的干员如果带被动技能,复活之后不会
// 立马突袭出去,还要等突袭CD」. A 突袭 member counts a running deploy-timed skill ("部署后…秒内": 缄默德克萨斯 S1–S3 …) as 技能
// 就绪 (bonds/addon/battle.js raidPoll, GitHub #49 / #109), so after a redeploy it jumps at once when nothing is in its range.
// PRTS's 复活 (M3茧甲, 埃芒加德) is 退场 + a 0-time / 0-cost redeploy (PRTS 盟约记录 备注) — a new deployment — but the remake
// revives in place: her window, over by then, stayed over and she waited out the 10 s idle time. Now an in-place revive
// starts a deploy-timed skill again (content/items/battle.js revivedInPlace); 不屈's real redeploy already did.
// Run: node --test test/sim/feedback5-raid-revive.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const TEX = 'chess_char_4_16_a';
const DUMMY = { enemy_rv: enemyRec({ key: 'enemy_rv', hp: 1e9, speed: 0, mass: 0, atk: 0 }) };
const RAID = { raidShip: { count: 3, active: true, tier: 1, layers: 10 } };
/**
* 缄默德克萨斯 (S3, a 6 s deploy-timed skill) in 突袭 with `extra` (items / band): she jumps to a parked enemy, stays past her
* window, the enemy is removed, she is knocked out with nothing in range, a new enemy parks away from her. Returns how long
* after the knock-out her next jump comes (null: none within 15 s) and her skill state right after the revive.
*/
function reviveAndJump({ items, bandId = null, bonds = {} }) {
const h = makeBattle({
seed: 3, autoFinish: false, timeLimit: 120, hooks: ['deploy', 'death'], defs: { enemies: DUMMY }, bandId,
bonds: { ...RAID, ...bonds }, enemies: [{ key: 'enemy_rv', pos: [9, 9] }], units: [{ chessId: TEX, row: 12, col: 3, skillIndex: 2, items }],
});
h.step();
const u = h.unit(TEX);
const jumps = () => h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial);
assert.ok(h.runUntil(() => jumps().length >= 1, 10), 'her first jump');
h.run(8);
assert.equal(u.skill.active, false, 'her 6 s window is over');
for (const e of h.b.enemies.filter((x) => x.alive)) h.b.kill(e, null);
const t0 = h.b.time;
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
h.spawn('enemy_rv', { pos: [12, 9] });
h.step();
const state = { alive: u.alive, active: u.skill.active, timeLeft: u.skill.timeLeft };
const n = jumps().length;
const jumped = h.runUntil(() => jumps().length > n && u.tileR === 12, 15);
checkInvariants(h.b);
return { after: jumped ? h.b.time - t0 : null, state, u, h };
}
test('M3茧甲 / 埃芒加德 revive 缄默德克萨斯 in place: her deploy-timed S3 runs again and she jumps at once (until 0.2.0 only after the 10 s idle)', () => {
for (const [label, opts] of [['M3茧甲', { items: ['chess_item_4_12_e_a'] }], ['埃芒加德', { bandId: 'band_ermengard' }]]) {
const { after, state } = reviveAndJump(opts);
assert.ok(state.alive, `${label}: revived in place`);
assert.ok(state.active, `${label}: her deploy-timed S3 runs again`);
assert.ok(after != null && after < 1, `${label}: jumped ${after?.toFixed(2)} s after the knock-out`);
}
});
test('不屈\'s 立刻重新部署 (a real redeploy) already jumped at once — unchanged', () => {
const { after } = reviveAndJump({ bonds: { indomShip: { count: 2, active: true, tier: 1, layers: 300 } } });
assert.ok(after != null && after < 1, `jumped ${after?.toFixed(2)} s after the knock-out`);
});
test('an in-place revive inside the window sets it back to its full length; 砾 revived by M3茧甲 gets a fresh barrier', () => {
const h = makeBattle({ seed: 3, autoFinish: false, timeLimit: 60, units: [{ chessId: TEX, row: 12, col: 3, skillIndex: 2, items: ['chess_item_4_12_e_a'] }] });
h.step();
const u = h.unit(TEX);
h.run(3);
assert.ok(u.skill.active && u.skill.timeLeft < u.skill.duration - 2, 'half-way through the window');
h.b.dealDamage(null, u, { amount: 1e9, type: 'true' });
h.step(2);
assert.ok(u.alive && u.skill.active && u.skill.timeLeft > u.skill.duration - 0.2, `back to the full ${u.skill.duration} s (${u.skill.timeLeft.toFixed(2)})`);
checkInvariants(h.b);
const g = makeBattle({ seed: 3, autoFinish: false, timeLimit: 60, units: [{ chessId: 'chess_char_2_12_a', row: 10, col: 4, items: ['chess_item_4_12_e_a'] }] });
g.step();
const gr = g.unit('chess_char_2_12_a');
g.run(12);
assert.ok(!gr.findBuff('gravel:rats'), 'the deployment barrier has run out');
g.b.dealDamage(null, gr, { amount: 1e9, type: 'true' });
g.step(2);
assert.ok(gr.alive && gr.findBuff('gravel:rats')?.shield > 0, 'a fresh barrier after the revive');
checkInvariants(g.b);
});