// test/content/op_mantra.test.js — the 自选 operator kit of 真言 (char_4204_mantra, 6★ 本源术师; kit // server/sim/content/kits/ops/op-mantra.js), fielded the production way (a DIY slot + its `diy` pick, simdata getDiy) in // every form: tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module or PRI-X // “中枢神经探测模块” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json (the form of that // slot status); the fidelity checklist of kits/README.md item by item. // Run: node --test test/content/op_mantra.test.js import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js'; import { KITTED_CHARS, OPERATOR_KITS, KITS } from '../../server/sim/content/kits/index.js'; import { diyPool, validateDiyPicks } from '../../shared/diy.js'; const load = (f) => JSON.parse(readFileSync(new URL(`../../data/${f}.json`, import.meta.url), 'utf8')); const CHESS = load('chess'); const BACKUPS = load('backups'); const MANTRA = 'char_4204_mantra'; const FORMS = BACKUPS.units[MANTRA].forms; const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' }; const PRIX = 'uniequip_002_mantra'; const S1 = 'skchr_mantra_1', S2 = 'skchr_mantra_2', S3 = 'skchr_mantra_3'; const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0']; const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id); const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null); const talentOf = (tier, elite, mod, i) => { const m = elite ? modOf(tier, mod) : null; return m?.talentChanges.find((t) => t.talentIndex === i)?.bb ?? formOf(tier, elite).talents.find((t) => t.index === i).bb; }; const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`); const dummy = (key, o = {}) => enemyRec({ key, hp: 1e9, speed: 0, mass: 0, ...o }); const ENEMIES = { enemy_dummy: dummy('enemy_dummy'), enemy_fly: dummy('enemy_fly', { motion: 'FLY' }), enemy_shooter: dummy('enemy_shooter', { atk: 1, bat: 1, range: 4 }), enemy_walker: enemyRec({ key: 'enemy_walker', hp: 1e9, speed: 0.3, mass: 0 }), enemy_immune: dummy('enemy_immune', { otherImmunities: ['palsy'] }), }; const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, PRIX].map((m) => [t, true, m]))]; const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`; function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, setup, stage } = {}) { const h = makeBattle({ defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed, setup, ...(stage ? { flat: stage } : null), flags: { dpPerSec: 0 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'attack', 'elementBurst'], captureNoisy: true, units: [{ uid: 1, diy: { slot: SLOT[tier], charId: MANTRA, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others], }); h.step(); return { h, u: h.unit(1) }; } const atkHits = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack); const tagged = (h, u, tag, src = u) => h.hooksOf('damaged').filter((c) => (c.source === src || c.credit === src) && (c.dmg?.tags || []).includes(tag)); function done(h) { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); } function cast(u) { u.skill.gainSp(999); assert.ok(u.skill.activate('test')); } const palsy = (e) => e.findBuff('palsy')?.stacks ?? 0; test('真言 in every 自选 form: her operator kit (all three skills authored), stats + PRI-X attributes, 3-1 range, ranged arts that hits air units, targetable by ground enemies, 萨尔贡 bond, no 特质', () => { assert.equal(OPERATOR_KITS[MANTRA], KITS[MANTRA]); for (const f of FORMS_ALL) { const [tier, elite, mod] = f; for (const skill of [0, 1, 2]) { const { h, u } = field({ tier, elite, mod, skill }); const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null; assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [MANTRA, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f)); assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def], [form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def], `${label(f)}: stats`); assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 1.6], `${label(f)}: 本源术师`); assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-1`); assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['sargonShip'], []], `${label(f)}: bonds / 特质`); assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`); done(h); } } assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1071, 614, 1308, 688]); assert.deepEqual([modOf(5, PRIX).attr, modOf(6, PRIX).attr], [{ maxHp: 90, atk: 43 }, { maxHp: 145, atk: 87 }]); }); test('a 自选 pick: 真言 is offered at tiers 5 and 6 (she has a kit) and a roster with her passes validateDiyPicks', () => { const data = { chess: CHESS, backups: BACKUPS }; assert.ok(KITTED_CHARS.includes(MANTRA)); for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(MANTRA), `tier ${t}`); assert.deepEqual(validateDiyPicks({ [SLOT[5]]: { charId: MANTRA, skillIndex: 2, uniEquipId: PRIX } }, { data, kitted: KITTED_CHARS }), { ok: true, picks: { [SLOT[5]]: { charId: MANTRA, skillIndex: 2, uniEquipId: PRIX } } }); }); test('S1 共鸣溃缩 (AUTO, attack SP 3, data DEFAULT): every 4th attack deals 230 % / 275 % ATK arts, then 15 % / 20 % of it as 神经 损伤, then — on a target in its 神经 burst — 150 % / 160 % ATK 元素伤害', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const sk = skillOf(tier, elite, S1); const { h, u } = field({ tier, elite, skill: 0 }); assert.deepEqual([u.skill.rule, u.skill.kind, u.skill.spType, u.skill.spCost, sk.bb.atk_scale, sk.bb.ep_damage_ratio, sk.bb.element_atk_scale], ['DEFAULT', 'instant', 'attack', 3, elite ? 2.75 : 2.3, elite ? 0.2 : 0.15, elite ? 1.6 : 1.5], `T${tier}`); const e = h.spawn('enemy_dummy', { pos: [10, 7] }); h.run(16 * 1.6 + 0.5); const hits = atkHits(h, u).filter((c) => c.target === e); const skillHits = hits.filter((c) => c.dmg.isSkill); assert.ok(hits.length >= 15, `T${tier}: ${hits.length} attacks`); hits.forEach((c, i) => assert.equal(c.dmg.isSkill, i % 4 === 3, `T${tier}: attack ${i + 1}`)); const fills = h.hooksOf('damaged').filter((c) => c.source === u && c.type === 'element'); for (const c of skillHits) { const fill = fills.find((x) => Math.abs(x.t - c.t) < 1e-9); if (fill) approx(fill.amount, c.amount * sk.bb.ep_damage_ratio, `T${tier}: 神经 = ${sk.bb.ep_damage_ratio * 100} % of the damage`); } approx(skillHits[0].amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`); assert.ok(fills.length >= 1 && fills.every((x) => x.dmg.element === 'neural')); // in its 神经 burst: + element_atk_scale × ATK 元素伤害 after the 神经 fill (a fresh target one point short of it) h.b.kill(e, null); const e2 = h.spawn('enemy_dummy', { pos: [10, 7] }); e2.elem.neural = e2.gaugeMax - 1; const n0 = h.hooksOf('damaged').length; h.runUntil(() => h.hooksOf('damaged').slice(n0).some((c) => c.source === u && c.dmg?.isSkill && c.dmg?.isAttack), 8); h.step(); const after = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.type === 'elemental' && (c.dmg.tags || []).includes('mantra')); assert.ok(h.hooksOf('elementBurst').some((c) => c.target === e2 && c.element === 'neural'), `T${tier}: her 神经 burst it`); assert.equal(after.length, 1, `T${tier}: the burst made by its own 神经 counts`); approx(after[0].amount, u.s.atk * sk.bb.element_atk_scale, `T${tier}: ${sk.bb.element_atk_scale * 100} % ATK 元素伤害`); done(h); } }); test('S2 意识联协 (MANUAL, data DEFAULT): 25 s, attack interval 1.6 − 0.6 / 0.7 s; 170 % / 210 % ATK on the target, then 2 / 3 jumps at 3.2 tiles/s to the nearest new enemy within 2.0 tiles for 100 % / 110 %; 神经 riders on every hit', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const sk = skillOf(tier, elite, S2); const b = sk.bb; const { h, u } = field({ tier, elite, skill: 1 }); assert.deepEqual([u.skill.rule, sk.duration, b.base_attack_time, b['attack@atk_scale'], b['attack@chain.atk_scale'], b.chain_times, b['attack@ep_damage_ratio'], b['attack@element_atk_scale']], ['DEFAULT', 25, elite ? -0.7 : -0.6, elite ? 2.1 : 1.7, elite ? 1.1 : 1, elite ? 3 : 2, elite ? 0.15 : 0.13, elite ? 0.2 : 0.18], `T${tier}`); // a line of enemies 1.5 tiles apart (the 4th is 2.1 from the 3rd: out of the jump radius at tier 6) const main = h.spawn('enemy_dummy', { pos: [10, 7] }); const j1 = h.spawn('enemy_dummy', { pos: [10, 8.5] }); const j2 = h.spawn('enemy_fly', { pos: [10, 10] }); const j3 = h.spawn('enemy_dummy', { pos: [11.5, 10] }); const out = h.spawn('enemy_dummy', { pos: [11.5, 12.1] }); cast(u); approx(u.s.interval, 1.6 + b.base_attack_time, `T${tier}: interval`); const a0 = h.hooksOf('attack').filter((c) => c.attacker === u).length; h.runUntil(() => h.hooksOf('attack').filter((c) => c.attacker === u).length > a0, 2); const at = h.hooksOf('attack').filter((c) => c.attacker === u).slice(-1)[0]; assert.equal(at.targets[0], main, `T${tier}: the first target`); h.run(3); const id = atkHits(h, u).find((c) => c.target === main && c.t >= at.t).dmg.attackId; const chainHits = atkHits(h, u).filter((c) => c.dmg.attackId === id); const order = chainHits.map((c) => c.target); assert.deepEqual(order, [main, j1, j2, j3].slice(0, 1 + b.chain_times), `T${tier}: the nearest new enemy each time`); assert.ok(!order.includes(out), `T${tier}: 2.1 tiles: out of reach`); approx(chainHits[0].amount, u.s.atk * b['attack@atk_scale'], `T${tier}: main`); for (const c of chainHits.slice(1)) { approx(c.amount, u.s.atk * b['attack@chain.atk_scale'], `T${tier}: jump`); assert.ok(c.dmg.tags.includes('chain') && c.dmg.isSkill); } approx(chainHits[2].t - chainHits[1].t, 1.5 / 3.2, `T${tier}: 3.2 tiles/s`, 0.1); for (const [i, c] of chainHits.entries()) { const fill = h.hooksOf('damaged').find((x) => x.source === u && x.type === 'element' && x.target === c.target && Math.abs(x.t - c.t) < 1e-9); assert.ok(fill, `T${tier}: a 神经 fill with hit ${i}`); approx(fill.amount, c.amount * b['attack@ep_damage_ratio'], `T${tier}: 神经 ${i}`); } h.runUntil(() => !u.skill.active, 30); approx(u.s.interval, 1.6, `T${tier}: interval back`); done(h); } }); test('S3 无言为真 (MANUAL, data ACTIVE_RANGE on its 3-17): cast for an enemy outside her own range, 40 s, ATK +170 % / +210 %, 2 targets, the 3-17 while on; 隐匿 enemies on it are revealed', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const sk = skillOf(tier, elite, S3); const { h, u } = field({ tier, elite, skill: 2 }); assert.deepEqual([u.skill.rule, sk.rangeId, sk.duration, sk.bb.atk, sk.bb['attack@max_target'], sk.bb.max_trigger_cnt, sk.bb.per_active, sk.bb.interval_projectile_trigger, sk.bb.atk_scale], ['ACTIVE_RANGE', '3-17', 40, elite ? 2.1 : 1.7, 2, 3, 2, 1.5, elite ? 1.45 : 1.1], `T${tier}`); u.skill.gainSp(999); h.run(1); assert.equal(u.skill.activations, 0); const far = h.spawn('enemy_dummy', { pos: [12, 6] }); // dRow +2: the 3-17 only assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast`); assert.ok(!formOf(tier, elite).rangeGrid.some(([r, c]) => r === far.y - 10 && c === far.x - 5), `T${tier}: the enemy is not on her own 3-1`); assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-17`); approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`); const sneak = h.spawn('enemy_dummy', { pos: [10, 7] }); h.b.addBuff(sneak, { key: 'test:stealth', flags: { stealth: true } }); h.step(); assert.ok(sneak.s.flags.reveal, `T${tier}: revealed on her range`); const a0 = h.hooksOf('attack').filter((c) => c.attacker === u).length; h.run(4); const atks = h.hooksOf('attack').filter((c) => c.attacker === u).slice(a0); assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 2), `T${tier}: 2 targets`); h.runUntil(() => !u.skill.active, 45); h.run(0.3); assert.ok(!sneak.s.flags.reveal, `T${tier}: hidden again once it ends`); assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-1`); done(h); } }); test('S3 无言为真 麻痹付与 and the cap 2: 0.1 s after the cast every enemy on her range gets 2 麻痹; another operator on her range starting a skill grants again (3 times per cast, a grant kept while nobody is there); stacks over 2 become 溢出麻痹 — each an 元素伤害 bounce of 110 % / 145 % ATK on the holder and the nearest other enemy within 2.0 tiles, one at once and one per 1.5 s', () => { for (const [tier, elite] of [[5, false], [6, true]]) { const sk = skillOf(tier, elite, S3); const others = [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 6 }, { uid: 3, chessId: 'chess_char_1_08_a', row: 9, col: 2 }]; const { h, u } = field({ tier, elite, skill: 2, others }); const inside = h.unit(2), outside = h.unit(3); for (const a of [inside, outside]) { a.skill.sp = 0; a.skill.charges = 0; } cast(u); h.run(0.05); // nobody on her range yet: the grant at 0.1 s is kept h.run(0.1); assert.equal(u.mem.mantraS3.grants, 3, `T${tier}: kept`); const e = h.spawn('enemy_dummy', { pos: [10, 7] }); const near = h.spawn('enemy_dummy', { pos: [10, 8] }); h.step(); // an operator outside her range starting a skill: nothing outside.skill.gainSp(999); outside.skill.activate('test'); h.step(); assert.equal(palsy(e), 0, `T${tier}: an operator off her range`); // the one on her range: the first grant (2 stacks, at the cap) inside.skill.gainSp(999); inside.skill.activate('test'); h.step(); assert.deepEqual([palsy(e), palsy(near), u.mem.mantraS3.grants], [2, 2, 2], `T${tier}: 2 麻痹 each`); assert.equal(tagged(h, u, 'mantraOverflow').length, 0, `T${tier}: no overflow yet`); // the second: 2 over the cap each ⇒ 溢出麻痹 2 each, one bounce at once each const n0 = h.hooksOf('damaged').length; inside.skill.gainSp(999); inside.skill.activate('test'); h.step(); assert.deepEqual([palsy(e), palsy(near)], [2, 2], `T${tier}: capped at 2`); let bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow')); assert.deepEqual(bounces.map((c) => c.target.id).sort(), [e.id, near.id].sort(), `T${tier}: each holder hit at once`); for (const c of bounces) { approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`); assert.deepEqual([c.type, c.dmg.element], ['elemental', 'neural']); } h.run(0.5); // the jumps land (1 tile at 3.2 tiles/s) bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow')); assert.equal(bounces.length, 4, `T${tier}: holder + the nearest other, twice`); h.run(1.1); // 1.5 s after: the second 溢出麻痹 of each bounces = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && (c.dmg.tags || []).includes('mantraOverflow')); assert.ok(bounces.length >= 6, `T${tier}: the next one 1.5 s later (${bounces.length})`); // the third grant, then no more inside.skill.gainSp(999); inside.skill.activate('test'); h.step(); assert.equal(u.mem.mantraS3.grants, 0, `T${tier}: 3 grants used`); const g = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'palsy').length; inside.skill.gainSp(999); inside.skill.activate('test'); h.step(); assert.equal(h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'palsy').length, g, `T${tier}: no 4th grant`); done(h); } }); test('S3 无言为真: an enemy holding 3 麻痹 entering her range keeps 2 and bounces the third; 溢出麻痹 left when it leaves the range or the skill ends all bounce at once; a 麻痹免疫 enemy counts the stacks she gives it', () => { const { h, u } = field({ tier: 6, elite: true, skill: 2 }); cast(u); h.run(0.2); // the grant of 0.1 s finds nobody (kept, never fired again here) const e = h.spawn('enemy_dummy', { pos: [10, 12] }); // off her range h.b.applyStatus(e, 'palsy', { value: 3 }); h.step(); assert.equal(palsy(e), 3, 'off her range: cap 3'); e.x = 7; h.step(); assert.equal(palsy(e), 2, 'on her range: 2'); assert.equal(tagged(h, u, 'mantraOverflow').filter((c) => c.target === e).length, 1, 'the third bounced'); // a neural burst's 3 麻痹 on a holder at the cap: 3 溢出麻痹 — one at once, two left; it leaves ⇒ both at once const n0 = tagged(h, u, 'mantraOverflow').length; h.b.applyStatus(e, 'palsy', { value: 3 }); assert.equal(tagged(h, u, 'mantraOverflow').length - n0, 1, 'one at once'); e.x = 13; h.step(); assert.equal(tagged(h, u, 'mantraOverflow').length - n0, 3, 'the two left at once when it leaves'); assert.equal(palsy(e), 2, 'its stacks stay (cap 3 again)'); // the skill ends with 溢出麻痹 held: all of it bounces const f = h.spawn('enemy_dummy', { pos: [10, 6] }); h.step(); h.b.applyStatus(f, 'palsy', { value: 3 }); h.b.applyStatus(f, 'palsy', { value: 3 }); const n1 = tagged(h, u, 'mantraOverflow').filter((c) => c.target === f).length; u.skill.end('test'); assert.equal(tagged(h, u, 'mantraOverflow').filter((c) => c.target === f).length - n1, 2, 'the rest at the end'); // 麻痹免疫: her grants count (ineffective stacks), so the second grant overflows const { h: h2, u: u2 } = field({ tier: 6, elite: true, skill: 2, others: [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 6 }] }); const imm = h2.spawn('enemy_immune', { pos: [10, 7] }); cast(u2); h2.run(0.2); assert.equal(palsy(imm), 0, 'no 麻痹 buff (immune)'); assert.equal(imm.mem.mantraPalsy, 2, 'counted'); const ally = h2.unit(2); ally.skill.gainSp(999); ally.skill.activate('test'); h2.step(); assert.equal(tagged(h2, u2, 'mantraOverflow').filter((c) => c.target === imm).length, 1, 'its 溢出麻痹 bounces'); done(h); done(h2); }); test('T1 噤声限域: an enemy whose attack a 麻痹 stack interrupts takes 145 % (PRI-X stage 3: 170 %) of her ATK as 元素伤害 (tagged 持续伤害); 13 % / 18 % of the interrupts keep the stack; not for an enemy she cannot select (隐匿)', () => { for (const f of [[5, false, null], [6, true, null], [5, true, PRIX], [6, true, PRIX]]) { const [tier, elite, mod] = f; const t = talentOf(tier, elite, mod, 0); assert.deepEqual(t, elite && mod === PRIX && tier === 6 ? { atk_scale: 1.7, prob: 0.18 } : { atk_scale: 1.45, prob: 0.13 }, label(f)); const { h, u } = field({ tier, elite, mod, skill: 0, seed: 13 }); const e = h.spawn('enemy_shooter', { pos: [10, 8] }); h.b.applyStatus(u, 'disarm', { duration: 999, source: null }); // her attacks would kill nothing, but keep it clean h.run(0.6); // the aura's 0.5 s check has marked it let interrupts = 0, kept = 0; h.b.on('palsyTrigger', (c) => { if (c.enemy === e) { interrupts++; } }); for (let i = 0; i < 400; i++) { h.b.applyStatus(e, 'palsy', { value: 1 }); const before = interrupts; h.runUntil(() => interrupts > before, 3); h.step(); if (palsy(e) > 0) { kept++; h.b.removeBuff(e, 'palsy'); } } const hush = tagged(h, u, 'mantraHush'); assert.equal(hush.length, interrupts, `${label(f)}: one hit per interrupt`); for (const c of hush.slice(0, 5)) { approx(c.amount, u.s.atk * t.atk_scale, `${label(f)}: ${t.atk_scale * 100} % ATK`); assert.deepEqual([c.type, c.dmg.element, c.dmg.tags.includes('dot')], ['elemental', 'neural', true]); } assert.ok(Math.abs(kept / interrupts - t.prob) < 0.05, `${label(f)}: kept ${kept} / ${interrupts} ≈ ${t.prob}`); done(h); } // 隐匿: no mark, no hit; during S3 the check ignores 隐匿 const { h, u } = field({ tier: 6, elite: true, skill: 2, seed: 1 }); const e = h.spawn('enemy_shooter', { pos: [10, 2] }); // behind her: it shoots her, she cannot see it (and S3 does not reveal it) h.b.addBuff(e, { key: 'test:stealth', flags: { stealth: true } }); let interrupts = 0; h.b.on('palsyTrigger', (c) => { if (c.enemy === e) interrupts++; }); h.run(0.6); h.b.applyStatus(e, 'palsy', { value: 2 }); h.run(2.2); assert.equal(interrupts, 2, 'its attacks were interrupted'); assert.equal(tagged(h, u, 'mantraHush').length, 0, 'a 隐匿 enemy: no effect'); cast(u); h.run(0.6); assert.ok(!e.s.flags.reveal, 'not revealed (off her range)'); h.b.applyStatus(e, 'palsy', { value: 2 }); h.run(2.2); assert.equal(tagged(h, u, 'mantraHush').length, 2, 'S3: the check ignores 隐匿'); done(h); }); test('T2 全局洞悉: the 侵入点 nearest to her (Manhattan) — every enemy that comes out of it gets 1 麻痹 0.1 s later (隐匿 too); the other gate\'s do not', () => { for (const [row, gate, other] of [[10, 0, 1], [11, 1, 0]]) { // flat stage gates: route 0 starts at (9,10), route 1 at (12,10) const { h, u } = field({ tier: 5, skill: 0, row, seed: 2 }); assert.deepEqual(u.mem.mantraGate, gate === 0 ? [9, 10] : [12, 10], `row ${row}: the nearest gate`); const a = h.spawn('enemy_walker', { routeIndex: gate }); const b = h.spawn('enemy_walker', { routeIndex: other }); h.b.addBuff(a, { key: 'test:stealth', flags: { stealth: true } }); h.step(); assert.equal(palsy(a), 0, 'not at once'); h.run(0.1); assert.deepEqual([palsy(a), palsy(b)], [1, 0], `row ${row}`); done(h); } // a tie (two gates at the same distance) is drawn at random const picks = new Set(); for (let seed = 1; seed <= 12; seed++) { const { h, u } = field({ tier: 5, skill: 0, row: 10, col: 10, seed, stage: { rows: { 11: '##hrrrrrrrSrrrrrrrf##' } } }); picks.add(String(u.mem.mantraGate)); done(h); } assert.deepEqual([...picks].sort(), ['11,10', '9,10'], 'both gates one tile away get drawn'); }); test('PRI-X “中枢神经探测模块” (stages 1 and 3): damage to an enemy in an element burst ×1.1; none without it', () => { for (const f of [[5, true, PRIX], [6, true, PRIX], [6, true, null], [5, false, null]]) { const [tier, elite, mod] = f; const { h, u } = field({ tier, elite, mod, skill: 0, seed: 8 }); const plain = h.spawn('enemy_dummy', { pos: [10, 7] }); h.run(1.7); const n0 = atkHits(h, u).length; h.b.addBuff(plain, { key: 'burnBurst', duration: 99, flags: { burstLock: true } }); h.run(1.7); const a = atkHits(h, u)[0].amount, b = atkHits(h, u).slice(n0)[0].amount; approx(b / a, elite && mod === PRIX ? 1.1 : 1, label(f)); if (elite && mod === PRIX) assert.equal(modOf(tier, PRIX).traitOverride.bb.damage_scale, 1.1); done(h); } });