From e376cff5a8dc081e29e4455db92d5e794097c4e8 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 19:27:34 +0800 Subject: [PATCH] =?UTF-8?q?feedback3=20WF:=20a=20=E5=82=80=E5=84=A1?= =?UTF-8?q?=E5=B8=88=20leaves=20every=20switch=20at=20full=20HP=20(PRTS:?= =?UTF-8?q?=20the=20HP=20is=20reset=20during=20the=20switch,=20=E4=B8=8D?= =?UTF-8?q?=E6=AD=BB=20throughout)=20=E2=80=94=20a=20second=20=E9=98=BF?= =?UTF-8?q?=E6=88=88=E5=B0=94=20devour=20mark=20inside=20the=201-s=20switc?= =?UTF-8?q?h=20no=20longer=20leaves=20=E5=BD=92=E6=BA=9F=E5=B9=BD=E7=81=B5?= =?UTF-8?q?=E9=B2=A8's=20=E6=9B=BF=E8=BA=AB=20at=201=20HP=20(community=20r?= =?UTF-8?q?eport=20#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/SIM.md | 2 +- server/sim/professions.js | 13 +++++++++++-- test/sim/feedback2-doll.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/SIM.md b/docs/SIM.md index 3049d9f0..da3a7c98 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -1143,7 +1143,7 @@ table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡 | agent / hookmaster | can hit FLY (ranged reach); hook displacement comes from skills (generic: `force`) | | bearer | block 0 while the skill is active | | alchemist | ranged lob, can hit FLY | -| dollkeeper | fatal damage (no 不死 — a running 坚固维式重锤 window comes first) ⇒ a 1 s switch animation [ASSUMED length, also on a direct switch] (无敌, 不死, 阻回, 禁疗, 孤立, 缴械, 眩晕 / 冻结 / 睡眠 immune; ends the running skill and removes the statuses), then the <替身> for 20 s (bb duration): block 0 from the switch on, 阻回, HP = its own 替身 token's (风丸 纸偶), else its own max HP (归溟幽灵鲨: PRTS resets it "至最大值", the trait's 替身 HP bonus `max_hp` is 0, 风丸's 纸偶 has her HP); then the switch back (the same animation) at full HP; dies if the 替身 dies. Kit flag `dollNoAttack` (归溟幽灵鲨): the 替身 makes no normal attack and casts no skill. Hook `dollSwitch` switches it at once (归溟幽灵鲨 S2's end). Model form `'doll'` (PRTS 分支特性信息 傀儡师, DESIGN §22.11) | +| dollkeeper | fatal damage (no 不死 — a running 坚固维式重锤 window comes first) ⇒ a 1 s switch animation [ASSUMED length, also on a direct switch] (无敌, 不死, 阻回, 禁疗, 孤立, 缴械, 眩晕 / 冻结 / 睡眠 immune; ends the running skill and removes the statuses; the HP is set to the max at its start and again at its end — PRTS "切换途中重设自身生命至最大值" —, so a lethal 流失 inside it, held at 1 HP, never outlasts it), then the <替身> for 20 s (bb duration): block 0 from the switch on, 阻回, HP = its own 替身 token's (风丸 纸偶), else its own max HP (归溟幽灵鲨: PRTS resets it "至最大值", the trait's 替身 HP bonus `max_hp` is 0, 风丸's 纸偶 has her HP); then the switch back (the same animation) at full HP; dies if the 替身 dies. Kit flag `dollNoAttack` (归溟幽灵鲨): the 替身 makes no normal attack and casts no skill. Hook `dollSwitch` switches it at once (归溟幽灵鲨 S2's end). Model form `'doll'` (PRTS 分支特性信息 傀儡师, DESIGN §22.11) | | geek | loses 1–3 % max HP per second (bb hp_ratio), never lethal on its own | | merchant | −3 DP every 3 s (bb cost/interval); retreats when DP runs out | | skywalker | can block FLY enemies (蒂比's kit: only while airborne — 起飞, flag `liftoff`, which also releases the ground enemies she blocked) | diff --git a/server/sim/professions.js b/server/sim/professions.js index b23f279e..f7b7b237 100644 --- a/server/sim/professions.js +++ b/server/sim/professions.js @@ -128,15 +128,24 @@ const installDollkeeper = (battle, unit) => { }; const at = (extra) => ({ x: unit.x, y: unit.y, id: unit.id, ...extra }); // a switch animation: "切换动画开始时会清除自身一切Buff" (header: the running skill and the statuses), then 1 s of 不死 - // (the fatal hook below) 无敌 阻回 禁疗 孤立 强制缴械 and 眩晕 / 冻结 / 睡眠 immunity (beforeStatus below) + // (the fatal hook below) 无敌 阻回 禁疗 孤立 强制缴械 and 眩晕 / 冻结 / 睡眠 immunity (beforeStatus below). "切换途中重设自身 + // 生命至最大值": the HP is set to the max at the start (enter / leave) and again when the switch ends, so she always + // leaves it at full HP — a lethal 流失 inside the switch (无敌 does not stop it, 不死 holds it at 1 HP: a later 阿戈尔 + // devour mark on her) used to leave her the 替身's 20 s at 1 HP (community report #2) const startSwitch = () => { const sk = unit.skill; if (sk && sk.active && sk.kind !== 'passive') sk.end('substitute'); for (const b of unit.buffs.slice()) if (b.status) battle.removeBuff(unit, b); unit.trait.dollSwitching = true; const done = () => { unit.trait.dollSwitching = false; }; + const ended = () => { + done(); + if (!unit.alive || !unit.deployed) return; + unit.markDirty(); + unit.hp = unit.s.maxHp; + }; battle.addBuff(unit, { - key: 'trait:dollSwitching', duration: DOLL_SWITCH, onExpire: done, onRemove: done, + key: 'trait:dollSwitching', duration: DOLL_SWITCH, onExpire: ended, onRemove: done, flags: { invulnerable: true, noSp: true, noHeal: true, isolated: true, disarm: true }, }); }; diff --git a/test/sim/feedback2-doll.test.js b/test/sim/feedback2-doll.test.js index d2e8a5d2..bc9cedf7 100644 --- a/test/sim/feedback2-doll.test.js +++ b/test/sim/feedback2-doll.test.js @@ -158,6 +158,36 @@ test('S1 / S3 running at a lethal hit end with the switch (清除自身一切Buf assert.equal(s2.h.b.emit('dollSwitch', { unit: s2.g, reason: 'test', done: false }).done, false, 'already the 替身'); }); +test('E: a lethal 流失 inside the switch (a second 阿戈尔 devour mark) — she leaves it at full HP, both ways (PRTS "切换途中重设自身生命至最大值"; community report #2)', { skip }, () => { + // 乌尔比安 → 斯卡蒂 → 归溟幽灵鲨 → 深巡 → 幽灵鲨 → 隐现 (5 阿戈尔): 乌尔比安's mark switches her, 斯卡蒂's lands inside + // the 1 s switch (无敌 does not stop a 流失, 不死 holds it at 1 HP; the switch is no knock-out, so the mark is not + // cancelled). Until 0.1.3 she then fought the 替身's 20 s at 1 HP and fell to the first hit + const ids = ['chess_char_5_05_a', 'chess_char_3_05_a', G, 'chess_char_1_04_a', 'chess_char_2_07_a', 'chess_char_1_01_a']; + const h = makeBattle({ + seed: 4, timeLimit: 300, autoFinish: false, captureNoisy: true, hooks: ['damaged', 'death'], + units: ids.map((chessId, i) => ({ chessId, row: 10, col: 3 + i })), + bonds: { egirShip: { count: 5, active: true, tier: 2, layers: 0 } }, + }); + h.step(); + const g = h.unit(G); + const marks = h.hooksOf('damaged').filter((c) => c.target === g && c.dmg?.tags?.includes('bond:egir:devour')); + assert.equal(marks.length, 2, 'two devour marks resolve on her'); + assert.ok(g.alive && g.trait.doll && g.trait.dollSwitching, 'the first switched her'); + assert.ok(g.hp < 2, `the second, inside the switch, left 1 HP (不死; a regen tick aside): ${g.hp}`); + h.run(DOLL_SWITCH + 0.05); + assert.ok(g.trait.doll && !g.trait.dollSwitching, 'the 替身 fights'); + approx(g.hp, g.s.maxHp, 'at full HP when the switch ends'); + // the switch back: a lethal 流失 inside it is held at 1 HP, then she is the 本体 at full HP + assert.ok(h.runUntil(() => !g.trait.doll && g.trait.dollSwitching, 25), 'switching back'); + h.b.loseHp(g, 1e9, { source: null }); + assert.ok(g.alive && g.hp < 2, `not knocked out while switching: held at 1 HP (${g.hp})`); + h.run(DOLL_SWITCH + 0.05); + assert.ok(!g.trait.doll && !g.trait.dollSwitching && g.form === null, 'the 本体'); + approx(g.hp, g.s.maxHp, 'at full HP when the switch back ends'); + assert.equal(h.hooksOf('death').filter((c) => c.unit === g).length, 0, 'never knocked out'); + checkInvariants(h.b); +}); + test('a 本体 holding 不死 (a running 坚固维式重锤 window) does not switch: held at 1 HP (PRTS "受到足以致命的伤害且未持有不死的情况下")', { skip }, () => { const { h, g, lethal } = setup({ items: ['chess_item_3_09_e_a'] }); lethal();