From 0907c3b7a4cf0cc831cbdd05e6f86ec2cc5b77f8 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 00:07:15 +0800 Subject: [PATCH] =?UTF-8?q?feedback2=20WD:=20a=20=E9=87=8D=E7=94=9F=20clea?= =?UTF-8?q?rs=20the=20lasting=20=E6=B4=BB=E6=80=A7=E6=BA=90=E7=9F=B3=20eff?= =?UTF-8?q?ect=20(an=20ember=20that=20crossed=20the=20tile=20no=20longer?= =?UTF-8?q?=20dies=20from=20its=20ticks);=20a=20relocated=20operator=20kee?= =?UTF-8?q?ps=20it,=20an=20operator=20drains=20past=20300=20s=20only=20by?= =?UTF-8?q?=20the=20[ASSUMED]=20refresh=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/sim/content/devices.js | 10 ++++--- server/sim/content/enemies.js | 14 ++++++--- test/content/tokens_devices.test.js | 46 ++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/server/sim/content/devices.js b/server/sim/content/devices.js index 9531fa36..517a9166 100644 --- a/server/sim/content/devices.js +++ b/server/sim/content/devices.js @@ -356,10 +356,12 @@ function enterTerrain(battle, st, u, code) { * global_tile_infection_1 「目标:可控感染」 "踏过活性源石地块的敌人不再持续损失生命值" switches the lasting HP loss off, * so without it the loss continues. One effect per unit (PRTS 作战机制: "同名buff的默认叠加策略buff只能表现出一个"): a * unit that already carries it gets its full `duration` back and keeps its per-second rhythm — no second effect, no - * extra tick [ASSUMED: the time counts from the last contact]. An operator deployed on it is always in contact and - * keeps draining, as officially. Leaving the field drops it with every buff; a 重生 keeps it (enemies.js rebirthCleanse - * keeps source-less buffs [ASSUMED]). The tick (terrainDamage) is true damage no unit deals (无来源), tagged 'terrain' = - * 环境伤害 (PRTS 自然环境 lists 活性源石), not 'dot' [ASSUMED: PRTS 伤害分类's list of BUFF damage does not name it]. + * extra tick [ASSUMED: the time counts from the last contact — so an operator deployed on it, always in contact, drains + * past `duration`]. An operator moved off the tile (Battle.relocate: 乌尔比安 S3, 夕's 小自在 …) keeps it for its time; + * leaving the field drops it with every buff; a 重生 clears it (enemies.js rebirthCleanse: PRTS 特殊机制 §重生 "清空自身 + * 身上除白名单外所有Buff") and contact gives it again while the unit is on the tile [ASSUMED]. The tick (terrainDamage) + * is true damage no unit deals (无来源), tagged 'terrain' = 环境伤害 (PRTS 自然环境 lists 活性源石), not 'dot' [ASSUMED: + * PRTS 伤害分类's list of BUFF damage does not name it]. */ function touchInfection(battle, st, u) { const I = st.infection; diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 9fd75b22..2b076bd2 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -749,9 +749,12 @@ function freeAllPrisoners(b) { * buff an operator / summon / device put on it and every source-less catalogue status (晕眩, 减速, 恐惧, 脆弱, 诱导 … — * buffs.js STATUS). Kept [ASSUMED: the whitelist]: its talents and traits (`persist` buffs, and what it or another enemy * gave it — 锏's 抵抗 is a self-applied status, enemy auras refresh every few tenths of a second anyway) and the field's - * state buffs without a source (terrain, airflow — re-applied by position — and element burst locks). Without it a 逐火 - * knocked out while feared (叙拉古 / 妮芙: 恐惧 makes it unblockable) stayed unblockable — so, 隐匿, untargetable — as an - * ember until the fear ran out. + * state buffs without a source (on-tile terrain, airflow — re-applied by position — and element burst locks). Without it + * a 逐火 knocked out while feared (叙拉古 / 妮芙: 恐惧 makes it unblockable) stayed unblockable — so, 隐匿, untargetable — + * as an ember until the fear ran out. 活性源石's lasting effect (devices.js touchInfection) is no field state but a timed + * buff that outlives the tile, so it is cleared like any buff (PRTS 特殊机制 非首次标记: "如无特殊说明,也默认同常规Buff一样 + * 可被重生清除"); contact gives it again while the enemy stands on the tile. Kept, its 1 s ticks — each a hit of a husk's + * 特殊生命值 — killed a 逐火 ember that had crossed the tile long before it could stand up (review of GitHub #33 item 6). */ /** * The end of a 重生 (PRTS 特殊机制 §重生 "重生结束时,重置自身的通用技能与当前形态的技能冷却为初始冷却"): every ability with a @@ -761,10 +764,13 @@ function rebirthCooldowns(e) { for (const s of (e.mem.ab && e.mem.ab.list) || []) if (s.cd != null) s.left = num(s.icd, 0); } +/** devices.js' lasting 活性源石 effect: a timed buff, not field state — cleared at a 重生 (rebirthCleanse). */ +const INFECTION_BUFF = 'terrain:infection'; + function rebirthCleanse(b, e) { for (const x of e.buffs.slice()) { if (x.persist || (x.source && x.source.side === 'enemy')) continue; - if (x.source || x.status) b.removeBuff(e, x); + if (x.source || x.status || x.key === INFECTION_BUFF) b.removeBuff(e, x); } } diff --git a/test/content/tokens_devices.test.js b/test/content/tokens_devices.test.js index 78961983..d135e001 100644 --- a/test/content/tokens_devices.test.js +++ b/test/content/tokens_devices.test.js @@ -7,6 +7,7 @@ import { hasGeneratedData, getDefaultSource } from '../../server/sim/simdata.js' import { genericKit } from '../../server/sim/content/generic.js'; import { spawnYanyou, spawnMapChar, TOKEN_IDS, wolfShadows, tileFree, findSummonTile, summonToken } from '../../server/sim/content/tokens.js'; import { startColdWind, kjeragColdWind, activateTurrets, terrainAt, deviceOverridesOf } from '../../server/sim/content/devices.js'; +import { HUSK_REBIRTH } from '../../server/sim/content/enemies.js'; const REAL = { skip: !hasGeneratedData() }; const ds = getDefaultSource(); @@ -731,18 +732,55 @@ test('活性源石: one effect per unit — contact restarts its duration, it en h.run(3); assert.equal(e.hp, hp0 - loss, 'nothing after the end'); approx(e.s.atk, 500, 1e-9); - // an operator deployed on it is always in contact: it keeps draining past `duration` (no lifetime of the tiles) + // an operator deployed on it is always in contact: it keeps draining past `duration` (no lifetime of the tiles; 3 × + // `duration`, so that a shut-off at `duration` followed by the effect's own `duration` would still show) const g = guard({ stats: { atk: 1000, maxHp: 1e5 } }); - const h2 = makeBattle({ stage, defs: { chess: { test_guard: g } }, units: [{ chessId: 'test_guard', row: 11, col: 6 }], autoFinish: false, timeLimit: 60 }); + const h2 = makeBattle({ stage, defs: { chess: { test_guard: g } }, units: [{ chessId: 'test_guard', row: 11, col: 6 }], autoFinish: false, timeLimit: 120 }); h2.step(); - h2.run(2 * bb.duration); + h2.run(3 * bb.duration); const u = h2.unit('test_guard'); - approx(u.stats.taken, 2 * bb.duration * bb.damage, 1e-9, 'ally damage'); + approx(u.stats.taken, 3 * bb.duration * bb.damage, 1e-9, 'ally damage'); approx(u.s.atk, 1000 * (1 + bb.atk), 1e-9); + // moved off the tile (Battle.relocate, as 乌尔比安 S3 does), it keeps the effect for its time, then it ends + assert.ok(h2.b.relocate(u, 11, 7)); + const taken0 = u.stats.taken; + h2.run(3); + assert.ok(u.findBuff('terrain:infection'), 'kept after the move'); + approx(u.stats.taken - taken0, 3 * bb.damage, 1e-9, 'still draining'); + h2.run(bb.duration); + assert.equal(u.findBuff('terrain:infection'), null, `gone ${bb.duration} s after the move`); + approx(u.s.atk, 1000, 1e-9); checkInvariants(h.b); checkInvariants(h2.b); }); +test('活性源石: a 重生 clears the lasting effect — a 深池逐火战士 that crossed the tile, knocked out off it, stands up from its ember after 1 + 10 s', REAL, () => { + const key = 'enemy_1288_duskls'; + const delay = ds.getEnemy(key).talent['Revive[Trigger].interval']; + // it crosses (11,6) going left, then waits on (11,4), off the tile + const route = { motion: 'WALK', start: [11, 8], end: [9, 2], checkpoints: [{ type: 'MOVE', pos: [11, 4] }, { type: 'WAIT', time: 99 }] }; + const h = makeBattle({ stageId: 'act1autochess_m04', enemies: [{ key, route }], autoFinish: false, timeLimit: 120 }); + h.step(); + const e = h.enemy(key); + const onTile = () => Math.round(e.y) === 11 && Math.round(e.x) === 6; + assert.ok(h.runUntil(onTile, 20), 'steps on (11,6)'); + assert.ok(h.runUntil(() => Math.round(e.x) === 4, 20), 'waits on (11,4)'); + assert.ok(e.findBuff('terrain:infection'), 'carries the effect off the tile'); + const max = e.s.maxHp; + h.b.dealDamage(null, e, { amount: e.hp + 1e6, type: 'true', canDodge: false }); + assert.ok(e.alive, 'knocked out into its ember'); + assert.equal(e.findBuff('terrain:infection'), null, 'the 重生 cleared it'); + h.run(HUSK_REBIRTH + delay - 0.5); + assert.ok(e.alive && e.s.maxHp < max, 'still the ember'); + assert.equal(e.hp, e.s.maxHp, 'no tick took one of its hits'); + h.run(1); + assert.ok(e.alive); + assert.equal(e.s.maxHp, max, `stood up ${HUSK_REBIRTH} + ${delay} s after the knock-out`); + assert.equal(e.hp, max); + assert.equal(e.findBuff('terrain:infection'), null); + checkInvariants(h.b); +}); + test('盟约寒风: every interval all enemies turn cold; a gust on cold enemies freezes them; kjeragColdWind uses live layers', REAL, () => { const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, enemies: [{ key: 'enemy_dummy', pos: [10, 6] }, { key: 'enemy_dummy', pos: [11, 8] }], autoFinish: false, timeLimit: 60 }); h.step();