From a4d69b6ba3e48a1e39b2eeb98facccc9d034db15 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sun, 4 Oct 2026 00:07:34 +0800 Subject: [PATCH] =?UTF-8?q?feedback2=20WD:=20=E6=B7=B1=E6=B0=B4=E5=8C=BA's?= =?UTF-8?q?=20tick=20is=20=E6=97=A0=E6=9D=A5=E6=BA=90=20true=20=E6=8C=81?= =?UTF-8?q?=E7=BB=AD=E4=BC=A4=E5=AE=B3,=20not=20=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E4=BC=A4=E5=AE=B3=20(tags=20dot=20/=20periodic=20/=20deepsea):?= =?UTF-8?q?=20=E7=BA=A0=E7=BC=A0=E8=97=A4=E8=94=93=20no=20longer=20turns?= =?UTF-8?q?=20fragile=20in=20water,=20=E9=94=A1=E4=BA=BA's=20=E5=87=8B?= =?UTF-8?q?=E6=95=9D=E9=AD=82=E7=81=B5=20raises=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/05-enemies-levels.md | 2 +- server/sim/content/devices.js | 21 ++++++++++++++++----- server/sim/content/enemies.js | 3 ++- test/content/enemies_bosses.test.js | 5 ++++- test/content/tokens_devices.test.js | 26 ++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/research/05-enemies-levels.md b/docs/research/05-enemies-levels.md index 80a01f11..1e58e410 100644 --- a/docs/research/05-enemies-levels.md +++ b/docs/research/05-enemies-levels.md @@ -75,7 +75,7 @@ Boss board: | `S` `E` `I` `O` | start / end / telin / telout | LOW | NONE | yes | | | `m` | mire | LOW | ALL | yes | a trigger every 1 s on it (PRTS 沼泽控制; the first on entering): enemies +1 layer of atk-speed −5 / move −5 % (重量 ≥ 3, the skill's `value`: +2 layers), operators +1 layer of atk-speed −5 only; max 10; cleared on leaving | | `g` | smog | LOW | ALL | yes | an operator here cannot be targeted by enemy **ranged** attacks | -| `d` | deepsea | LOW | ALL | yes | enemies take 40 dmg/s, atk-speed −60, move ×0.6 | +| `d` | deepsea | LOW | ALL | yes | enemies take 40 dmg/s (无来源 true 持续伤害, not 环境伤害), atk-speed −60, move ×0.6 | | `i` | infection | LOW | ALL | yes | allies on it and enemies crossing it get 70 true dmg/s, +20 % ATK, +20 ASPD for 300 s from their last contact (an enemy keeps it after leaving the tile; the tiles never switch off) | ### 2.4 Stage pool (`stageDatasDict`) diff --git a/server/sim/content/devices.js b/server/sim/content/devices.js index 86caad4c..51270496 100644 --- a/server/sim/content/devices.js +++ b/server/sim/content/devices.js @@ -14,7 +14,8 @@ // (fractions are ×100 ASPD) and move speed × (1 + moveMulPerStack × layers), an operator 1 layer of the // ASPD part only; at most maxStacks layers; cleared on leaving // 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them) -// 深水 d ground enemies on it: sea_drown[enemy].damage true dmg/s, ASPD attack_speed (×100), move × move_speed +// 深水 d ground enemies on it: sea_drown[enemy].damage dmg/s (无来源 true 持续伤害, not 环境伤害: tags dot / +// periodic / deepsea), ASPD attack_speed (×100), move × move_speed // 活性源石 i a unit on it (allies and ground enemies) gets a timed effect: damage true dmg/s, ATK + atk, ASPD + // attack_speed for `duration` s from its last contact — an enemy keeps it after walking off; one effect // per unit, its time starts again while the unit is on the tile; the tiles never switch off @@ -333,10 +334,20 @@ function buildTerrain(battle, st) { } } -const terrainDamage = (battle, amount) => (ctx) => { +/** 活性源石's tick: true damage no unit deals (无来源), tagged 'terrain' = 环境伤害 ("受到来自自然环境的伤害" content reads it). */ +const infectionDamage = (battle, amount) => (ctx) => { if (amount > 0 && ctx.unit.alive) battle.dealDamage(null, ctx.unit, { amount, type: 'true', canDodge: false, tags: ['terrain'] }); }; +/** + * 深水区's 【水蚀】 tick (PRTS 涨潮控制 技能3 深水: "每秒受到40点无来源真实持续伤害(不属于环境伤害,不会触发受击回复)"; PRTS 伤害分类 + * lists 深水区/涨潮水蚀 as BUFF damage): 无来源 true 持续伤害 — tags 'dot' (锡人's 凋敝魂灵 raises it), 'periodic' and + * 'deepsea' (the 免疫水蚀 swimmers cancel it), no 'terrain' (not 环境伤害), no 受击回复 (noSp). + */ +const deepWaterDamage = (battle, amount) => (ctx) => { + if (amount > 0 && ctx.unit.alive) battle.dealDamage(null, ctx.unit, { amount, type: 'true', canDodge: false, sourceless: true, noSp: true, tags: ['dot', 'periodic', 'deepsea'] }); +}; + function enterTerrain(battle, st, u, code) { const m = u.mem; if (m.terrain === code) return; @@ -350,7 +361,7 @@ function enterTerrain(battle, st, u, code) { if (code === TERRAIN.smog) battle.addBuff(u, { key: BUFF[code], flags: { stealth: true } }); else if (code === TERRAIN.deepsea) { const D = st.deepsea; - battle.addBuff(u, { key: BUFF[code], mods: { aspd: D.aspd, moveMul: D.moveMul }, interval: 1, onTick: terrainDamage(battle, D.damage) }); + battle.addBuff(u, { key: BUFF[code], mods: { aspd: D.aspd, moveMul: D.moveMul }, interval: 1, onTick: deepWaterDamage(battle, D.damage) }); } // mire layers are applied by tickMire, 活性源石 by touchInfection (every tick on the tile) } @@ -365,7 +376,7 @@ function enterTerrain(battle, st, u, code) { * 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) + * 身上除白名单外所有Buff") and contact gives it again while the unit is on the tile [ASSUMED]. The tick (infectionDamage) * is true damage no unit deals (无来源), tagged 'terrain' = 环境伤害 (PRTS 自然环境 lists 活性源石), not 'dot' [ASSUMED: * PRTS 伤害分类's list of BUFF damage does not name it]. */ @@ -374,7 +385,7 @@ function touchInfection(battle, st, u) { const key = BUFF[TERRAIN.infection]; const b = u.findBuff(key); if (b) { if (b.timeLeft < I.duration) b.timeLeft = I.duration; return; } - battle.addBuff(u, { key, duration: I.duration, mods: { atkPct: I.atk, aspd: I.aspd }, interval: 1, onTick: terrainDamage(battle, I.damage) }); + battle.addBuff(u, { key, duration: I.duration, mods: { atkPct: I.atk, aspd: I.aspd }, interval: 1, onTick: infectionDamage(battle, I.damage) }); } /** Layers an enemy gains per 【陷入沼泽】 trigger at 重量 ≥ heavyWeight (PRTS 沼泽控制: "若其重量大于等于3,改为获得2层"). */ diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 2b076bd2..d0f6c870 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -1122,7 +1122,8 @@ function kitDeepsea(ab, { swim = false, drown = false }) { if (drown && wet) { const v = T(ab, 'Drown.damage') ?? 0; if (v > 0) b.dealDamage(null, e, { ...periodicDamage(v * dt), tags: ['dot', 'periodic', 'drown'] }); } a.wet = wet; }, - hitIn(c, b, e, a) { if (swim && a.wet && c.dmg.tags && c.dmg.tags.some((t) => t === 'terrain' || t === 'deepsea' || t === 'drown')) c.dmg.cancel = true; }, + // 免疫水蚀: the deep-water tick (devices.js, tag 'deepsea') and drowning — not 环境伤害 ('terrain', e.g. 活性源石) + hitIn(c, b, e, a) { if (swim && a.wet && c.dmg.tags && c.dmg.tags.some((t) => t === 'deepsea' || t === 'drown')) c.dmg.cancel = true; }, }]; } diff --git a/test/content/enemies_bosses.test.js b/test/content/enemies_bosses.test.js index f64aec67..c50ce1ad 100644 --- a/test/content/enemies_bosses.test.js +++ b/test/content/enemies_bosses.test.js @@ -397,8 +397,11 @@ test(`${nm('enemy_1158_divman')}: in deep water ATK +Swim.atk and stealth; immun h.run(0.5); approx(e.s.atk, dry.s.atk * (1 + tb('enemy_1158_divman', 'Swim.atk'))); assert.ok(e.s.flags.stealth && !dry.s.flags.stealth); - h.b.dealDamage(null, e, { amount: 40, type: 'true', tags: ['terrain'] }); + // 免疫水蚀: devices.js' deep-water tick (tag 'deepsea'); 环境伤害 ('terrain', e.g. 活性源石) still lands + h.b.dealDamage(null, e, { amount: 40, type: 'true', canDodge: false, sourceless: true, noSp: true, tags: ['dot', 'periodic', 'deepsea'] }); assert.equal(e.hp, e.s.maxHp); + h.b.dealDamage(null, e, { amount: 40, type: 'true', tags: ['terrain'] }); + assert.equal(e.hp, e.s.maxHp - 40); }); for (const key of ['enemy_1160_hvyslr', 'enemy_1160_hvyslr_2']) { diff --git a/test/content/tokens_devices.test.js b/test/content/tokens_devices.test.js index 97e2f4ff..4a932bc0 100644 --- a/test/content/tokens_devices.test.js +++ b/test/content/tokens_devices.test.js @@ -682,6 +682,32 @@ test('深水 (act2 m04): ground enemies in deep water take damage/s, ASPD −60, checkInvariants(h.b); }); +// PRTS 涨潮控制 技能3 深水: "【水蚀】的敌方单位每秒受到40点无来源真实持续伤害(不属于环境伤害,不会触发受击回复)" +test('深水: the 【水蚀】 tick is 无来源 true 持续伤害, not 环境伤害 (tags dot / periodic / deepsea, no terrain, no 受击回复) — 纠缠藤蔓 does not turn fragile in it', REAL, () => { + const bb = ds.getStage('act2autochess_m04').special.deepsea.bb; + const dmg = bb['sea_drown[enemy].damage']; + const wait = (pos) => ({ motion: 'WALK', start: pos, end: [9, 2], checkpoints: [{ type: 'WAIT', time: 99 }] }); + const ticks = []; + const h = makeBattle({ stageId: 'act2autochess_m04', defs: { enemies: { enemy_dummy: dummy({ speed: 1 }) } }, + enemies: [{ key: 'enemy_dummy', pos: [11, 6], route: wait([11, 6]) }, { key: 'enemy_2052_smgia', pos: [10, 6], route: wait([10, 6]) }], autoFinish: false, timeLimit: 30, + setup: (b) => b.on('hit', ({ source, target, dmg: d }) => { if (target.defId === 'enemy_dummy') ticks.push({ source, type: d.type, tags: [...d.tags], sourceless: d.sourceless, noSp: d.noSp }); }) }); + h.step(); + h.run(5); + const e = h.enemy('enemy_dummy'), vine = h.enemy('enemy_2052_smgia'); + assert.deepEqual([terrainAt(h.b, 11, 6), terrainAt(h.b, 10, 6)], ['deepsea', 'deepsea']); + assert.equal(ticks.length, 5); + for (const t of ticks) { + assert.equal(t.source, null); + assert.equal(t.type, 'true'); + assert.deepEqual(t.tags, ['dot', 'periodic', 'deepsea']); + assert.ok(t.sourceless && t.noSp); + } + approx(1e7 - e.hp, 5 * dmg, 1e-9); + assert.equal(vine.findBuff('ab:natureWeak'), null, '受到来自自然环境的伤害 does not fire'); + approx(vine.s.maxHp - vine.hp, 5 * dmg, 1e-9, '40/s, not ×2'); + checkInvariants(h.b); +}); + test('活性源石 (act1 m04): units on it take damage/s and gain ATK/ASPD (allies and ground enemies)', REAL, () => { const bb = ds.getStage('act1autochess_m04').special.infection.bb; const g = guard({ stats: { atk: 1000, maxHp: 1e5 } });