diff --git a/docs/SIM.md b/docs/SIM.md index cb23f25d..608b0771 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -230,7 +230,9 @@ a crowd of dying 萨卡兹枯朽战士 totals 50 / s, not 50 × n. **Knock-outs that are not deaths** (`content/enemies.js`; player reports after 0.1.0): a `killed` ability that keeps the enemy alive hides the knock-out from every later `kill` handler, the kill count, kill credit and the bounty — they all wait for the real death, the only one with a `die` event. 重生 / form changes: `reborn()` (first knock-out → second -form), `husk()` (talent Revive[Trigger], every knock-out: 1 s 重生 — 无敌, 无法阻挡, immobile — then a hit-count husk that +form; fx forms 'reborn' → 'form2'), `statue()` (守墓石像: melee only while blocked; first knock-out → 10 s unblockable, +immobile statue → a flyer with ranged arts attacks that skip flyers; forms 'stone' → 'fly'), `husk()` (talent +Revive[Trigger], every knock-out: 1 s 重生 — 无敌, 无法阻挡, immobile — then a hit-count husk that walks its route on: the 深池逐火 余烬 / 火灰 are 隐匿 and disarmed, so only a blocked one can be targeted and beaten; 假想敌: 再生's 傀儡 is unblockable; a husk still standing after `Revive[Trigger].interval` s stands up again with full HP), 转译基底·α (its original form cancels every damage instance; the 4th physical / arts instance or a block starts a 2 s diff --git a/public/js/render/app.js b/public/js/render/app.js index 67847013..c71fd17d 100644 --- a/public/js/render/app.js +++ b/public/js/render/app.js @@ -90,7 +90,7 @@ import { GEO, ANIM, UF } from '../../../shared/constants.js'; import { Camera, presetCamera, lerpCamera, easeInOutCubic, pickTile, normRect } from './projection.js'; import { SnapshotBuffer, frameTime } from './interp.js'; import { TileField } from './tiles.js'; -import { UnitView, ItemView, DeviceView } from './units.js'; +import { UnitView, ItemView, DeviceView, FORMS } from './units.js'; import { FxSystem, ensureDamageFonts } from './fx.js'; import { createDragController, pieceTile } from './drag.js'; import { backdropTextures, shadowTexture, refreshTierChips, silhouetteTexture } from './textures.js'; @@ -1421,13 +1421,14 @@ export async function createFieldView(host, options = {}) { if (consumedIds.size > 200) consumedIds.delete(consumedIds.values().next().value); } // an enemy's mode change (掠海漂移体 → 爬行模式, user playtest #5 item 1; 转译基底's forms, a 逐火 ember and its - // revival — user report after 0.1.0): a 'phase' kind, or the `form` any other fx carries, switches the view's - // clip set (UnitView.setForm); the info keeps it for a view built later + // revival, the leaders' 重生 — user report after 0.1.0): a 'phase' kind, or the `form` any other fx carries, + // switches the view's clip set (UnitView.setForm, a kind without a clip set of that skeleton changes nothing); + // the info keeps it for a view built later const ex4 = e[4] && typeof e[4] === 'object' && e[4].id != null ? e[4] : null; const form = !ex4 ? undefined : e[1] === 'phase' ? ex4.kind : 'form' in ex4 ? ex4.form : undefined; if (form !== undefined) { const inf = infos.get(ex4.id); - if (inf) inf.form = typeof form === 'string' ? form : null; + if (inf && (typeof form !== 'string' || FORMS[inf.spine || inf.defId]?.[form])) inf.form = typeof form === 'string' ? form : null; views.get(ex4.id)?.setForm?.(form); } fx.simFx(e[1], Number(e[2]), Number(e[3]), e[4]); diff --git a/public/js/render/units.js b/public/js/render/units.js index d698a863..5e9c6dc1 100644 --- a/public/js/render/units.js +++ b/public/js/render/units.js @@ -108,8 +108,12 @@ export const EL_BAR = Object.freeze({ icon: 0.15, min: 8, max: 15, enemy: 0.8, g * clip: it never attacks) or 特战术师 D_* — its manifest roles are the original form's A_Idle / A_Move; * - 深池逐火战士 / 精锐战士 / 护卫 (#8): knocked out → 'Die' (the 1 s 重生), the 余烬 on Idle_2 / Move_2 and its death on * Die_2; standing up again → 'Revive', then the warrior's manifest clips; - * - 假想敌:再生: knocked out → A_Die, the 傀儡 on B_Idle / B_Move / B_Die; back → B_Revive, then the A_* manifest clips. - * 吉兆飞鳞's 晕眩模式 is its Stun clip already. + * - 假想敌:再生: knocked out → A_Die, the 傀儡 on B_Idle / B_Move / B_Die; back → B_Revive, then the A_* manifest clips; + * - the leaders' 重生 (sim reborn(): forms 'reborn' → 'form2'): 锏 Revive1, Revive2 held, Revive3, then B_*; 扎罗 A_revive_1 / + * _2 / _3, then B_* (its double-hit attack); “复仇者” Revive_Begin / _Loop / _End; 杰斯顿 C1_Die (its 4 s 重生), then + * C2_* — their manifests mix the forms (锏 / 扎罗 died on B_Die from the A model, the look of report #5); + * - 守墓石像 (forms 'stone' → 'fly'): the statue on Sleep [ASSUMED by name], then the flyer's *_2 clips. + * A kind without a clip set of this skeleton (barriers, charges, …) changes nothing. 吉兆飞鳞's 晕眩模式 is its Stun clip. */ const loop = (name, via = null) => Object.freeze(via ? { begin: null, loop: name, end: null, via } : { begin: null, loop: name, end: null }); const clipSet = (idle, move, die, attack = null) => Object.freeze({ @@ -121,6 +125,16 @@ const EMBER = Object.freeze({ husk: Object.freeze({ change: 'Die', roles: clipSet('Idle_2', 'Move_2', 'Die_2') }), revived: Object.freeze({ change: 'Revive', roles: Object.freeze({}) }), }); +/** A 重生 held on `hold` (also while the sim reports the rebirth's stun) after `begin`, then `end` and the second form. */ +const rebirth = (begin, hold, end, form2 = Object.freeze({})) => Object.freeze({ + reborn: Object.freeze({ change: begin, roles: Object.freeze({ idle: hold, deploy: hold, move: loop(hold), stun: loop(hold), attack: null, skill: null }) }), + form2: Object.freeze({ change: end, roles: form2 }), +}); +const STATUE = Object.freeze({ + stone: Object.freeze({ change: null, roles: Object.freeze({ idle: 'Sleep', deploy: 'Sleep', move: loop('Sleep'), stun: loop('Sleep'), attack: null, skill: null }) }), + fly: Object.freeze({ change: null, roles: clipSet('Idle_2', 'Move_2', 'Die_2', 'Attack_2') }), +}); +const JAKILL2 = clipSet('C2_Idle', 'C2_Move', 'C2_Die', 'C2_Attack'); export const FORMS = Object.freeze({ enemy_2025_syufo: Object.freeze({ crawl: Object.freeze({ change: 'Change', roles: clipSet('Idle_02', 'Move_02', 'Die_02', 'Attack_02') }), @@ -137,6 +151,15 @@ export const FORMS = Object.freeze({ husk: Object.freeze({ change: 'A_Die', roles: clipSet('B_Idle', 'B_Move', 'B_Die') }), revived: Object.freeze({ change: 'B_Revive', roles: Object.freeze({}) }), }), + enemy_1525_blkswb: rebirth('Revive1', 'Revive2', 'Revive3', clipSet('B_Idle', 'B_Move', 'B_Die', 'B_Attack')), + enemy_1535_wlfmster: rebirth('A_revive_1', 'A_revive_2', 'A_revive_3', clipSet('B_Idle', 'B_Move', 'B_Die', 'B_Attack')), + enemy_1539_reid: rebirth('Revive_Begin', 'Revive_Loop', 'Revive_End'), + enemy_1516_jakill: Object.freeze({ + reborn: Object.freeze({ change: 'C1_Die', roles: JAKILL2 }), + form2: Object.freeze({ change: null, roles: JAKILL2 }), + }), + enemy_1172_dugago: STATUE, + enemy_1172_dugago_2: STATUE, }); /** @@ -363,12 +386,14 @@ export class UnitView { } /** - * The unit changed mode (sim fx 'phase' { id, kind }): the mode's clip set (FORMS) after its change clip; a kind with - * no clip set of its own goes back to the manifest clips. Kept for a model built later. + * The unit changed mode (sim fx 'phase' { id, kind } or another fx's `form`): the mode's clip set (FORMS) after its + * change clip; null goes back to the manifest clips; a kind this skeleton has no clip set for (an arts barrier, a + * broken charge …) changes nothing. Kept for a model built later. */ setForm(kind) { const k = typeof kind === 'string' ? kind : null; if (k === this.form) return; + if (k && !FORMS[this.info.spine || this.info.defId]?.[k]) return; const had = !!this._formSpec(); this.form = k; this.info.form = k; diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 285da740..230cf841 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -694,7 +694,8 @@ function freeAllPrisoners(b) { * a rebirth of `dur` s — invulnerable, untargetable, released by its blocker, inert (no move / attack / skill). * `onKo(b, e, a)` runs at once (self-destruct, freeing prisoners …), `during(b, e, a, elapsed)` every tick of it; then the * enemy stands up with `hpRatio` of its max HP, `onReborn(b, e, a)` switches the form and `invincible` s of 无敌 follow. - * `a.state`: undefined → 'reborn' → 'form2'. + * `a.state`: undefined → 'reborn' → 'form2'. The 'telegraph' fx of the knock-out carries `form: 'reborn'` and the 'revive' + * fx `form: 'form2'`: models with those clip sets (render/units.js FORMS — 锏, 扎罗, “复仇者”, 杰斯顿) play them. */ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = null, onReborn = null, key = 'ab:reborn' } = {}) { const finish = (b, e, a) => { @@ -707,7 +708,7 @@ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = nu if (invincible > 0) b.addBuff(e, { key: `${key}:inv`, duration: invincible, visible: true, flags: { invulnerable: true } }); if (e.route) e.route.pts = null; e.atkCd = 0; - b.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'reborn' }); + b.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'reborn', form: 'form2' }); }; return { killed(c, b, e, a) { @@ -725,7 +726,7 @@ function reborn({ dur = 0, hpRatio = 1, invincible = 0, onKo = null, during = nu onTick: during ? ({ battle }) => during(battle, e, a, battle.time - a.t0) : null, onExpire: ({ battle }) => finish(battle, e, a), }); - b.fx('telegraph', { x: e.x, y: e.y, r: 1, dur, kind: 'reborn', id: e.id }); + b.fx('telegraph', { x: e.x, y: e.y, r: 1, dur, kind: 'reborn', id: e.id, form: 'reborn' }); return true; }, }; @@ -845,28 +846,35 @@ function husk({ hits, delay, stealthy = true, unblock = false, onHusk = null, ke }; } -/** 守墓石像: first knock-out → statue (DEF/RES up, immobile) → reborn as a flyer. */ +/** + * 守墓石像 (PRTS 守墓石像 天赋): 地面模式 — melee attacks only while blocked; the first defeat is an instant 重生 to 100 % HP + * into 转换模式 for stone.duration s — unblockable, 自缚 (immobile), 失衡免疫, immune to 浮空, DEF +stone.def, RES + * +stone.magic_resistance, no attack [ASSUMED: PRTS lists none] — then 飞行模式: a flyer (失衡免疫) whose ranged attacks (the + * data's 1.6 radius) deal arts damage and never target flyers; the HP is not refilled again. fx forms 'stone' / 'fly' + * (its Sleep and *_2 clips, render/units.js FORMS). <破碎支柱> (an event device) has no counterpart in this mode. + */ function statue(ab) { const dur = T(ab, 'stone.duration') ?? 0; return { - killed(c, b, e, a) { + spawn(b, e) { e.profile.melee = true; }, + killed(c, b, e, a, ab2) { if (a.done || !(dur > 0)) return false; a.done = true; a.noAtk = e.profile.noAttack; e.profile.noAttack = true; e.hp = e.s.maxHp; - b.fx('stone', { x: e.x, y: e.y, id: e.id, dur }); + ab2.immune = new Set([...(ab2.immune || []), 'levitate']); + b.fx('stone', { x: e.x, y: e.y, id: e.id, dur, form: 'stone' }); b.addBuff(e, { - key: 'ab:stone', duration: dur, visible: true, flags: { noMove: true }, + key: 'ab:stone', duration: dur, visible: true, flags: { noMove: true, unblockable: true, noDisplace: true }, mods: { defFlat: T(ab, 'stone.def') ?? 0, resFlat: T(ab, 'stone.magic_resistance') ?? 0 }, onExpire: ({ battle }) => { if (!e.alive) return; e.motion = 'FLY'; - e.profile.noAttack = a.noAtk; - e.hp = e.s.maxHp; - battle.addBuff(e, { key: 'ab:liftoff', duration: 0.2, flags: { unblockable: true } }); // releases its blocker + Object.assign(e.profile, { noAttack: a.noAtk, melee: false, dmgType: 'arts', canTarget: (u) => !u.isFlying }); + battle.addBuff(e, { key: 'ab:flight', persist: true, flags: { noDisplace: true } }); if (e.route) e.route.pts = null; - battle.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'fly' }); + battle.fx('revive', { x: e.x, y: e.y, id: e.id, kind: 'fly', form: 'fly' }); }, }); return true; @@ -2279,7 +2287,7 @@ export const KITS = Object.freeze({ enemy_1170_dushld_2: kitRefraction, // 深池重甲卫士队长 · refraction enemy_1169_duphlx: kitPhalanx, // 深池方阵步兵 · refraction + DEF +200 per nearby same unit enemy_1169_duphlx_2: kitPhalanx, // 深池方阵指挥官 · same - enemy_1172_dugago: (ab) => [...kitRefraction(ab), statue(ab)], // 守墓石像 · refraction; 1st KO → statue 10 s → flyer + enemy_1172_dugago: (ab) => [...kitRefraction(ab), statue(ab)], // 守墓石像 · refraction; melee when blocked; 1st KO → unblockable statue 10 s → arts flyer enemy_1172_dugago_2: (ab) => [...kitRefraction(ab), statue(ab)], // 愤怒的守墓石像 · same enemy_1174_duholy: kitHolyGuard, // 深池伙友卫队 · refraction; ASPD-down field next to 影刃 (taunt from data) enemy_1174_duholy_2: kitHolyGuard, // 深池伙友卫队精英 · same diff --git a/test/content/enemies_bosses.test.js b/test/content/enemies_bosses.test.js index fd0257e4..04163c67 100644 --- a/test/content/enemies_bosses.test.js +++ b/test/content/enemies_bosses.test.js @@ -648,17 +648,33 @@ for (const key of ['enemy_1169_duphlx', 'enemy_1169_duphlx_2']) { } for (const key of ['enemy_1172_dugago', 'enemy_1172_dugago_2']) { - test(`${nm(key)}: first knock-out ⇒ statue (DEF +${tb(key, 'stone.def')}) for ${tb(key, 'stone.duration')} s, then reborn as a flyer`, () => { - const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 7 }] }); + test(`${nm(key)}: melee only while blocked; first knock-out ⇒ unblockable statue (DEF +${tb(key, 'stone.def')}, 失衡 / 浮空 immune) for ${tb(key, 'stone.duration')} s, then a flyer with ranged arts attacks that skip flyers (PRTS 天赋)`, () => { + // 地面模式: an operator in its 1.6 radius but not blocking it is never attacked + const h0 = arena({ units: [{ chessId: 't_gun', row: 11, col: 7 }] }); + h0.step(); + put(h0, key, [10, 7]); + h0.run(6); + assert.equal(h0.unit('t_gun').stats.taken, 0, 'no ranged attack in its ground mode'); + const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 7 }, { chessId: 't_gun', row: 11, col: 7 }], kits: { t_gun: NOATK }, hooks: ['damaged'], captureNoisy: true }); h.step(); const e = put(h, key, [10, 7]); h.step(); killed(h, e, null); assert.ok(e.alive); + assert.equal(e.hp, e.s.maxHp, 'an instant 重生 to full HP'); assert.equal(e.s.def, E[key].stats.def + tb(key, 'stone.def')); + h.step(); + assert.ok(e.s.flags.unblockable && e.s.flags.noDisplace && !e.blockedBy, '转换模式: 无法被阻挡, 失衡免疫'); + assert.equal(h.b.applyStatus(e, 'levitate', { duration: 2 }), false, '免疫浮空'); + h.b.dealDamage(h.unit('t_gun'), e, { amount: e.s.maxHp * 0.3, type: 'true' }); + const hp = e.hp; h.run(tb(key, 'stone.duration') + 0.5); assert.equal(e.motion, 'FLY'); assert.ok(!e.blockedBy); + assert.equal(e.hp, hp, 'no second refill when it takes off'); + assert.ok(h.runUntil(() => e.stats.attacks > 0, 10), '飞行模式 attacks at range'); + assert.ok(h.unit('t_gun').stats.taken + h.unit('t_wall').stats.taken > 0, 'an operator in its radius'); + assert.ok(h.hooksOf('damaged').filter((c) => c.source === e && c.dmg.isAttack).every((c) => c.dmg.type === 'arts'), 'arts damage'); killed(h, e, null); assert.ok(!e.alive, 'second knock-out is final'); }); diff --git a/test/render/forms.test.js b/test/render/forms.test.js index a1bbbcb0..7e4165fd 100644 --- a/test/render/forms.test.js +++ b/test/render/forms.test.js @@ -212,6 +212,67 @@ describe('逐火 embers and the 再生 puppet (user report after 0.1.0, #8)', () }); }); +describe('the leaders\' 重生 and 守墓石像 (audit of the knock-out forms after report #5)', () => { + const REBIRTH = { + enemy_1525_blkswb: ['Revive1', 'Revive2', 'Revive3', 'B_Idle', 'B_Die'], + enemy_1535_wlfmster: ['A_revive_1', 'A_revive_2', 'A_revive_3', 'B_Idle', 'B_Die'], + enemy_1539_reid: ['Revive_Begin', 'Revive_Loop', 'Revive_End', 'Idle', 'Die'], + }; + for (const [id, [begin, hold, end, idle2, die2]] of Object.entries(REBIRTH)) { + test(`${id}: knock-out ⇒ ${begin}, ${hold} held through the 重生's stun, ${end}, then ${idle2}; dies on ${die2}`, async () => { + const anims = assets.enemies[id].spine.animations; + for (const name of [begin, hold, end, idle2, die2]) assert.ok(name in anims, name); + const v = await enemy(id); + v.sync(sample(UF.STUNNED, ANIM.STUN), 1); + v.setForm('reborn'); + assert.equal(clip(v), begin); + frames(v, Math.ceil(anims[begin] * 60) + 5); + assert.equal(clip(v), hold, 'held (its stun role) while the sim keeps it stunned'); + v.sync(sample(0, ANIM.IDLE), 6); + v.setForm('form2'); + assert.equal(clip(v), end); + frames(v, Math.ceil(anims[end] * 60) + 5); + assert.equal(clip(v), idle2); + v.die(); + assert.equal(clip(v), die2); + }); + } + + test('enemy_1516_jakill 杰斯顿: C1_Die is its 4 s 重生, then the killer form C2_*', async () => { + const v = await enemy('enemy_1516_jakill'); + v.setForm('reborn'); + assert.equal(clip(v), 'C1_Die'); + frames(v, 245); + v.setForm('form2'); + v.sync(sample(0, ANIM.MOVE), 5); + assert.equal(clip(v), 'C2_Move'); + v.onAttack(null, 5.5); + assert.equal(clip(v), 'C2_Attack'); + }); + + test('enemy_1172_dugago 守墓石像: the statue on Sleep, the flyer on *_2', async () => { + const v = await enemy('enemy_1172_dugago'); + v.setForm('stone'); + v.sync(sample(), 1); + assert.equal(clip(v), 'Sleep'); + v.setForm('fly'); + v.sync(sample(UF.FLYING, ANIM.MOVE), 2); + assert.equal(clip(v), 'Move_2'); + v.die(); + assert.equal(clip(v), 'Die_2'); + }); + + test('a kind without a clip set of that skeleton (an arts barrier, a broken charge) keeps the current form', async () => { + const v = await enemy('enemy_10081_mpplai'); + v.setForm('translator_fuchou'); + frames(v, 130); + v.setForm('artsBarrier'); + assert.equal(v.form, 'translator_fuchou'); + v.sync(sample(), 3); + assert.equal(clip(v), 'B_Idle'); + }); +}); + test('render/app.js hands the sim\'s fx \'phase\' kind — or the `form` of any fx — to the view and keeps the mode on the unit info', () => { const src = readFileSync(path.join(ROOT, 'public/js/render/app.js'), 'utf8'); assert.match(src, /e\[1\] === 'phase' \? ex4\.kind : 'form' in ex4 \? ex4\.form[\s\S]{0,200}inf\.form = [\s\S]{0,120}setForm\?\.\(form\)/); diff --git a/test/sim/feedback1-transform.test.js b/test/sim/feedback1-transform.test.js index 35db61ef..64761e08 100644 --- a/test/sim/feedback1-transform.test.js +++ b/test/sim/feedback1-transform.test.js @@ -211,6 +211,45 @@ describe('#8 深池逐火: a knock-out is a walking 隐匿 ember that a blocker }); }); +// --------------------------------------------------------------------------------------------------------------- +// audit: every other knock-out form of the mode ends in a targetable, killable unit and tells the model its clip set + +describe('audit: the leaders\' 重生 and 守墓石像 — no vanishing, killable afterwards, the model follows', () => { + const CASES = [ + ['enemy_1525_blkswb', 'Reborn.duration', 'Reborn.invincible', 'reborn', 'form2'], + ['enemy_1535_wlfmster', 'Reborn.duration', 'Passive2.invincible_time', 'reborn', 'form2'], + ['enemy_1539_reid', 'Reborn.duration', 'Reborn.invincible', 'reborn', 'form2'], + ['enemy_1516_jakill', 'reborn.duration', null, 'reborn', 'form2'], + ['enemy_1517_xi', 'reborn.duration', 'reborn.invincible', 'reborn', 'form2'], + ['enemy_1512_mcmstr', 'reborn.duration', null, 'reborn', 'form2'], + ['enemy_10098_crhro', 'reborn.duration', 'reborn.invincible', 'reborn', 'form2'], + ['enemy_1172_dugago', 'stone.duration', null, 'stone', 'fly'], + ['enemy_1172_dugago_2', 'stone.duration', null, 'stone', 'fly'], + ]; + for (const [key, durKey, invKey, f1, f2] of CASES) { + test(`${key} ${E[key].name}: knock-out ⇒ form '${f1}' (no die event, no kill), then '${f2}' — targetable and killed for good by the next knock-out`, () => { + const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 4 }], kits: QUIET_GUNS }); + h.step(); + const e = put(h, key, { pos: [10, 7], mods: { speedMul: 0 } }); + const gun = h.unit('t_gun'); + h.b.kill(e, gun); + assert.ok(e.alive, 'not removed'); + const fxOf = (form) => h.eventsOf('fx').some((ev) => ev[4] && ev[4].id === e.id && ev[4].form === form); + assert.ok(fxOf(f1), `fx form '${f1}' at the knock-out`); + assert.equal(h.eventsOf('die').filter((ev) => ev[1] === e.id).length, 0, 'no die event'); + assert.equal(h.b.killed, 0, 'no kill'); + h.run((tb(key, durKey) ?? 0) + (invKey ? tb(key, invKey) ?? 0 : 0) + 0.3); + assert.ok(e.alive && fxOf(f2), `fx form '${f2}' when it stands up`); + assert.ok(!e.s.flags.untargetable && !e.s.flags.invulnerable, 'targetable and vulnerable again'); + if (!e.s.flags.stealth && !e.isFlying) assert.ok(canTargetEnemy(gun, e, gun.profile), 'a ranged operator can target it'); + h.b.kill(e, gun); + assert.ok(!e.alive && e.removeReason === 'killed', 'the second knock-out is its death'); + assert.equal(h.b.killed, 1); + assert.equal(h.eventsOf('die').filter((ev) => ev[1] === e.id).length, 1); + }); + } +}); + // --------------------------------------------------------------------------------------------------------------- // #5 转译基底·α