Files
ark-stronghold-protocol/test/content/op_sleach.test.js
T
sganggs 806a047717 feedback6 R21P: every operator fights at full potential (潜能 6) — the owner's decision of 2026-10-07 (GitHub #252, PR #255; no host slider): tools/build-data.mjs OPERATOR_POTENTIAL adds every character_table potentialRanks attribute step (部署费用 / 再部署时间 / ATK / HP / DEF / RES / ASPD, all ADDITION, before rounding) to every chess (normal and elite), 补位 and 自选 unit form and picks talent / trait / module-talent candidates up to requiredPotentialRank 5 (「天赋效果增强」); a summon picks its talent candidates at its owner's potential (their requiredPotentialRank mirrors the owner's — PRTS 凯尔希: Mon3tr 3.5 s / 1400) and takes none of the owner's attribute steps; the 原型干员 stand-ins have no potential ranks (unchanged). 刺玫 413 / 17 → 435 / 15 as in the tournament video, 砾 18 → 16 s, 至简 80 → 70 s. data/chess.json (256 records), backups.json (the 71 owned picks, 8 summons), tokens.json (5 summons) and the data translations en / ja / ko / zh-TW rebuilt — only potential-driven fields (JSON-compared); the 干员调配 局内数值 and detail cards read the records. op-wang.js reads her stock cap 「最多拥有8(+1)枚」 through the potential step (the pattern fell back to 7: she sat at the cap from the start). Docs: DATA §2 / §18 / tokens, PLAYING (detail card), research 03, kits README, shared/diy.js / standIn.js, the 71 自选 kit headers and the kit comments that quoted potential-0 numbers. Tests: test/full-potential.test.js (new, fails on the parent); data.test.js re-derivation adds the potential steps (+ respawnTime); every test that hard-coded a potential-0 number carries the full-potential one (from the data and the test's own formula; no check loosened); golden: moves almost everywhere — roster 49/49, bonds 46/46, fields 22/22, matches 18/18, diy 133/138 (each battle parts on a stat: roster-001 / roster-020 at tick 1 on 角峰's HP (生命上限+250), bond-arcaneShip-high at 4.73 s and diy-001 at 3.70 s on an enemy's HP from a higher ATK; diy-110 / 111 also by 望's cap; the matches through those battles); unchanged: standins 10/10 and diy-135…138 (no potential ranks), diy-012 (twelve 夜莺 never hit, never redeployed)
2026-10-07 18:35:39 +08:00

313 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
});