313 lines
21 KiB
JavaScript
313 lines
21 KiB
JavaScript
// test/content/op_sleach.test.js — the 自选 operator kit of 琴柳 (char_479_sleach, 6★ 执旗手; kit
|
||
// server/sim/content/kits/ops/op-sleach.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, BEA-X 牧人的歌 or
|
||
// BEA-Y “友谊万岁” at stage 1 (tier 5) / 3 (tier 6). Every number is read back from data/backups.json; the fidelity checklist
|
||
// of kits/README.md item by item.
|
||
// Run: node --test test/content/op_sleach.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';
|
||
import { FLAG_FLIGHT, FLAG_ALLY, FLAG_ENEMY, S2_KEY, FRONT_KEY, CAMOU_KEY } from '../../server/sim/content/kits/ops/op-sleach.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 SL = 'char_479_sleach';
|
||
const FORMS = BACKUPS.units[SL].forms;
|
||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||
const BX = 'uniequip_002_sleach', BY = 'uniequip_003_sleach';
|
||
const S1 = 'skcom_assist_cost[3]', S2 = 'skchr_sleach_2', S3 = 'skchr_sleach_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);
|
||
/** The talents of a form with the module's changes (the composed record's). */
|
||
const talentOf = (tier, elite, mod, i) => {
|
||
const ch = modOf(tier, elite ? mod : null)?.talentChanges?.find((t) => t.talentIndex === i);
|
||
return ch ?? formOf(tier, elite).talents.find((t) => t.index === i);
|
||
};
|
||
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_walk: enemyRec({ key: 'enemy_walk', hp: 1e9, speed: 0.6, mass: 0 }) };
|
||
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, BX, BY].map((m) => [t, true, m]))];
|
||
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
|
||
const TEXAS = 'chess_char_1_08_a', YAK = 'chess_char_1_02_a', PROVE = 'chess_char_1_07_a';
|
||
|
||
/** A battle with 琴柳 as uid 1 at (row, col) facing RIGHT, plus `others`. */
|
||
function field({ tier = 5, elite = false, mod = null, skill = 0, row = 10, col = 5, others = [], seed = 5, flags = {} } = {}) {
|
||
const h = makeBattle({
|
||
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
|
||
flags: { dpPerSec: 0, dpMax: 999, ...flags }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'deploy', 'death', 'attack'], captureNoisy: true,
|
||
units: [{ uid: 1, diy: { slot: SLOT[tier], charId: SL, skillIndex: skill, uniEquipId: mod }, elite, row, col }, ...others],
|
||
});
|
||
h.step();
|
||
return { h, u: h.unit(1) };
|
||
}
|
||
function done(h) {
|
||
checkInvariants(h.b);
|
||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||
}
|
||
const dpOf = (h) => h.b.players[0].dp;
|
||
|
||
test('琴柳 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 1-1, 执旗手 (blocks 1, melee physical, ground-only), ground-targetable, 维多利亚; the three triggers SP_FULL (the 执旗手 row)', () => {
|
||
assert.equal(OPERATOR_KITS[SL], KITS[SL]);
|
||
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, sk = form.skills[skill];
|
||
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [SL, SLOT[tier], sk.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 + (m?.attr.def ?? 0)], `${label(f)}: stats`);
|
||
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.profile.sub, u.base.bat], [1, 'melee', false, 'phys', 'bearer', 1.3], `${label(f)}: 执旗手`);
|
||
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1`);
|
||
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['victoriaShip'], []], `${label(f)}: bonds / 特质`);
|
||
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
|
||
assert.deepEqual([u.skill.rule, u.skill.spCost, u.skill.kind, sk.trigger.rawRule], ['SP_FULL', sk.spCost, 'duration', 'ALWAYS'], `${label(f)}: trigger / SP`);
|
||
done(h);
|
||
}
|
||
}
|
||
// the form numbers (zh_CN, full potential): E2 Lv1 1339 / 483 / 311, E2 Lv60 1668 / 541 / 348; BEA-X +150 / +48 → +200 / +68 HP / ATK,
|
||
// BEA-Y +150 / +45 → +200 / +70 HP / DEF
|
||
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], [1339, 483, 1668, 541]);
|
||
assert.deepEqual([modOf(5, BX).attr, modOf(6, BX).attr, modOf(5, BY).attr, modOf(6, BY).attr], [{ maxHp: 150, atk: 48 }, { maxHp: 200, atk: 68 }, { maxHp: 150, def: 45 }, { maxHp: 200, def: 70 }]);
|
||
});
|
||
|
||
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(SL));
|
||
for (const t of [5, 6]) assert.ok(diyPool(t, { data, kitted: KITTED_CHARS }).includes(SL), `tier ${t}`);
|
||
assert.deepEqual(validateDiyPicks({ [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } }, { data, kitted: KITTED_CHARS }),
|
||
{ ok: true, picks: { [SLOT[6]]: { charId: SL, skillIndex: 2, uniEquipId: BY } } });
|
||
});
|
||
|
||
test('S1 支援号令·γ型: cast as soon as its SP is full (no enemy needed), 8 s, no attack, blocks 0 meanwhile; +1 DP every 0.44 s from 0.44 s, 18 in all', () => {
|
||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
|
||
const sk = skillOf(tier, elite, S1);
|
||
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: YAK, row: 10, col: 7 }] });
|
||
assert.deepEqual([sk.duration, sk.bb.value, sk.bb.interval, sk.bb.cost, sk.initSp, sk.spCost], [8, 18, 0.44, 1, elite ? 12 : 11, elite ? 29 : 32], `T${tier}`);
|
||
h.run(sk.spCost - sk.initSp - 0.5);
|
||
assert.equal(u.skill.activations, 0, `T${tier}: not before its SP is full`);
|
||
h.b.players[0].dp = 0;
|
||
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast at full SP with no enemy on the field`);
|
||
const t0 = h.b.time;
|
||
let attacksOn = 0;
|
||
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) attacksOn++; });
|
||
assert.equal(u.s.blockCnt, 0, `T${tier}: 执旗手 — block 0 while it runs`);
|
||
h.spawn('enemy_dummy', { pos: [10, 6] });
|
||
h.runUntil(() => dpOf(h) >= 1, 1);
|
||
approx(h.b.time - t0, 0.44, `T${tier}: the first DP after one interval`, 0.1);
|
||
h.runUntil(() => !u.skill.active, 9);
|
||
approx(h.b.time - t0, 8, `T${tier}: 8 s`, 0.01);
|
||
approx(dpOf(h), 18, `T${tier}: 18 DP`);
|
||
assert.equal(attacksOn, 0, `T${tier}: 停止攻击`);
|
||
h.run(1);
|
||
approx(dpOf(h), 18, `T${tier}: nothing after`);
|
||
assert.equal(u.s.blockCnt, 1, `T${tier}: block back`);
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('S2 信仰传承: the flag goes to the operator with the lowest HP share on the x-1 (not her, not a 孤立 one, not out of range): DEF +def and 生命回复速度 ATK × ratio there (through 禁疗), T1\'s area follows the flag; 20 DP over 15 s; back to her after', () => {
|
||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BY]]) {
|
||
const sk = skillOf(tier, elite, S2);
|
||
const others = [
|
||
{ uid: 2, chessId: YAK, row: 10, col: 6 }, // [0, 1]: in range, 80 %
|
||
{ uid: 3, chessId: TEXAS, row: 11, col: 4 }, // [1, -1]: in range, 40 % — the target
|
||
{ uid: 4, chessId: PROVE, row: 12, col: 7 }, // [2, 2]: outside the x-1, 10 %
|
||
{ uid: 5, chessId: 'chess_char_1_09_a', row: 9, col: 5 }, // [-1, 0]: in range, 5 % but 孤立 — never chosen
|
||
];
|
||
const { h, u } = field({ tier, elite, mod, skill: 1, row: 10, col: 5, others });
|
||
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
|
||
assert.deepEqual([sk.duration, sk.bb.value, sk.bb['sleach_s_2[cost].interval'], sk.rangeId, sk.bb.def, sk.bb.atk_to_hp_recovery_ratio], [15, 20, 0.75, 'x-1', elite ? 0.35 : 0.22, elite ? 0.35 : 0.22], `T${tier}`);
|
||
yak.hp = yak.s.maxHp * 0.8; texas.hp = texas.s.maxHp * 0.4; prove.hp = prove.s.maxHp * 0.1;
|
||
u.hp = u.s.maxHp * 0.05; // her own share never makes her the target
|
||
const iso = h.unit(5);
|
||
iso.hp = iso.s.maxHp * 0.05;
|
||
h.b.addBuff(iso, { key: 'test:isolated', flags: { isolated: true } });
|
||
h.b.addBuff(texas, { key: 'test:noheal', flags: { noHeal: true } });
|
||
h.b.players[0].dp = 0;
|
||
const defBefore = texas.s.def;
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||
const t0 = h.b.time;
|
||
assert.deepEqual(u.mem.sleachFlag && [u.mem.sleachFlag.r, u.mem.sleachFlag.c], [11, 4], `T${tier}: the flag on 德克萨斯's tile`);
|
||
h.run(0.3);
|
||
const b = texas.findBuff(`${S2_KEY}:${u.id}`);
|
||
assert.ok(b, `T${tier}: the flag effect on 德克萨斯`);
|
||
approx(b.mods.defPct, sk.bb.def, `T${tier}: DEF +${sk.bb.def * 100} %`);
|
||
approx(b.mods.hpRegen, u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: 生命回复速度 = ATK × ratio`);
|
||
approx(texas.s.def - defBefore, texas.base.def * sk.bb.def, `T${tier}: DEF applied (直接乘算)`, 1e-3);
|
||
const hp0 = texas.hp;
|
||
h.run(2);
|
||
assert.ok(texas.hp > hp0 + 1.5 * u.s.atk * sk.bb.atk_to_hp_recovery_ratio, `T${tier}: it regenerates through 禁疗 (${hp0} → ${texas.hp})`);
|
||
assert.equal(yak.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: only the operator on the flag's tile`);
|
||
assert.equal(prove.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the 10 % operator outside the x-1 is never chosen`);
|
||
// T1's 3×3 follows the flag: 德克萨斯 (on it) and 琴柳 ([−1, +1] of it) are inside, 角峰 ([−1, +2]) is not
|
||
const asp = talentOf(tier, elite, mod, 0).bb['sleach_t_1[ally].attack_speed'];
|
||
assert.equal(texas.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 德克萨斯 in the flag's area`);
|
||
assert.equal(u.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 琴柳 next to the flag`);
|
||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `T${tier}: 角峰 outside the flag's area`);
|
||
h.runUntil(() => !u.skill.active, 16);
|
||
approx(h.b.time - t0, 15, `T${tier}: 15 s`, 0.01);
|
||
approx(dpOf(h), 20, `T${tier}: 20 DP`);
|
||
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
|
||
assert.equal(texas.findBuff(`${S2_KEY}:${u.id}`), null, `T${tier}: the effect ends with the skill`);
|
||
h.run(0.5);
|
||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, asp, `T${tier}: 角峰 next to her again`);
|
||
done(h);
|
||
}
|
||
// nobody else on the x-1: the flag stays on her tile — she has DEF / 生命回复速度 there
|
||
const { h, u } = field({ tier: 6, elite: true, mod: BX, skill: 1 });
|
||
const sk = skillOf(6, true, S2);
|
||
u.skill.gainSp(999);
|
||
h.run(0.3);
|
||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 5], 'her own tile');
|
||
approx(u.findBuff(`${S2_KEY}:${u.id}`)?.mods.defPct ?? 0, sk.bb.def, 'on herself');
|
||
done(h);
|
||
});
|
||
|
||
test('S3 光辉旗帜: +10 DP at once; the flag on the ground tile of the enemy ranked first in the 2-1 (never a flyer): 停顿 + 脆弱 on its 3×3 at once, the strike 1 s later — atk_scale × ATK physical and 3 s 晕眩 to ground and air enemies of the 3×3; no ground enemy → her own tile', () => {
|
||
for (const [tier, elite, mod] of [[5, false, null], [6, true, BX]]) {
|
||
const sk = skillOf(tier, elite, S3);
|
||
const { h, u } = field({ tier, elite, mod, skill: 2, row: 10, col: 4 });
|
||
assert.deepEqual([sk.duration, sk.bb.cost, sk.bb.atk_scale, sk.bb.stun, sk.bb['debuff.damage_scale'], sk.rangeId], [10, 10, elite ? 2.4 : 2, 3, elite ? 0.25 : 0.2, '2-1'], `T${tier}`);
|
||
// in the 2-1 (facing RIGHT from 10,4): (10,6) [0,2] and (11,5) [1,1]; a taunt makes (10,6) the one her targeting ranks
|
||
// first (the highest 仇恨); its 3×3 (rows 9–11, cols 5–7) holds (11,5) and the flyer at (9,6), not (10,8)
|
||
const first = h.spawn('enemy_dummy', { pos: [10, 6] }), second = h.spawn('enemy_dummy', { pos: [11, 5] });
|
||
h.b.addBuff(first, { key: 'test:taunt', mods: { taunt: 1 } });
|
||
const fly = h.spawn('enemy_fly', { pos: [9, 6] });
|
||
const out = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||
h.b.players[0].dp = 0;
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||
const t0 = h.b.time;
|
||
approx(dpOf(h), 10, `T${tier}: +10 DP at once`);
|
||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 6], `T${tier}: the first-ranked ground enemy's tile`);
|
||
h.run(0.3);
|
||
const inArea = [first, second, fly];
|
||
for (const e of inArea) {
|
||
assert.ok(e.findBuff('sluggish'), `T${tier}: ${e.defId} 停顿 before the flag lands`);
|
||
approx(e.findBuff('fragile')?.data?.value ?? 0, sk.bb['debuff.damage_scale'], `T${tier}: ${e.defId} 脆弱`);
|
||
assert.equal(e.findBuff('stun'), null, `T${tier}: no 晕眩 yet`);
|
||
}
|
||
assert.equal(h.hooksOf('damaged').filter((c) => c.source === u).length, 0, `T${tier}: no damage before it lands`);
|
||
assert.ok(h.runUntil(() => h.hooksOf('damaged').some((c) => c.source === u), 2));
|
||
approx(h.b.time - t0, FLAG_FLIGHT, `T${tier}: it lands ${FLAG_FLIGHT} s after the throw`, 0.05);
|
||
const hits = h.hooksOf('damaged').filter((c) => c.source === u);
|
||
const struck = new Set(hits.map((c) => c.target));
|
||
for (const e of inArea) {
|
||
assert.ok(struck.has(e), `T${tier}: ${e.defId} struck (ground and air)`);
|
||
const c = hits.find((x) => x.target === e);
|
||
approx(c.amount, u.s.atk * sk.bb.atk_scale * (1 + sk.bb['debuff.damage_scale']), `T${tier}: ${sk.bb.atk_scale * 100} % ATK × the 脆弱`);
|
||
assert.deepEqual([c.type, c.dmg.isSkill], ['phys', true]);
|
||
const st = h.hooksOf('statusApplied').find((x) => x.source === u && x.status === 'stun' && x.target === e);
|
||
approx(st?.duration ?? 0, sk.bb.stun, `T${tier}: ${sk.bb.stun} s 晕眩`);
|
||
}
|
||
assert.ok(!struck.has(out) && !out.findBuff('sluggish'), `T${tier}: nothing outside the 3×3`);
|
||
h.runUntil(() => !u.skill.active, 11);
|
||
approx(h.b.time - t0, 10, `T${tier}: 10 s`, 0.01);
|
||
assert.equal(u.mem.sleachFlag, null, `T${tier}: the flag back`);
|
||
done(h);
|
||
}
|
||
// only a flyer in the 2-1, then nobody: her own tile
|
||
for (const pos of [[10, 6], null]) {
|
||
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 2, row: 10, col: 4 });
|
||
if (pos) h.spawn('enemy_fly', { pos });
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||
assert.deepEqual([u.mem.sleachFlag.r, u.mem.sleachFlag.c], [10, 4], pos ? 'a flyer is never the target' : 'no enemy: her tile');
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('T1 不退之旗 in every form: operators of the 3×3 around her ASPD +12 (BEA-X stage 3 +15) — 孤立 ones too, summons not —, enemies there (ground and air) ASPD −12 (−15); nothing outside, nothing once she left', () => {
|
||
for (const f of FORMS_ALL) {
|
||
const [tier, elite, mod] = f;
|
||
const tb = talentOf(tier, elite, mod, 0).bb;
|
||
const others = [{ uid: 2, chessId: YAK, row: 11, col: 6 }, { uid: 3, chessId: TEXAS, row: 9, col: 4 }, { uid: 4, chessId: PROVE, row: 10, col: 7 }];
|
||
const { h, u } = field({ tier, elite, mod, skill: 2, others });
|
||
const yak = h.unit(2), texas = h.unit(3), prove = h.unit(4);
|
||
h.b.addBuff(texas, { key: 'test:isolated', flags: { isolated: true } });
|
||
const tok = h.b.spawnToken('p1', 'token_10000_silent_healrb', 11, 4);
|
||
const e1 = h.spawn('enemy_dummy', { pos: [10, 6] }), e2 = h.spawn('enemy_fly', { pos: [9, 5] }), e3 = h.spawn('enemy_dummy', { pos: [10, 7] });
|
||
h.run(0.5);
|
||
const a = tb['sleach_t_1[ally].attack_speed'], en = tb['sleach_t_1[enemy].attack_speed'];
|
||
assert.deepEqual([a, en], tier === 6 && elite && mod === BX ? [15, -15] : [12, -12], label(f));
|
||
for (const x of [u, yak, texas]) assert.equal(x.findBuff(`${FLAG_ALLY}:${u.id}`)?.mods.aspd, a, `${label(f)}: ${x.defId} +${a}`);
|
||
assert.equal(prove.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: two tiles away`);
|
||
assert.ok(tok && !tok.findBuff(`${FLAG_ALLY}:${u.id}`), `${label(f)}: no summon`);
|
||
for (const e of [e1, e2]) assert.equal(e.findBuff(`${FLAG_ENEMY}:${u.id}`)?.mods.aspd, en, `${label(f)}: ${e.defId} ${en}`);
|
||
assert.equal(e3.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: the enemy two tiles away`);
|
||
approx(e1.s.aspd, 100 + en, `${label(f)}: enemy ASPD`);
|
||
h.b.retreat(u);
|
||
h.run(0.5);
|
||
assert.equal(yak.findBuff(`${FLAG_ALLY}:${u.id}`), null, `${label(f)}: gone with her`);
|
||
assert.equal(e1.findBuff(`${FLAG_ENEMY}:${u.id}`), null, `${label(f)}: gone with her (enemy)`);
|
||
done(h);
|
||
}
|
||
});
|
||
|
||
test('T2 精神感召: from her deployment the operators of her player off the field cost −2 until the next operator deployment; BEA-Y stage 3 adds +2 DP when that operator is 近战位 (once) — nothing at stage 1 or for a 远程位 one', () => {
|
||
for (const f of FORMS_ALL) {
|
||
const [tier, elite, mod] = f;
|
||
const bonus = tier === 6 && elite && mod === BY ? 2 : 0;
|
||
if (bonus) assert.deepEqual(modOf(6, BY).talentChanges.find((t) => t.talentIndex === -1).bb, { cost: 2 });
|
||
// she deploys last (col 7 after cols 3 / 4): no operator waits, so the effect stays; a knocked-out one gets the cut
|
||
for (const [chessId, melee] of [[TEXAS, true], [PROVE, false]]) {
|
||
const { h } = field({ tier, elite, mod, skill: 0, row: 10, col: 7, others: [{ uid: 2, chessId, row: 10, col: 3 }, { uid: 3, chessId: YAK, row: 11, col: 4 }] });
|
||
const op = h.unit(2), cost = op.base.cost;
|
||
h.b.kill(op);
|
||
assert.equal(op.base.cost, cost - 2, `${label(f)} ${chessId}: −2 while it waits`);
|
||
h.b.players[0].dp = 20;
|
||
assert.ok(h.b.redeploy(op, { free: false }), 'redeploys');
|
||
approx(dpOf(h), 20 - (cost - 2) + (melee ? bonus : 0), `${label(f)} ${chessId}: paid ${cost - 2}${melee && bonus ? ', +2 DP (近战位)' : ''}`);
|
||
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: the cost back once used`);
|
||
h.b.kill(op);
|
||
assert.equal(op.base.cost, cost, `${label(f)} ${chessId}: used up — no cut on the next knock-out`);
|
||
h.b.players[0].dp = 20;
|
||
h.b.redeploy(op, { free: false });
|
||
approx(dpOf(h), 20 - cost, `${label(f)} ${chessId}: and no second +DP`);
|
||
done(h);
|
||
}
|
||
}
|
||
// an operator deploying after her in the battle-start deployment uses it up at once (free: nothing saved); her leaving
|
||
// ends a pending one
|
||
const { h, u } = field({ tier: 6, elite: true, mod: BY, skill: 0, row: 10, col: 3, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }] });
|
||
const yk = h.unit(2), cost = yk.base.cost;
|
||
approx(dpOf(h), 10 + 2, 'BEA-Y stage 3: 角峰 (近战位) deployed right after her: +2 DP at the battle start');
|
||
h.b.kill(yk);
|
||
assert.equal(yk.base.cost, cost, 'used up by the battle-start deployment');
|
||
h.b.retreat(u);
|
||
h.b.redeploy(u, { free: true });
|
||
assert.equal(yk.base.cost, cost - 2, 'her redeploy: the waiting 角峰 −2 again');
|
||
h.b.retreat(u);
|
||
assert.equal(yk.base.cost, cost, 'her leaving ends it');
|
||
done(h);
|
||
});
|
||
|
||
test('BEA-X “牧人的歌”: while her skill runs the operator in front of her blocks +1 (stages 1 and 3), not before / after, not without the module; BEA-Y “友谊万岁”: 迷彩 while her skill runs', () => {
|
||
for (const f of FORMS_ALL) {
|
||
const [tier, elite, mod] = f;
|
||
const { h, u } = field({ tier, elite, mod, skill: 0, row: 10, col: 5, others: [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 5 }] });
|
||
const front = h.unit(2), side = h.unit(3);
|
||
const bx = elite && mod === BX, by = elite && mod === BY;
|
||
if (bx) assert.deepEqual(u.def.raw.trait.bb, { block_cnt: 1 }, label(f));
|
||
const base = front.s.blockCnt;
|
||
u.skill.gainSp(999);
|
||
assert.ok(h.runUntil(() => u.skill.active, 1));
|
||
h.run(0.3);
|
||
assert.equal(front.s.blockCnt, base + (bx ? 1 : 0), `${label(f)}: the operator in front`);
|
||
assert.equal(!!side.findBuff(`${FRONT_KEY}:${u.id}`), false, `${label(f)}: not the one beside her`);
|
||
assert.equal(!!u.s.flags.camou, by, `${label(f)}: 迷彩 while it runs`);
|
||
assert.equal(!!u.findBuff(CAMOU_KEY), by);
|
||
h.runUntil(() => !u.skill.active, 10);
|
||
h.step();
|
||
assert.equal(front.s.blockCnt, base, `${label(f)}: back after`);
|
||
assert.equal(!!u.s.flags.camou, false, `${label(f)}: no 迷彩 after`);
|
||
done(h);
|
||
}
|
||
});
|