Files
ark-stronghold-protocol/test/content/op_bgsnow.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

307 lines
19 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_bgsnow.test.js — the 自选 operator kit of 鸿雪 (char_4055_bgsnow, 6★ 重射手; kit
// server/sim/content/kits/ops/op-bgsnow.js) and of her summon “打字机” (token_10026_bgsnow_subbow), fielded the production
// way (a DIY slot + its `diy` pick, simdata getDiy; the typewriter as the placed hand piece of her player) in every form:
// tiers 5 / 6, normal (E2 Lv1, skill rank 4, no module) and elite (E2 Lv60, rank 7) with no module, ARC-Y “打字机色带” or
// ARC-X “仪式感” at stage 1 (tier 5) / 3 (tier 6). Numbers from data/backups.json; the fidelity checklist of kits/README.md.
// Run: node --test test/content/op_bgsnow.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 SNOW = 'char_4055_bgsnow';
const TOK = 'token_10026_bgsnow_subbow';
const FORMS = BACKUPS.units[SNOW].forms;
const TOKREC = BACKUPS.tokens[TOK];
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
const Y = 'uniequip_002_bgsnow', X = 'uniequip_003_bgsnow';
const S1 = 'skchr_bgsnow_1', S2 = 'skchr_bgsnow_2', S3 = 'skchr_bgsnow_3';
const statusOf = (tier, elite) => (elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0');
const formOf = (tier, elite) => FORMS[statusOf(tier, elite)];
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 typewriter's variant of a form, with the pick's skill / module (getDiyToken). */
const tokOf = (tier, elite, skill, mod) => {
let v = TOKREC.variants[`${SNOW}@${statusOf(tier, elite)}`];
if (v.bySkill?.[skill]) v = { ...v, ...v.bySkill[skill] };
if (mod && v.byModule?.[mod]) v = { ...v, ...v.byModule[mod] };
return v;
};
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' }) };
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, Y, X].map((m) => [t, true, m]))];
/** 鸿雪 as uid 1 at (10, 5) facing RIGHT; `tok` = the typewriter piece's tile (uid 2) or null. */
function field({ tier = 5, elite = false, mod = null, skill = 0, tok = null, tokFirst = false, seed = 5, others = [] } = {}) {
const op = { uid: 1, diy: { slot: SLOT[tier], charId: SNOW, skillIndex: skill, uniEquipId: mod }, elite, row: 10, col: 5 };
const piece = tok ? [{ uid: 2, kind: 'token', tokenId: TOK, ownerUid: 1, row: tok[0], col: tok[1] }] : [];
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 900, autoFinish: false, seed,
flags: { dpPerSec: 0, dpMax: 999 }, hooks: ['damaged', 'skillStart', 'skillEnd', 'statusApplied', 'death', 'deploy', 'attack', 'dodge'], captureNoisy: true,
units: tokFirst ? [...piece, op, ...others] : [op, ...piece, ...others],
});
h.step();
return { h, u: h.unit(1), t: h.b.allyUnits.find((a) => a.kind === 'token' && a.defId === TOK) ?? null };
}
const from = (h, src) => h.hooksOf('damaged').filter((c) => c.source === src);
function done(h) {
checkInvariants(h.b);
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
}
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
test('鸿雪 in every 自选 form: her kit, the form\'s stats + module attributes, 3-6, ranged physical, hits air, blocks 1, ground-targetable, the data triggers; offered as a pick', () => {
assert.equal(OPERATOR_KITS[SNOW], KITS[SNOW]);
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], [SNOW, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.respawnTime + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.canHitFly, u.profile.dmgType, u.base.bat], [1, 'ranged', true, 'phys', 1.6], `${label(f)}: 重射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-6`);
assert.equal(u.skill.rule, form.skills[skill].trigger.rule, `${label(f)}: the data's trigger`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'SKILL_RANGE', 'ACTIVE_RANGE']);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr, modOf(5, X).attr, modOf(6, X).attr],
[{ maxHp: 130, atk: 55 }, { maxHp: 170, atk: 75 }, { atk: 80, def: 10, respawnTime: -25 }, { atk: 120, def: 16, respawnTime: -25 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(SNOW) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(SNOW)));
assert.equal(validateDiyPicks({ [SLOT[5]]: { charId: SNOW, skillIndex: 1, uniEquipId: X } }, { data, kitted: KITTED_CHARS }).ok, true);
});
test('S1 抑扬格 (AUTO, attack SP 29 / 25, data DEFAULT): after that many attacks ATK +35 % / +50 % until she leaves the field, every attack meanwhile 30 % to deal ×145 % / ×160 %', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.spCost, sk.bb.atk, sk.bb.prob, sk.bb.atk_scale], elite ? [25, 0.5, 0.3, 1.6] : [29, 0.35, 0.3, 1.45], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 0, seed: 12 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.spType], ['toggle', 'DEFAULT', 'attack'], `T${tier}`);
h.spawn('enemy_dummy', { pos: [11, 6] });
assert.ok(h.runUntil(() => u.skill.active, 60), `T${tier}: cast`);
const before = from(h, u);
assert.equal(before.length, sk.spCost, `T${tier}: after ${sk.spCost} attacks`);
assert.ok(before.every((c) => Math.abs(c.amount - u.base.atk) < 1e-6), `T${tier}: plain before it`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
h.run(500);
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
const after = from(h, u).slice(before.length);
const atk = u.base.atk * (1 + sk.bb.atk);
for (const c of after) assert.ok([atk, atk * sk.bb.atk_scale].some((v) => Math.abs(c.amount - v) < 1e-6), `T${tier}: ${c.amount}`);
const r = after.filter((c) => c.amount > atk + 1e-6).length / after.length;
assert.ok(Math.abs(r - sk.bb.prob) < 0.06, `T${tier}: ${r}`);
h.b.retreat(u);
assert.ok(!u.skill.active, `T${tier}: ends when she leaves`);
done(h);
}
});
test('S2 点题 (MANUAL, 2 charges, data SKILL_RANGE on 3-1): an enemy 3 tiles ahead (3-1, not 3-6) casts it and is struck at once: atk_scale × ATK as a 普通伤害 attack, then twice as 持续伤害 (not dodgeable); her card range stays 3-6', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb.atk_scale, sk.bb.respawn_time, sk.maxChargeTime, sk.spCost, sk.initSp], elite ? [2, 0.6, 2, 10, 6] : [1.8, 0.65, 2, 11, 3], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 1, seed: 3 });
assert.deepEqual([u.skill.kind, u.skill.rule, u.skill.maxCharges], ['charges', 'SKILL_RANGE', 2], `T${tier}`);
u.skill.gainSp(999);
h.run(1);
assert.equal(u.skill.activations, 0, `T${tier}: nobody in range`);
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), `T${tier}: cast for the enemy on 3-1`);
const tc = h.b.time;
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: the card keeps 3-6`);
h.run(0.5);
const atk = h.hooksOf('attack').filter((c) => c.attacker === u);
approx(atk[0].t, tc, `T${tier}: 立即`, 0.04);
assert.equal(atk[0].isSkill, true);
const dodged = h.hooksOf('dodge').filter((c) => c.source === u);
const hits = from(h, u);
assert.equal(dodged.length, 1, `T${tier}: the 普通伤害 hit can be dodged (a dodge enemy)`);
assert.equal(hits.length, 2, `T${tier}: the two 持续伤害 hits cannot`);
for (const c of hits) {
approx(c.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ×${sk.bb.atk_scale}`);
assert.deepEqual([c.dmg.isAttack, c.dmg.canDodge, c.dmg.tags.includes('dot'), c.type], [false, false, true, 'phys']);
}
assert.equal(u.skill.charges, 1, `T${tier}: one of two charges spent`);
h.b.removeBuff(e, 'test:dodge');
const n0 = from(h, u).length;
h.run(3.5);
const second = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.deepEqual(second.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: the second charge 3 s later: 普通 then 2 × 持续`);
done(h);
}
});
test('S3 锐笔速写 (MANUAL, data ACTIVE_RANGE on 3-1): an enemy only on [0,3] casts it; range 3-1 (and back), interval 1.6 − 0.6 s, ×155 % / 170 % off her line, ×185 % / 215 % on the 正前方3格', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.duration, sk.bb.base_attack_time, sk.bb.atk_scale, sk.bb['bgsnow_s_3[atk_up].atk_scale'], sk.rangeId], elite ? [27, -0.6, 1.7, 2.15, '3-1'] : [26, -0.6, 1.55, 1.85, '3-1'], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 2, seed: 7 });
assert.deepEqual([u.skill.kind, u.skill.rule], ['duration', 'ACTIVE_RANGE']);
u.skill.gainSp(999);
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: cast for [0,3] (outside 3-6)`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 3-1 while it runs`);
approx(u.s.interval, 1.0, `T${tier}: interval`);
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
h.b.kill(far, null);
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
h.run(8);
const onFront = from(h, u).filter((c) => c.dmg.isAttack && c.target === front);
h.b.kill(front, null);
h.run(4);
const onSide2 = from(h, u).filter((c) => c.dmg.isAttack && c.target === side);
assert.ok(onFront.length > 3 && onSide2.length > 1, `T${tier}: both struck (${onFront.length} / ${onSide2.length})`);
for (const c of onFront) approx(c.amount, u.base.atk * sk.bb['bgsnow_s_3[atk_up].atk_scale'], `T${tier}: 正前方3格`);
for (const c of onSide2) approx(c.amount, u.base.atk * sk.bb.atk_scale, `T${tier}: off the line`);
u.skill.extend(-999);
h.step(2);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: back to 3-6`);
approx(u.s.interval, 1.6, `T${tier}: interval back`);
done(h);
}
});
test('ARC-Y 打字机色带 trait: ×105 % and no dodge on her straight line ahead (stages 1 and 3) — 点题\'s 持续伤害 hits too; off the line it dodges; ARC-X / no module nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y = elite && mod === Y;
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 3 });
u.skill.charges = 0; u.skill.sp = 0;
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
const front = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_dummy', { pos: [11, 7] });
for (const e of [front, side]) h.b.addBuff(e, { key: 'test:dodge', mods: { dodgePhys: 1 } });
h.run(12);
const onFront = from(h, u).filter((c) => c.target === front);
if (y) {
assert.ok(onFront.length > 4, `${label(f)}: the front enemy is hit`);
for (const c of onFront) approx(c.amount, u.s.atk * 1.05, `${label(f)}: ×1.05`);
// 点题 on the front enemy: all three hits ×1.05
h.b.removeBuff(u, 'test:noSp');
u.skill.addCharge(1);
const n0 = from(h, u).length;
h.run(0.5);
const s2 = from(h, u).slice(n0).filter((c) => c.dmg.isSkill);
assert.equal(s2.length, 3, `${label(f)}: 点题's three hits land`);
for (const c of s2) approx(c.amount, u.s.atk * skillOf(tier, true, S2).bb.atk_scale * 1.05, `${label(f)}: 点题 ×1.05`);
h.b.kill(front, null);
h.run(6);
assert.ok(h.hooksOf('dodge').filter((c) => c.source === u && c.target === side).length > 2, `${label(f)}: off the line it dodges`);
} else {
assert.equal(onFront.length, 0, `${label(f)}: every attack dodged`);
}
done(h);
}
});
test('“打字机”: her placed piece runs its kit (not the generic one) — the variant\'s stats (ARC-Y stage 3 stronger), untargetable, 3-2, blocks 0, hits air; deploys with the board, leaves after 25 s, back on its tile after 40 s (S2: ×65 % / ×60 %) for 5 DP, never while 鸿雪 is down', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, elite ? mod : null);
const { h, t } = field({ tier, elite, mod, skill, tok: [12, 3], tokFirst: skill === 1 });
assert.ok(t && t.alive && t.deployed, `${label(f)} S${skill + 1}: deployed with the board`);
assert.deepEqual([!!t.kit.generic, t.skill.id, t.base.atk, t.base.maxHp, t.base.respawnTime, t.base.cost], [false, v.skill.skillId, v.stats.atk, v.stats.maxHp, 40, 5], `${label(f)} S${skill + 1}`);
assert.deepEqual([!!t.s.flags.untargetable, t.s.blockCnt, t.profile.canHitFly, t.profile.attack, t.liveRangeGrid], [true, 0, true, 'ranged', [[0, 0], [0, 1], [0, 2], [0, 3]]], `${label(f)} S${skill + 1}: 不会受到攻击`);
done(h);
}
}
assert.deepEqual([tokOf(5, false, 0).stats.atk, tokOf(5, true, 0).stats.atk, tokOf(6, true, 0, Y).stats.atk, tokOf(5, true, 0, Y).stats.atk], [666, 799, 869, 799]);
// life and redeploys (S1: 40 s; S2: ×respawn_time)
for (const [tier, elite, skill, mul] of [[5, false, 0, 1], [5, false, 1, 0.65], [6, true, 1, 0.6]]) {
const { h, u, t } = field({ tier, elite, skill, tok: [12, 3] });
h.b.players[0].dp = 20;
assert.ok(h.runUntil(() => !t.alive, 26));
approx(h.b.time, 25, `T${tier} S${skill + 1}: 25 s`, 0.05);
assert.equal(h.hooksOf('death').find((c) => c.unit === t).reason, 'expired');
assert.ok(h.runUntil(() => t.alive, 60));
approx(h.b.time, 25 + 40 * mul, `T${tier} S${skill + 1}: back after ${40 * mul} s`, 0.3);
assert.deepEqual([t.tileR, t.tileC, h.b.players[0].dp], [12, 3, 15], `T${tier} S${skill + 1}: on its tile, 5 DP`);
assert.ok(u.alive);
done(h);
}
// 鸿雪 down: the typewriter waits for her
const { h, u, t } = field({ tier: 5, elite: false, skill: 0, tok: [12, 3] });
h.b.players[0].dp = 0;
h.run(10);
h.b.kill(u, null);
h.run(60);
assert.ok(!t.alive, 'not while 鸿雪 is down');
h.b.players[0].dp = 25;
assert.ok(h.runUntil(() => u.alive, 30), '鸿雪 back');
assert.ok(h.runUntil(() => t.alive, 1), 'then the typewriter');
done(h);
});
test('“打字机”\'s skill is the copy of hers, free once per deployment: S1 ATK +35 % / +50 % for good with the 30 % proc, S2 three hits of ×180 % / ×200 % (普通 + 2 × 持续), S3 4-1 + interval −0.6 s + ×185 % / ×215 % for 16 / 17 s', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
for (const skill of [0, 1, 2]) {
const v = tokOf(tier, elite, skill, null);
const tb = v.skill.bb;
const { h, t } = field({ tier, elite, skill, tok: [11, 5], seed: 4 });
h.spawn('enemy_dummy', { pos: [11, 7] });
assert.deepEqual([t.skill.rule, t.skill.spCost, t.skill.charges], ['DEFAULT', 0, 1], `T${tier} S${skill + 1}: free, ready`);
assert.ok(h.runUntil(() => t.skill.activations === 1, 1), `T${tier} S${skill + 1}: cast at its first attack`);
if (skill === 0) {
approx(t.s.atk, t.base.atk * (1 + tb.atk), `T${tier}: S1 copy ATK`);
h.run(20);
assert.ok(t.skill.active, `T${tier}: S1 copy stays`);
const hits = from(h, t);
const atk = t.base.atk * (1 + tb.atk);
assert.ok(hits.some((c) => Math.abs(c.amount - atk * tb.atk_scale) < 1e-6) && hits.some((c) => Math.abs(c.amount - atk) < 1e-6), `T${tier}: S1 copy procs ×${tb.atk_scale}`);
} else if (skill === 1) {
h.run(0.5);
const s2 = from(h, t).filter((c) => c.dmg.isSkill);
assert.deepEqual(s2.map((c) => [c.dmg.isAttack, c.dmg.tags.includes('dot')]), [[true, false], [false, true], [false, true]], `T${tier}: S2 copy`);
for (const c of s2) approx(c.amount, t.base.atk * tb.atk_scale, `T${tier}: S2 copy ×${tb.atk_scale}`);
} else {
assert.deepEqual(t.liveRangeGrid, [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]], `T${tier}: S3 copy 4-1`);
approx(t.s.interval, 1.0, `T${tier}: S3 copy interval`);
approx(t.skill.timeLeft, v.skill.duration, `T${tier}: ${v.skill.duration} s`, 0.05);
h.run(3);
for (const c of from(h, t).filter((x) => x.dmg.isSkill)) approx(c.amount, t.base.atk * tb['attack@atk_scale'], `T${tier}: S3 copy ×${tb['attack@atk_scale']}`);
}
h.run(24);
assert.equal(t.skill.activations, 1, `T${tier} S${skill + 1}: once per deployment`);
done(h);
}
}
});
test('弱点速记: every damage of the typewriter cuts the target\'s DEF 20 % for 5 s, 25 % on one of the 4 tiles next to 鸿雪 (not diagonal); ARC-X stage 3: 30 % / 35 % and the 8 tiles; 鸿雪 down ⇒ the base cut', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const x3 = tier === 6 && elite && mod === X;
const tal = tokOf(tier, elite, 2, elite ? mod : null).talents.find((x) => x && x.bb.duration);
const c1 = -tal.bb['bgsnow_token[def_down]_1.def'], c2 = -tal.bb['bgsnow_token[def_down]_2.def'];
assert.deepEqual([c1, c2, tal.bb.duration], x3 ? [0.3, 0.35, 5] : [0.2, 0.25, 5], label(f));
for (const [tile, near] of [[[11, 5], true], [[11, 4], x3], [[12, 3], false]]) {
const { h, u, t } = field({ tier, elite, mod, skill: 0, tok: tile, seed: 2 });
h.spawn('enemy_dummy', { pos: [tile[0], tile[1] + 2] });
h.run(4);
const cuts = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown');
assert.ok(cuts.length >= 2, `${label(f)} ${tile}: cuts`);
for (const c of cuts) assert.deepEqual([c.value, c.duration], [near ? c2 : c1, 5], `${label(f)} ${tile}: ${near ? 'near' : 'apart'}`);
if (near) {
h.b.kill(u, null);
h.run(3);
const after = h.hooksOf('statusApplied').filter((c) => c.source === t && c.status === 'defDown').slice(cuts.length);
assert.ok(after.length && after.every((c) => c.value === c1), `${label(f)}: 鸿雪 down ⇒ ${c1}`);
}
done(h);
}
}
});