From 215d52efe30576b2d6e64f09619c3ec1a46fe5a5 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 22:58:11 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feedback2=20WC:=20=E9=98=BF=E6=88=88?= =?UTF-8?q?=E5=B0=94's=20devour=20cancels=20the=20pending=20marks=20of=20a?= =?UTF-8?q?=20unit=20it=20knocked=20out,=20also=20when=20that=20unit=20is?= =?UTF-8?q?=20revived=20at=20once,=20so=20the=20revived=20members=20stay?= =?UTF-8?q?=20standing=20(GitHub=20#33=20item=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/02-bonds.md | 4 +- server/sim/content/bonds/core.js | 14 ++++++- server/sim/content/items.js | 5 ++- server/sim/content/items/battle.js | 3 +- test/content/bonds_core.test.js | 65 ++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/docs/research/02-bonds.md b/docs/research/02-bonds.md index 7acea6b8..e057f7b0 100644 --- a/docs/research/02-bonds.md +++ b/docs/research/02-bonds.md @@ -373,12 +373,12 @@ Members (11 chess, 10 in current shop pool; by tier in shop: {'1': 1, '2': 1, '3 - **[3 distinct]** 阿戈尔 members maxHP x(1 + 0.35 + 0.01*L). - **[3 distinct (battle start)]** Devour (吞噬): see algorithm. -- **[5 distinct]** The first 3 阿戈尔 members to be knocked out (first time each) revive immediately (max_free_respawn_cnt 3). +- **[5 distinct]** The first 3 阿戈尔 members to be knocked out (first time each) revive immediately (max_free_respawn_cnt 3). Devour knock-outs count: a chain that knocks out 3 members spends the 3 revives at battle start (and those members stay standing, step 4). - Algorithm: 1. Order: 阿戈尔 members sorted leftmost first, then topmost ("更靠左和靠上"). 2. Each 阿戈尔 in order marks the unit on the tile directly in front of it (its facing direction) and also the front-tile unit of every 阿戈尔 it has marked (chain). It never marks itself, a unit it already marked, or a unit that marked it. 3. The marker immediately gains the base ATK of every unit it marked (added to base ATK at the final stage) and their block counts. - 4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target that dies cancels its remaining pending marks. + 4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target knocked out for the first time has its pending marks cancelled ("目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果") — also when it is revived at once (the 5-tier revive, 埃芒加德, M3茧甲), so a revived member is not devoured again. [ASSUMED] a marker knocked out during the pass gives no further mark either, revived or not (PRTS names only the target). DESIGN §22.3. 5. Each devoured unit adds layers to 阿戈尔 equal to its tier (1-6), once per unit per battle. 6. (5-member tier) Revive is implemented as: when the unit leaves the field for any reason other than being moved, its next deployment has 0 redeploy time and 0 cost (PRTS). Devour layers: each devoured unit adds its tier once per battle (refreshes next round). - Formulas: `hpMultiplier = 1.35 + 0.01*L`; `devourDamage = 5000`; `layersPerDevoured = tier of devoured unit` diff --git a/server/sim/content/bonds/core.js b/server/sim/content/bonds/core.js index ac509e97..d7ea2492 100644 --- a/server/sim/content/bonds/core.js +++ b/server/sim/content/bonds/core.js @@ -339,7 +339,9 @@ function installLaterano(battle, pid, bb, members) { * 物理流失 (PRTS 盟约记录: "造成5000点物理流失", 修正 "【吞噬】的物理流失来源为被付与目标自身;单位被【吞噬】击杀时,击杀来源始终为 * 对应标记的付与来源"; PRTS 作战机制: a 物理流失 "会受到目标当前防御力…影响而相应衰减") — less the target's DEF as a physical hit * (its own source: no DEF ignore), then battle.loseHp: no shields, dodge or damage multipliers (DEF-free until 0.1.1); the - * kill is credited to the marker — in marking order. A unit that died cancels its remaining marks (as target and as marker). + * kill is credited to the marker — in marking order. A unit knocked out during the pass cancels its remaining marks, also + * when it is revived at once (the 5-tier 立刻复活, 埃芒加德 / M3茧甲): as target (PRTS 盟约记录 "目标首次被击倒后解除自身被付与 + * 但还未触发的【吞噬】效果") and as marker (the remake's rule since 0.1.0, [ASSUMED]: PRTS names only the target). * Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields). * Tokens / devices / empty tiles are never devoured. */ @@ -382,8 +384,14 @@ function devour(battle, pid, bb, members) { } const amount = num(bb.damage_value, 0); const layered = new Set(); + // knocked out during the pass = off the field, or in another deployment than when the marks were placed (items + // deploymentOf: the 5-tier revive redeploys, 埃芒加德 / M3茧甲 revive in place) — a revived member was standing again + // when its pending marks used to knock it out a second time and spend every revive at t = 0 (GitHub #33) + const dep = new Map(); + for (const pair of marks) for (const u of pair) if (!dep.has(u)) dep.set(u, items.deploymentOf(u)); + const knocked = (u) => !u.alive || items.deploymentOf(u) !== dep.get(u); for (const [m, t] of marks) { - if (!t.alive || !m.alive) continue; + if (knocked(t) || knocked(m)) continue; S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id }); if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] }); if (!layered.has(t)) { @@ -408,6 +416,8 @@ function installEgir(battle, pid, bb, members) { // fell —, or its own home when it fell on another board piece's home; PRTS 卫戍协议/帮助 §作战阶段 单位部署) with full // HP, SP reset and `deploy` effects (卡西米尔 / 叙拉古). Death priority 11: before 不屈 (10), // whose redeploy "also consumes a 复活 charge" — with this order the charge is always the one used, same outcome. + // A member the battle-start devour knocks out spends a charge like any other first knock-out; the marks still pending + // on it are cancelled (devour), so it stays standing. const memberSet = new Set(members); const max = Math.max(0, Math.floor(num(bb.max_free_respawn_cnt, 0))); const st = { knocked: new Set(), revives: 0 }; diff --git a/server/sim/content/items.js b/server/sim/content/items.js index 52339af9..3633de86 100644 --- a/server/sim/content/items.js +++ b/server/sim/content/items.js @@ -1,6 +1,7 @@ // server/sim/content/items.js — equipment & Arts content (research 04, docs/DATA.md §5). // Battle side: install(battle) is called once per Battle by content/index.js (inside try/catch). // Prep side: registerMeta(registry) is called once at server boot by match/effectsMeta.js. -// lendItemEffects(battle, fromUnit, toUnit, { maxTier, duration }) is used by 萨尔贡 × 娜仁图亚 (bands). -export { install, lendItemEffects } from './items/battle.js'; +// lendItemEffects(battle, fromUnit, toUnit, { maxTier, duration }) is used by 萨尔贡 × 娜仁图亚 (bands); deploymentOf(u) +// (the deployment a unit is in — a redeploy or an in-place 复活 opens a new one) by 阿戈尔's devour (bonds/core.js). +export { install, lendItemEffects, deploymentOf } from './items/battle.js'; export { registerMeta } from './items/meta.js'; diff --git a/server/sim/content/items/battle.js b/server/sim/content/items/battle.js index ecf07141..970075a3 100644 --- a/server/sim/content/items/battle.js +++ b/server/sim/content/items/battle.js @@ -238,8 +238,9 @@ function hammerState(battle, rt, u) { * [ASSUMED a deployment], 阿戈尔's 立刻复活), and an in-place 复活 (M3茧甲, 埃芒加德: revivedInPlace) opens a new one too * [ASSUMED] — PRTS (M3茧甲 / 埃芒加德 / 阿戈尔 备注) "“复活”的实现方式为:受益者因移动之外的原因退场时下次部署的再部署时间和 * 费用归零": officially a revive is a 0-time / 0-cost redeploy; the remake keeps the unit standing instead. + * Also read by 阿戈尔's devour (bonds/core.js): a unit whose deployment changed during the pass was knocked out. */ -function deploymentOf(u) { return `${u.deploySeq}:${u.mem.revives | 0}`; } +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; } diff --git a/test/content/bonds_core.test.js b/test/content/bonds_core.test.js index 1bacf4fd..a5a7c1af 100644 --- a/test/content/bonds_core.test.js +++ b/test/content/bonds_core.test.js @@ -418,6 +418,71 @@ test('阿戈尔 5: the first 3 members knocked out for the first time redeploy a assert.ok(!h3.unit('g0_a').alive); }); +test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks out revives, its pending marks are cancelled and it stays standing (normal and 联防)', () => { + // one row, all facing right: 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现. PRTS 盟约记录 阿戈尔 备注 "标记按付与顺序触发 + // 【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; until 0.1.1 the marks still pending on a revived member + // knocked it out again, so all 3 revives went at t = 0 and the members stayed down + const ids = ['chess_char_6_04_a', 'chess_char_5_05_a', 'chess_char_2_07_a', 'chess_char_3_09_a', 'chess_char_1_04_a', 'chess_char_1_01_a']; + for (const kind of ['normal', 'unite']) { + const h = makeBattle({ + kind, units: ids.map((chessId, i) => ({ chessId, row: 10, col: 2 + i })), bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, + hooks: ['damaged', 'death', 'deploy'], captureNoisy: true, autoFinish: false, timeLimit: 60, + }); + h.step(1); + const [skadi, ulpia, ghost, hn, deep, fodder] = ids.map((id) => h.unit(id)); + // 浊心斯卡蒂 marks the whole row and resolves first; every later mark is on a unit already knocked out (乌尔比安's), or + // comes from one (幽灵鲨's, 海霓's, 深巡's) + assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), + [ulpia, ghost, hn, deep, fodder].map((t) => [skadi.defId, t.defId]), kind); + for (const u of [ghost, hn, deep]) { + assert.equal(h.hooksOf('death').filter((c) => c.unit === u && c.reason === 'killed').length, 1, `${kind}: ${u.defId} knocked out once`); + assert.equal(h.hooksOf('deploy').filter((c) => c.unit === u && !c.initial).length, 1, `${kind}: ${u.defId} revived at once`); + } + h.run(3); + for (const u of [skadi, ulpia, ghost, hn, deep]) assert.ok(u.alive && u.deployed && u.hp > 0, `${kind}: ${u.defId} standing`); + assert.ok(!fodder.alive, `${kind}: the fodder (no member) stays down`); + // the 3 revives went to the first knock-outs at t = 0, as officially: the next knock-out of a member is final + h.b.dealDamage(null, ghost, { amount: 1e9, type: 'true' }); + assert.ok(!ghost.alive, `${kind}: no revive left`); + checkInvariants(h.b); + } +}); + +test('阿戈尔 devour: a unit revived during the pass is out of it — a marker back from the 5-tier revive gives no mark, a target 埃芒加德 revived in place takes none', () => { + // g1 (10,3) → g2 (10,4, 3000 × 1.35 HP: knocked out by g1's mark, revived) → g3 (10,5) → fodder (10,6); g4 / g5 face empty tiles + const list = [ + ['g1_a', ['egirShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip'], { stats: { maxHp: 20000 } }], + ['g4_a', ['egirShip']], ['g5_a', ['egirShip']], ['fod_a', ['preciShip'], { tier: 2, stats: { maxHp: 20000 } }], + ]; + const units = [ + { chessId: 'g1_a', row: 10, col: 3 }, { chessId: 'g2_a', row: 10, col: 4 }, { chessId: 'g3_a', row: 10, col: 5 }, { chessId: 'fod_a', row: 10, col: 6 }, + { chessId: 'g4_a', row: 12, col: 3 }, { chessId: 'g5_a', row: 12, col: 5 }, + ]; + const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: ['damaged', 'deploy'], captureNoisy: true }); + h.step(1); + const [g2, g3, f] = ['g2_a', 'g3_a', 'fod_a'].map((id) => h.unit(id)); + assert.ok(g2.alive && h.hooksOf('deploy').some((c) => c.unit === g2 && !c.initial), 'g2 knocked out and revived'); + // [ASSUMED] (PRTS names only the target): g2's own marks on g3 and the fodder are cancelled with its knock-out + assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), + [['g1_a', 'g2_a'], ['g1_a', 'g3_a'], ['g1_a', 'fod_a'], ['g3_a', 'fod_a']]); + close(f.hp, 20000 - 2 * 5000, 1e-6, 'the fodder: g1 and g3 only'); + close(g2.s.atk, 1000 + 1000 + 1000, 1e-6, 'g2 keeps the base ATK it gained at marking (a persistent buff) [ASSUMED]'); + assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 1 + 2, 'each devoured unit adds its tier once'); + checkInvariants(h.b); + // 3 members + 埃芒加德 (命结之秘, the first 3 knocked-out operators revive in place — a new deployment, items revivedInPlace): + // g1 → g2 → fodder (3000 HP): g1's mark knocks the fodder out, 埃芒加德 revives it, g2's mark on it is cancelled + const list2 = [['g1_a', ['egirShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 20000 } }], ['g3_a', ['egirShip']], ['fod_a', ['preciShip'], { tier: 2, stats: { maxHp: 3000 } }]]; + const units2 = [{ chessId: 'g1_a', row: 10, col: 3 }, { chessId: 'g2_a', row: 10, col: 4 }, { chessId: 'fod_a', row: 10, col: 5 }, { chessId: 'g3_a', row: 12, col: 3 }]; + const e = makeBattle({ defs: defsOf(list2), units: units2, bandId: 'band_ermengard', bonds: { egirShip: bondOn(3, 0, null, [3, 5]) }, hooks: ['damaged'], captureNoisy: true }); + e.step(1); + const fe = e.unit('fod_a'); + assert.deepEqual(tagged(e, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a']]); + assert.ok(fe.alive, 'revived in place'); + close(fe.hp, fe.s.maxHp, 1e-6, 'at full HP: no second mark'); + assert.deepEqual(e.eventsOf('fx').filter((x) => x[1] === 'revive').map((x) => x[4].left), [2], 'one 埃芒加德 revive spent, not two'); + checkInvariants(e.b); +}); + // --------------------------------------------------------------------------------------------------------------------- // 叙拉古 From 73834190095cec94f9ecd9c03887d7769c384e4f Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 22:58:11 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feedback2=20WC:=20=E4=B9=8C=E5=B0=94?= =?UTF-8?q?=E6=AF=94=E5=AE=89=20S3=20throws=20the=20anchor=20onto=20his=20?= =?UTF-8?q?own=20tile=20while=20he=20blocks,=20so=20he=20stays=20put=20and?= =?UTF-8?q?=20leaves=20no=20marker=20(PRTS=20note;=20GitHub=20#33=20item?= =?UTF-8?q?=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/sim/content/kits/tier5.js | 32 ++++++++++++++++++++------------ test/content/kits_t5.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/server/sim/content/kits/tier5.js b/server/sim/content/kits/tier5.js index b6859140..1fbf763a 100644 --- a/server/sim/content/kits/tier5.js +++ b/server/sim/content/kits/tier5.js @@ -763,8 +763,9 @@ const KITS = { // --------------------------------------------------------------------------------------------------------------- // 乌尔比安 — S3 必须开辟的通路 (25 s, CUSTOM_RANGE row ahead): max HP/ATK +, throws an anchor forward that stops on the - // first enemy or at max distance: 135 % ATK phys + 6 s stun around it (projectile_range); moves onto the anchor tile when - // deployable (a 从不混淆的方向 marker keeps his tile) and returns at skill end. + // first enemy or at max distance — on his own tile while he blocks: 135 % ATK phys + 6 s stun around it + // (projectile_range); moves onto the anchor tile when deployable and not his own (a 从不混淆的方向 marker keeps his tile) + // and returns at skill end. // 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 @@ -815,23 +816,30 @@ const KITS = { kind: 'duration', mods: mods({ hpPct: num(bb.max_hp), atkPct: num(bb.atk) }), onStart({ battle, unit }) { - // the anchor flies straight ahead along his direction - let stop = null; - for (let d = 1; d <= reach; d++) { - const [r, c] = frontOf(unit.tileR, unit.tileC, unit.dir, d); - // the anchor stops at the field edge and in front of a ground obstacle (crates, roadblocks) - if (!battle.grid.inRect(r, c) || battle.grid.isObstacle(r, c)) break; - stop = d; - if (battle.enemiesInKeys([r * COLS + c], unit, { canHitFly: true }).length) break; + // PRTS 备注 ② — the anchor's target: his own tile while he blocks an enemy (e.g. just after a 突袭 landing), + // else the nearest tile of the skill range (6-1: his own tile and the 6 straight ahead along his direction) with + // an enemy on it, else its farthest tile + let stop = 0; + if (!unit.blocking.some((e) => e.alive && e.blockedBy === unit)) { + for (let d = 0; d <= reach; d++) { + const [r, c] = frontOf(unit.tileR, unit.tileC, unit.dir, d); + // the anchor stops at the field edge and in front of a ground obstacle (crates, roadblocks) + if (!battle.grid.inRect(r, c) || battle.grid.isObstacle(r, c)) break; + stop = d; + if (battle.enemiesInKeys([r * COLS + c], unit, { canHitFly: true }).length) break; + } } - const [sr, sc] = frontOf(unit.tileR, unit.tileC, unit.dir, stop ?? 0); + const [sr, sc] = frontOf(unit.tileR, unit.tileC, unit.dir, stop); const fromX = unit.x, fromY = unit.y; battle.fx('anchor', { x: sc, y: sr, id: unit.id, fromX, fromY, r: radius }); for (const e of battle.foesInRadius(sc, sr, radius)) { battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'phys', isSkill: true, tags: ['skill', 'anchor'] }); if (e.alive) battle.applyStatus(e, 'stun', { duration: num(bb.stun), source: unit }); } - if (stop == null || !unit.alive) return; + // ③ 【移动】 — only a change of tile moves him and leaves the 从不混淆的方向 ("若目标地块不为当前地块,会在原地部署"): an + // anchor on his own tile leaves him where he stands, no marker, nothing to return from [ASSUMED: the "tile one + // beyond the landing" is not tried when the landing is his own tile] + if (stop === 0 || !unit.alive) return; // PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile) const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c); const dest = [[sr, sc], frontOf(unit.tileR, unit.tileC, unit.dir, stop + 1)].find(ok); diff --git a/test/content/kits_t5.test.js b/test/content/kits_t5.test.js index fa813781..a40823b2 100644 --- a/test/content/kits_t5.test.js +++ b/test/content/kits_t5.test.js @@ -234,6 +234,38 @@ test('乌尔比安 S3: anchor 135 % ATK + stun on the first enemy ahead, moves t clean(h); }); +test('乌尔比安 S3 while he blocks (e.g. after a 突袭 landing): the anchor lands on his own tile — no 【移动】, no marker (PRTS 备注; GitHub #33)', () => { + // PRTS 乌尔比安 S3 备注: target = his own tile (only while blocking) > the nearest tile of the skill range (6-1, from his own + // tile on) with an enemy > its farthest tile; the 从不混淆的方向 only when the 【移动】 changes his tile + const run = (near) => { + const h = makeBattle({ + defs: { enemies: { enemy_dummy: dummy(), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) } }, + units: [{ chessId: 'chess_char_5_05_a', row: 9, col: 3 }], + enemies: [{ key: near, pos: [9, 3] }, { key: 'enemy_dummy', pos: [9, 7] }], hooks: ['damaged'], captureNoisy: true, autoFinish: false, timeLimit: 120, + }); + const u = h.unit('chess_char_5_05_a'); + h.step(); + const [e0, far] = h.b.enemies; + const blocked = e0.blockedBy === u; + u.skill.gainSp(1000); + assert.ok(h.runUntil(() => u.skill.active, 2), near); + const anchor = fxOf(h, 'anchor')[0]; + assert.deepEqual([anchor[2], anchor[3]], [3, 9], `${near}: the anchor on his own tile`); + assert.equal(tagged(h, 'anchor', e0).length, 1, `${near}: the enemy on his tile is hit`); + assert.ok(e0.s.flags.stun); + assert.equal(tagged(h, 'anchor', far).length, 0, `${near}: 4 tiles ahead, out of the 1.8 radius`); + assert.deepEqual([u.tileR, u.tileC], [9, 3], `${near}: no 【移动】`); + assert.ok(!h.b.allyUnits.some((x) => x.kind === 'token'), `${near}: no 从不混淆的方向`); + assert.ok(h.runUntil(() => !u.skill.active, 30)); + assert.deepEqual([u.tileR, u.tileC], [9, 3]); + clean(h); + return blocked; + }; + assert.equal(run('enemy_dummy'), true, 'he blocks the enemy on his tile'); + // not blocking, a flyer over him: his own tile is the nearest tile of the skill range with an enemy (distance 0) + assert.equal(run('enemy_fly'), false, 'a flyer is not blocked'); +}); + test('乌尔比安 T1 本性的坚守 heals per hit taken; T2 血脉的哺养 +HP/ATK per kill (abyssal allies half); elite healing ×1.2', () => { const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() }, chess: { t_abyss: ally('t_abyss', { charId: 'char_143_ghost' }) } }, From c2c1cf7fd8627427af28f81d375be3e2e8175f6f Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 22:58:11 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feedback2=20WC:=20DESIGN=20=C2=A722.3=20(Gi?= =?UTF-8?q?tHub=20#33=20items=201-3:=20the=20devour=20after=20a=20revive,?= =?UTF-8?q?=20=E4=B9=8C=E5=B0=94=E6=AF=94=E5=AE=89=20S3=20while=20blocking?= =?UTF-8?q?,=20the=20=E8=81=94=E9=98=B2=20order=20still=20unknown)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DESIGN.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 41c932cb..1ccc94d6 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1472,9 +1472,17 @@ TBD-22.1 TBD-22.2 -### 22.3 阿戈尔's devour skips members it has already knocked out (#33 item 1) +### 22.3 阿戈尔's devour skips members it has already knocked out (#33 item 1) — `content/bonds/core.js devour`, `content/items/battle.js deploymentOf` (exported through `content/items.js`), `kits/tier5.js` (乌尔比安 S3, #33 item 2) -TBD-22.3 +- **Seen** (GitHub #33 item 1, "阿戈尔阵营复活机制看起来不会触发"): the 5-tier revive works on its own (5 distinct 阿戈尔, one lethal hit each: the first 3 come back on their tile at full HP, normal and 联防 battles alike), but the usual food chain used it up at t = 0. One row, all facing right, 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现: the battle-start devour knocked out 幽灵鲨, 海霓 and 深巡, the revive brought each straight back (all 3 charges), and 乌尔比安's marks still pending on them knocked them out a second time — down until their 70 / 80 s redeploy, no revive left for the fight (联防 battles alike). +- **Official** (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 备注): "标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; the 5-tier "前3名【阿戈尔】干员首次被击倒时立刻复活" (bonds.json `max_free_respawn_cnt` 3), implemented as "受益者因移动之外的原因退场时下次部署的再部署时间和费用归零". So a devour knock-out is a first knock-out and spends a revive, and the revived member takes no further mark. +- **Cause**: `devour()` skipped a mark only while its target or marker was dead (`!t.alive || !m.alive`); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge). +- **Now**: when the marks are placed, the pass records each marker's and target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target or its marker is off the field or in another deployment, i.e. was knocked out during the pass, whether the 5-tier revive redeployed it or 埃芒加德 / M3茧甲 revived it in place. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each and revive, every later mark is cancelled; all five 阿戈尔 stand and only 隐现 falls. The 3 revives are still spent at t = 0, as officially. Without a revive in the pass (fewer than 5 members, no 埃芒加德 / M3茧甲) the devour resolves exactly as before. +- **联防 (#33 item 3, not changed)**: the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members nothing revives them, as before). The order of the 联防 forced exit and the battle-start devour stays [ASSUMED] (§19.3: forced exit first); no source gives it. +- **乌尔比安 S3 (#33 item 2) — seen**: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". **Official** (PRTS 乌尔比安 S3 备注; the chess skill is `skchr_ulpia_3`, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". **Cause**: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast missed it) and left the marker. **Now**: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. Without a blocked enemy the search starts on his own tile (distance 0: a flyer over him counts), then straight ahead as before. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷): with 突袭, 56 of 84 casts used to move him off his landing tile, now none does (every cast there came while he blocked); without 突袭, 64 of 80 casts still move him, as before. +- **乌尔比安 S3 — not reproduced** (the triage, real data): no 阿戈尔 gain is cleared on the return. The devour's base ATK / block (`bond:egir:devour`, e.g. +399 ATK, +1 block), the 阿戈尔 HP and the 血脉的哺养 stacks are there before, during and after the S3 (`Battle.relocate` keeps every buff); what ends is the S3's own +40–60 % HP / +200–230 % ATK. With 突袭 he does return and the marker is drawn, both on the landing tile next to the enemies, not on his board tile (8 stages × 5 round templates × 2 seeds); with this fix a cast while he blocks there leaves no marker at all, as officially. **Deviation, kept**: officially the 【移动】 keeps only the skill progress and the second talent's stacks, so the official game drops the devour gains on the move; the remake keeps them through the 【移动】 and the return. +- [ASSUMED]: (1) a marker knocked out during the pass gives no further mark, also when revived — the remake's marker rule since 0.1.0, extended so a revived marker acts like one that stays down; PRTS names only the target, and its "【吞噬】的物理流失来源为被付与目标自身" leaves open that the official marks do not depend on the marker at all. (2) A marker revived during the pass keeps the base ATK / block it gained at marking (a persistent buff, as through every knock-out since 0.1.0). (3) With the anchor on his own tile, 乌尔比安's 【移动】 does not try "the tile one beyond the landing" (he would otherwise step one tile forward). +- Tests: `test/content/bonds_core.test.js` (the chain in a normal and a 联防 battle: one knock-out and one revive each, all five standing, the next knock-out final; a marker revived by the 5-tier revive gives no mark; a target 埃芒加德 revived in place takes none — one revive spent, not two), `test/content/kits_t5.test.js` (乌尔比安 S3 while he blocks, and with a flyer over him: the anchor on his own tile, no move, no marker). ### 22.4 活性源石 stays on an enemy after it leaves the tile (#33 item 6) From 93b0592bff3e17666b093e3fe1662ab959d6d386 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 23:44:36 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feedback2=20WC:=20the=20devour=20keeps=200.?= =?UTF-8?q?1.0's=20marker=20rule=20(a=20marker=20back=20in=20the=20same=20?= =?UTF-8?q?pass=20still=20gives=20its=20marks)=20and=20cancels=20a=20targe?= =?UTF-8?q?t's=20marks=20after=20a=20=E4=B8=8D=E5=B1=88=20redeploy=20too?= =?UTF-8?q?=20(review=20of=20GitHub=20#33=20item=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/research/02-bonds.md | 2 +- server/sim/content/bonds/core.js | 19 ++++++++++--------- test/content/bonds_core.test.js | 31 +++++++++++++++++++++++-------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/research/02-bonds.md b/docs/research/02-bonds.md index e057f7b0..aa2bfefd 100644 --- a/docs/research/02-bonds.md +++ b/docs/research/02-bonds.md @@ -378,7 +378,7 @@ Members (11 chess, 10 in current shop pool; by tier in shop: {'1': 1, '2': 1, '3 1. Order: 阿戈尔 members sorted leftmost first, then topmost ("更靠左和靠上"). 2. Each 阿戈尔 in order marks the unit on the tile directly in front of it (its facing direction) and also the front-tile unit of every 阿戈尔 it has marked (chain). It never marks itself, a unit it already marked, or a unit that marked it. 3. The marker immediately gains the base ATK of every unit it marked (added to base ATK at the final stage) and their block counts. - 4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target knocked out for the first time has its pending marks cancelled ("目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果") — also when it is revived at once (the 5-tier revive, 埃芒加德, M3茧甲), so a revived member is not devoured again. [ASSUMED] a marker knocked out during the pass gives no further mark either, revived or not (PRTS names only the target). DESIGN §22.3. + 4. After all marks are placed, each mark makes its target suffer one 5000-point physical 流失 (HP loss, source = the target itself; if it dies, the kill is credited to the marker), resolved in marking order. A target knocked out for the first time has its pending marks cancelled ("目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果") — also when it is back at once (the 5-tier revive, 不屈's 立刻重新部署, 埃芒加德, M3茧甲), so a revived member is not devoured again. DESIGN §22.3. 5. Each devoured unit adds layers to 阿戈尔 equal to its tier (1-6), once per unit per battle. 6. (5-member tier) Revive is implemented as: when the unit leaves the field for any reason other than being moved, its next deployment has 0 redeploy time and 0 cost (PRTS). Devour layers: each devoured unit adds its tier once per battle (refreshes next round). - Formulas: `hpMultiplier = 1.35 + 0.01*L`; `devourDamage = 5000`; `layersPerDevoured = tier of devoured unit` diff --git a/server/sim/content/bonds/core.js b/server/sim/content/bonds/core.js index d7ea2492..b2b284ee 100644 --- a/server/sim/content/bonds/core.js +++ b/server/sim/content/bonds/core.js @@ -339,9 +339,10 @@ function installLaterano(battle, pid, bb, members) { * 物理流失 (PRTS 盟约记录: "造成5000点物理流失", 修正 "【吞噬】的物理流失来源为被付与目标自身;单位被【吞噬】击杀时,击杀来源始终为 * 对应标记的付与来源"; PRTS 作战机制: a 物理流失 "会受到目标当前防御力…影响而相应衰减") — less the target's DEF as a physical hit * (its own source: no DEF ignore), then battle.loseHp: no shields, dodge or damage multipliers (DEF-free until 0.1.1); the - * kill is credited to the marker — in marking order. A unit knocked out during the pass cancels its remaining marks, also - * when it is revived at once (the 5-tier 立刻复活, 埃芒加德 / M3茧甲): as target (PRTS 盟约记录 "目标首次被击倒后解除自身被付与 - * 但还未触发的【吞噬】效果") and as marker (the remake's rule since 0.1.0, [ASSUMED]: PRTS names only the target). + * kill is credited to the marker — in marking order. A target knocked out during the pass has its remaining marks + * cancelled, also when it is back at once (the 5-tier 立刻复活, 不屈's 立刻重新部署, 埃芒加德 / M3茧甲): PRTS 盟约记录 "目标首次被 + * 击倒后解除自身被付与但还未触发的【吞噬】效果". A marker off the field gives no further mark (the rule since 0.1.0; one knocked + * out and back in the same pass still gives its marks). * Each devoured operator adds its tier to 阿戈尔 once (IN_BATTLE gain, disabled in 联防 / boss fields). * Tokens / devices / empty tiles are never devoured. */ @@ -384,14 +385,14 @@ function devour(battle, pid, bb, members) { } const amount = num(bb.damage_value, 0); const layered = new Set(); - // knocked out during the pass = off the field, or in another deployment than when the marks were placed (items - // deploymentOf: the 5-tier revive redeploys, 埃芒加德 / M3茧甲 revive in place) — a revived member was standing again - // when its pending marks used to knock it out a second time and spend every revive at t = 0 (GitHub #33) + // a target knocked out during the pass = off the field, or in another deployment than when the marks were placed (items + // deploymentOf: the 5-tier revive and 不屈 redeploy it, 埃芒加德 / M3茧甲 revive it in place) — a revived member was + // standing again when its pending marks used to knock it out a second time and spend every revive at t = 0 (GitHub #33) const dep = new Map(); - for (const pair of marks) for (const u of pair) if (!dep.has(u)) dep.set(u, items.deploymentOf(u)); - const knocked = (u) => !u.alive || items.deploymentOf(u) !== dep.get(u); + for (const [, t] of marks) if (!dep.has(t)) dep.set(t, items.deploymentOf(t)); + const knocked = (t) => !t.alive || items.deploymentOf(t) !== dep.get(t); for (const [m, t] of marks) { - if (knocked(t) || knocked(m)) continue; + if (knocked(t) || !m.alive) continue; S.fxOn(battle, 'devour', t, 'bond:egirShip', 'devour', { from: m.id }); if (amount > 0) battle.loseHp(t, mitigate(amount, 'phys', t.s), { source: m, tags: ['bond:egir:devour'] }); if (!layered.has(t)) { diff --git a/test/content/bonds_core.test.js b/test/content/bonds_core.test.js index a5a7c1af..9cd718cd 100644 --- a/test/content/bonds_core.test.js +++ b/test/content/bonds_core.test.js @@ -448,7 +448,7 @@ test('阿戈尔 5 + the devour chain (GitHub #33): a member the devour knocks ou } }); -test('阿戈尔 devour: a unit revived during the pass is out of it — a marker back from the 5-tier revive gives no mark, a target 埃芒加德 revived in place takes none', () => { +test('阿戈尔 devour, a unit back during the pass: a marker revived by the 5-tier revive still gives its marks; a target revived in place (埃芒加德) or redeployed (不屈) takes none', () => { // g1 (10,3) → g2 (10,4, 3000 × 1.35 HP: knocked out by g1's mark, revived) → g3 (10,5) → fodder (10,6); g4 / g5 face empty tiles const list = [ ['g1_a', ['egirShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip'], { stats: { maxHp: 20000 } }], @@ -460,19 +460,22 @@ test('阿戈尔 devour: a unit revived during the pass is out of it — a marker ]; const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(5, 0, null, [3, 5]) }, hooks: ['damaged', 'deploy'], captureNoisy: true }); h.step(1); - const [g2, g3, f] = ['g2_a', 'g3_a', 'fod_a'].map((id) => h.unit(id)); + const [g2, f] = ['g2_a', 'fod_a'].map((id) => h.unit(id)); assert.ok(g2.alive && h.hooksOf('deploy').some((c) => c.unit === g2 && !c.initial), 'g2 knocked out and revived'); - // [ASSUMED] (PRTS names only the target): g2's own marks on g3 and the fodder are cancelled with its knock-out + // only a marker off the field gives no further mark (the rule since 0.1.0): g2, back at once, still devours g3 and the fodder assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), - [['g1_a', 'g2_a'], ['g1_a', 'g3_a'], ['g1_a', 'fod_a'], ['g3_a', 'fod_a']]); - close(f.hp, 20000 - 2 * 5000, 1e-6, 'the fodder: g1 and g3 only'); + [['g1_a', 'g2_a'], ['g1_a', 'g3_a'], ['g1_a', 'fod_a'], ['g2_a', 'g3_a'], ['g2_a', 'fod_a'], ['g3_a', 'fod_a']]); + close(f.hp, 20000 - 3 * 5000, 1e-6, 'the fodder: g1, g2 and g3'); close(g2.s.atk, 1000 + 1000 + 1000, 1e-6, 'g2 keeps the base ATK it gained at marking (a persistent buff) [ASSUMED]'); assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 1 + 2, 'each devoured unit adds its tier once'); checkInvariants(h.b); - // 3 members + 埃芒加德 (命结之秘, the first 3 knocked-out operators revive in place — a new deployment, items revivedInPlace): - // g1 → g2 → fodder (3000 HP): g1's mark knocks the fodder out, 埃芒加德 revives it, g2's mark on it is cancelled - const list2 = [['g1_a', ['egirShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 20000 } }], ['g3_a', ['egirShip']], ['fod_a', ['preciShip'], { tier: 2, stats: { maxHp: 3000 } }]]; + // 3 members, g1 → g2 → fodder (3000 HP): g1's mark knocks the fodder out, it is back at once, g2's mark on it is cancelled + const list2 = [ + ['g1_a', ['egirShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 20000 } }], ['g3_a', ['egirShip']], + ['fod_a', ['indomShip'], { tier: 2, stats: { maxHp: 3000 } }], + ]; const units2 = [{ chessId: 'g1_a', row: 10, col: 3 }, { chessId: 'g2_a', row: 10, col: 4 }, { chessId: 'fod_a', row: 10, col: 5 }, { chessId: 'g3_a', row: 12, col: 3 }]; + // 埃芒加德 (命结之秘: the first 3 knocked-out operators revive in place — a new deployment, items revivedInPlace) const e = makeBattle({ defs: defsOf(list2), units: units2, bandId: 'band_ermengard', bonds: { egirShip: bondOn(3, 0, null, [3, 5]) }, hooks: ['damaged'], captureNoisy: true }); e.step(1); const fe = e.unit('fod_a'); @@ -481,6 +484,18 @@ test('阿戈尔 devour: a unit revived during the pass is out of it — a marker close(fe.hp, fe.s.maxHp, 1e-6, 'at full HP: no second mark'); assert.deepEqual(e.eventsOf('fx').filter((x) => x[1] === 'revive').map((x) => x[4].left), [2], 'one 埃芒加德 revive spent, not two'); checkInvariants(e.b); + // 不屈 at p = min(1, 0.18 + 0.004 × 300) = 1: 立刻重新部署 where it lies — a knock-out and a new deployment. Until 0.1.1 + // g2's mark knocked the fodder out again within the same instant, where 不屈 does not fire twice, and it stayed down + const u = makeBattle({ + defs: defsOf(list2), units: units2, hooks: ['damaged', 'death', 'deploy'], captureNoisy: true, + bonds: { egirShip: bondOn(3, 0, null, [3, 5]), indomShip: bondOn(2, 300, 1, [2, 3]) }, + }); + u.step(1); + const fu = u.unit('fod_a'); + assert.deepEqual(tagged(u, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a']]); + assert.ok(fu.alive && u.hooksOf('deploy').some((c) => c.unit === fu && !c.initial), 'redeployed by 不屈'); + assert.equal(u.hooksOf('death').filter((c) => c.unit === fu && c.reason === 'killed').length, 1, 'knocked out once'); + checkInvariants(u.b); }); // --------------------------------------------------------------------------------------------------------------------- From ee70c608dcc5cdf5e2ed7255dfa2d4a49f50a9d5 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 23:44:36 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feedback2=20WC:=20=E4=B9=8C=E5=B0=94?= =?UTF-8?q?=E6=AF=94=E5=AE=89=20S3=20takes=20his=20own=20tile=20only=20whi?= =?UTF-8?q?le=20he=20blocks;=20otherwise=20the=20anchor=20search=20starts?= =?UTF-8?q?=20one=20tile=20ahead=20as=20in=200.1.1=20(review=20of=20GitHub?= =?UTF-8?q?=20#33=20item=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/sim/content/kits/tier5.js | 7 +++--- test/content/kits_t5.test.js | 42 +++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/server/sim/content/kits/tier5.js b/server/sim/content/kits/tier5.js index 1fbf763a..97d2822a 100644 --- a/server/sim/content/kits/tier5.js +++ b/server/sim/content/kits/tier5.js @@ -817,11 +817,12 @@ const KITS = { mods: mods({ hpPct: num(bb.max_hp), atkPct: num(bb.atk) }), onStart({ battle, unit }) { // PRTS 备注 ② — the anchor's target: his own tile while he blocks an enemy (e.g. just after a 突袭 landing), - // else the nearest tile of the skill range (6-1: his own tile and the 6 straight ahead along his direction) with - // an enemy on it, else its farthest tile + // else the nearest tile ahead in the skill range (straight along his direction) with an enemy on it, else the + // farthest one. His own tile is a candidate only while he blocks ("自身所在地块(仅阻挡敌人时)"); not taken: the + // reading of the range's own tile (6-1 starts at [0,0]) as distance 0 for the second rule (a flyer over him) let stop = 0; if (!unit.blocking.some((e) => e.alive && e.blockedBy === unit)) { - for (let d = 0; d <= reach; d++) { + for (let d = 1; d <= reach; d++) { const [r, c] = frontOf(unit.tileR, unit.tileC, unit.dir, d); // the anchor stops at the field edge and in front of a ground obstacle (crates, roadblocks) if (!battle.grid.inRect(r, c) || battle.grid.isObstacle(r, c)) break; diff --git a/test/content/kits_t5.test.js b/test/content/kits_t5.test.js index a40823b2..dbace6bf 100644 --- a/test/content/kits_t5.test.js +++ b/test/content/kits_t5.test.js @@ -235,9 +235,9 @@ test('乌尔比安 S3: anchor 135 % ATK + stun on the first enemy ahead, moves t }); test('乌尔比安 S3 while he blocks (e.g. after a 突袭 landing): the anchor lands on his own tile — no 【移动】, no marker (PRTS 备注; GitHub #33)', () => { - // PRTS 乌尔比安 S3 备注: target = his own tile (only while blocking) > the nearest tile of the skill range (6-1, from his own - // tile on) with an enemy > its farthest tile; the 从不混淆的方向 only when the 【移动】 changes his tile - const run = (near) => { + // PRTS 乌尔比安 S3 备注: target = his own tile (only while blocking) > the nearest tile of the skill range with an enemy > + // its farthest tile; the 从不混淆的方向 only when the 【移动】 changes his tile + const setup = (near) => { const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy(), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }) } }, units: [{ chessId: 'chess_char_5_05_a', row: 9, col: 3 }], @@ -249,21 +249,35 @@ test('乌尔比安 S3 while he blocks (e.g. after a 突袭 landing): the anchor const blocked = e0.blockedBy === u; u.skill.gainSp(1000); assert.ok(h.runUntil(() => u.skill.active, 2), near); - const anchor = fxOf(h, 'anchor')[0]; - assert.deepEqual([anchor[2], anchor[3]], [3, 9], `${near}: the anchor on his own tile`); - assert.equal(tagged(h, 'anchor', e0).length, 1, `${near}: the enemy on his tile is hit`); + return { h, u, e0, far, blocked, anchor: fxOf(h, 'anchor')[0] }; + }; + { // blocking the enemy on his tile: the anchor on his own tile, he stays + const { h, u, e0, far, blocked, anchor } = setup('enemy_dummy'); + assert.ok(blocked, 'he blocks the enemy on his tile'); + assert.deepEqual([anchor[2], anchor[3]], [3, 9], 'the anchor on his own tile'); + assert.equal(tagged(h, 'anchor', e0).length, 1, 'the blocked enemy is hit'); assert.ok(e0.s.flags.stun); - assert.equal(tagged(h, 'anchor', far).length, 0, `${near}: 4 tiles ahead, out of the 1.8 radius`); - assert.deepEqual([u.tileR, u.tileC], [9, 3], `${near}: no 【移动】`); - assert.ok(!h.b.allyUnits.some((x) => x.kind === 'token'), `${near}: no 从不混淆的方向`); + assert.equal(tagged(h, 'anchor', far).length, 0, '4 tiles ahead, out of the 1.8 radius'); + assert.deepEqual([u.tileR, u.tileC], [9, 3], 'no 【移动】'); + assert.ok(!h.b.allyUnits.some((x) => x.kind === 'token'), 'no 从不混淆的方向'); assert.ok(h.runUntil(() => !u.skill.active, 30)); assert.deepEqual([u.tileR, u.tileC], [9, 3]); clean(h); - return blocked; - }; - assert.equal(run('enemy_dummy'), true, 'he blocks the enemy on his tile'); - // not blocking, a flyer over him: his own tile is the nearest tile of the skill range with an enemy (distance 0) - assert.equal(run('enemy_fly'), false, 'a flyer is not blocked'); + } + { // not blocking, a flyer over his tile: his own tile is no candidate (only while blocking) — the anchor flies to the + // enemy 4 tiles ahead and he moves there, as in 0.1.1 + const { h, u, e0, far, blocked, anchor } = setup('enemy_fly'); + assert.ok(!blocked, 'a flyer is not blocked'); + assert.deepEqual([anchor[2], anchor[3]], [7, 9], 'the anchor on the enemy ahead'); + assert.equal(tagged(h, 'anchor', far).length, 1); + assert.equal(tagged(h, 'anchor', e0).length, 0, 'the flyer over his tile is out of the 1.8 radius'); + assert.deepEqual([u.tileR, u.tileC], [9, 7], 'moved onto the anchor tile'); + const marker = h.b.allyUnits.find((x) => x.kind === 'token' && x.alive); + assert.ok(marker && marker.tileR === 9 && marker.tileC === 3, 'the marker on his tile'); + assert.ok(h.runUntil(() => !u.skill.active, 30)); + assert.deepEqual([u.tileR, u.tileC], [9, 3], 'back home'); + clean(h); + } }); test('乌尔比安 T1 本性的坚守 heals per hit taken; T2 血脉的哺养 +HP/ATK per kill (abyssal allies half); elite healing ×1.2', () => { From d5ebc6c7cdcf5746f90fbd2e7af7ae13cb819cb4 Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 23:44:36 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feedback2=20WC:=20DESIGN=20=C2=A722.3=20fol?= =?UTF-8?q?lows=20the=20review=20(0.1.0's=20marker=20rule,=20=E4=B8=8D?= =?UTF-8?q?=E5=B1=88,=20his=20own=20tile=20only=20while=20blocking,=2082?= =?UTF-8?q?=20of=2084=20casts,=20measured=20before=20#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DESIGN.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1ccc94d6..0971a090 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1476,13 +1476,13 @@ TBD-22.2 - **Seen** (GitHub #33 item 1, "阿戈尔阵营复活机制看起来不会触发"): the 5-tier revive works on its own (5 distinct 阿戈尔, one lethal hit each: the first 3 come back on their tile at full HP, normal and 联防 battles alike), but the usual food chain used it up at t = 0. One row, all facing right, 浊心斯卡蒂 → 乌尔比安 → 幽灵鲨 → 海霓 → 深巡 → 隐现: the battle-start devour knocked out 幽灵鲨, 海霓 and 深巡, the revive brought each straight back (all 3 charges), and 乌尔比安's marks still pending on them knocked them out a second time — down until their 70 / 80 s redeploy, no revive left for the fight (联防 battles alike). - **Official** (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 阿戈尔 备注): "标记按付与顺序触发【吞噬】效果,目标首次被击倒后解除自身被付与但还未触发的【吞噬】效果"; the 5-tier "前3名【阿戈尔】干员首次被击倒时立刻复活" (bonds.json `max_free_respawn_cnt` 3), implemented as "受益者因移动之外的原因退场时下次部署的再部署时间和费用归零". So a devour knock-out is a first knock-out and spends a revive, and the revived member takes no further mark. -- **Cause**: `devour()` skipped a mark only while its target or marker was dead (`!t.alive || !m.alive`); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge). -- **Now**: when the marks are placed, the pass records each marker's and target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target or its marker is off the field or in another deployment, i.e. was knocked out during the pass, whether the 5-tier revive redeployed it or 埃芒加德 / M3茧甲 revived it in place. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each and revive, every later mark is cancelled; all five 阿戈尔 stand and only 隐现 falls. The 3 revives are still spent at t = 0, as officially. Without a revive in the pass (fewer than 5 members, no 埃芒加德 / M3茧甲) the devour resolves exactly as before. -- **联防 (#33 item 3, not changed)**: the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members nothing revives them, as before). The order of the 联防 forced exit and the battle-start devour stays [ASSUMED] (§19.3: forced exit first); no source gives it. -- **乌尔比安 S3 (#33 item 2) — seen**: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". **Official** (PRTS 乌尔比安 S3 备注; the chess skill is `skchr_ulpia_3`, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". **Cause**: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast missed it) and left the marker. **Now**: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. Without a blocked enemy the search starts on his own tile (distance 0: a flyer over him counts), then straight ahead as before. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷): with 突袭, 56 of 84 casts used to move him off his landing tile, now none does (every cast there came while he blocked); without 突袭, 64 of 80 casts still move him, as before. +- **Cause**: `devour()` skipped a mark only while its target or marker was dead (`!t.alive || !m.alive`); a member revived in the same pass was alive again, so its pending marks fired. 埃芒加德's revives went the same way (it revives in place: the next pending mark knocked the revived unit out again and spent a second charge), and so did 不屈's 立刻重新部署 (the redeployed unit was knocked out again in the same instant, where 不屈 does not fire twice, and stayed down). +- **Now**: when the marks are placed, the pass records each target's deployment (`items/battle.js deploymentOf`, the deploy counter plus the in-place revives — exported for this). A mark is skipped once its target is off the field or in another deployment, i.e. was knocked out during the pass — whether the 5-tier revive or 不屈's 立刻重新部署 redeployed it or 埃芒加德 / M3茧甲 revived it in place. The marker side is unchanged since 0.1.0 (PRTS describes only the target): a marker off the field gives no further mark, one knocked out and back in the same pass still gives its marks. The chain above: 浊心斯卡蒂's five marks resolve, the three members fall once each and revive, every later mark on them is cancelled; all five 阿戈尔 stand and only 隐现 falls. The 3 revives are still spent at t = 0, as officially. When nothing brings a devoured unit back during the pass (no 5-tier revive, 不屈 redeploy, 埃芒加德 or M3茧甲), the devour resolves exactly as before. +- **联防 (#33 item 3, not changed)**: the members knocked out twice at t = 0 were still down at the end of the 45–60 s own combat (70 s redeploy), so they entered every 联防 knocked out (forced out, §19.3) and the chain there formed without them. After this fix that is far rarer: with the 5-tier, the members the devour knocks out (up to 3) stand again and enter 联防 down only when the fight knocks them out once more (with 3–4 members no 阿戈尔 revive brings them back, as before). The order of the 联防 forced exit and the battle-start devour stays [ASSUMED] (§19.3: forced exit first); no source gives it. +- **乌尔比安 S3 (#33 item 2) — seen**: "乌尔比安3技能结束返回会清空阿戈尔阵营增益,转职突袭后疑似不会返回(未转职时三技能会在原地留下一个图标,转职后无图标)". **Official** (PRTS 乌尔比安 S3 备注; the chess skill is `skchr_ulpia_3`, skill range 6-1 = his own tile and the 6 ahead): the anchor's target is "自身所在地块(仅阻挡敌人时)>技能范围内距离自身最近的包含敌人的地块>技能范围内距离最远的地块"; the 【移动】 goes to the landing tile > the tile beyond it > his own tile, and "若目标地块不为当前地块,会在原地部署一个从不混淆的方向"; "【移动】后仅继承下列效果:技能进度、第二天赋叠加层数". **Cause**: the anchor search started one tile ahead, so while he blocked — typical right after a 突袭 landing next to the enemies — he threw ahead, walked off the enemy he blocked (the blast could miss it — with an enemy one tile ahead its 1.8 radius still reaches the one he blocks) and left the marker. **Now**: while he blocks an enemy the anchor lands on his own tile: the blast (radius 1.8) hits what he blocks, he stays put, no marker, nothing to return from. His own tile is a candidate only while he blocks ("仅阻挡敌人时"): without a blocked enemy the search starts one tile ahead, exactly as in 0.1.1, so a flyer over his tile does not keep him there. Considered and not taken: reading the own tile of the 6-1 range as distance 0 for the second rule. Measured with the triage's batch (8 real stages × 5 round templates × 2 seeds, 突袭 through 变形同构体 + 突袭手雷), before the 突袭 landing change (#51, §22.2) is merged: with 突袭, 56 of 84 casts used to move him off his landing tile, now none does — 82 of the 84 came while he blocked, the other 2 (act2 m03, tile (10,8)) have no standable tile ahead (pre-existing); without 突袭, 64 of 80 casts still move him, as before. - **乌尔比安 S3 — not reproduced** (the triage, real data): no 阿戈尔 gain is cleared on the return. The devour's base ATK / block (`bond:egir:devour`, e.g. +399 ATK, +1 block), the 阿戈尔 HP and the 血脉的哺养 stacks are there before, during and after the S3 (`Battle.relocate` keeps every buff); what ends is the S3's own +40–60 % HP / +200–230 % ATK. With 突袭 he does return and the marker is drawn, both on the landing tile next to the enemies, not on his board tile (8 stages × 5 round templates × 2 seeds); with this fix a cast while he blocks there leaves no marker at all, as officially. **Deviation, kept**: officially the 【移动】 keeps only the skill progress and the second talent's stacks, so the official game drops the devour gains on the move; the remake keeps them through the 【移动】 and the return. -- [ASSUMED]: (1) a marker knocked out during the pass gives no further mark, also when revived — the remake's marker rule since 0.1.0, extended so a revived marker acts like one that stays down; PRTS names only the target, and its "【吞噬】的物理流失来源为被付与目标自身" leaves open that the official marks do not depend on the marker at all. (2) A marker revived during the pass keeps the base ATK / block it gained at marking (a persistent buff, as through every knock-out since 0.1.0). (3) With the anchor on his own tile, 乌尔比安's 【移动】 does not try "the tile one beyond the landing" (he would otherwise step one tile forward). -- Tests: `test/content/bonds_core.test.js` (the chain in a normal and a 联防 battle: one knock-out and one revive each, all five standing, the next knock-out final; a marker revived by the 5-tier revive gives no mark; a target 埃芒加德 revived in place takes none — one revive spent, not two), `test/content/kits_t5.test.js` (乌尔比安 S3 while he blocks, and with a flyer over him: the anchor on his own tile, no move, no marker). +- [ASSUMED]: (1) a marker revived during the pass keeps the base ATK / block it gained at marking (a persistent buff, as through every knock-out since 0.1.0). (2) With the anchor on his own tile, 乌尔比安's 【移动】 does not try "the tile one beyond the landing" (he would otherwise step one tile forward). +- Tests: `test/content/bonds_core.test.js` (the chain in a normal and a 联防 battle: one knock-out and one revive each, all five standing, the next knock-out final; a marker revived by the 5-tier revive still gives its marks; a target 埃芒加德 revived in place takes none — one revive spent, not two; a target 不屈 redeployed takes none and stays standing), `test/content/kits_t5.test.js` (乌尔比安 S3 while he blocks: the anchor on his own tile, no move, no marker; a flyer over his tile, not blocked: the anchor ahead and the move, as in 0.1.1). ### 22.4 活性源石 stays on an enemy after it leaves the tile (#33 item 6)