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); }); // ---------------------------------------------------------------------------------------------------------------------