feedback1 final: six 重装 skills cast with an enemy in range, a deliberate deviation from the official TAKE_DAMAGE row (GitHub issue #4, PR #12)
The owner's decision (2026-10-03, after community feedback): 深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令 and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击 take the basic strategy (DEFAULT) instead of the 下半 TANK class row's TAKE_DAMAGE. Every other MANUAL 重装 skill, 深巡 S1 included, still waits for a hit. - tools/build-data.mjs TRIGGER_DEVIATIONS: a small table keyed by the normal chess id and the skill id (the elite follows through baseId), never by the skill id alone - 灰毫 S1 is the generic skcom_atk_up[3]. resolveTrigger applies it last; rawRule keeps the official TAKE_DAMAGE; validateAll fails the build when an entry no longer meets a TAKE_DAMAGE row. data/chess.json rebuilt with --offline: exactly the 12 records' rule changes. - PR #12's kit lines (trigger 'DEFAULT' on 深巡 / 雷蛇 S2) stay and agree with the data; their comments and tests now give this reason instead of the community summary. - Tests: test/sim/feedback1-tank-triggers.test.js (the data, kit = data, a real battle on 战场#01(下半) in which each of the six casts with an enemy in range before any hit and each of the other 46 MANUAL 重装 records casts on the first hit); data.test.js trigger expectations; the 灰毫 / 号角 kit tests follow; docs-consistency. - Docs: DESIGN §21.29 (new), §5.6, §19.2, §20.2, §21.20 (settled row), §21.23; DATA, SIM, PLAYING, CHANGELOG 0.1.1; research 03 §1.4 drops the arknights.wiki.gg "offensive / defensive" summary and records the activity_table history (上半 had no TANK row, 下半 added it) and this deviation; research 00-INDEX.
This commit is contained in:
1 parent
e6ab4e1ff8
commit
2dba6da598
20 files changed
+325
-58
No files matched your search
@@ -673,7 +673,8 @@ test('1_20 雷蛇 反击电弧: attack interval +70 % of the base (PRTS 增大(+
|
||||
for (const id of pair('20')) {
|
||||
const bb = raw(id).skill.bb;
|
||||
assert.equal(raw(id).skill.skillId, 'skchr_liskam_2');
|
||||
// 重装 S2 反击电弧 is an OFFENSIVE skill: the basic strategy casts it with an enemy in the skill range (issue #4) —
|
||||
// 重装 S2 反击电弧 is an OFFENSIVE skill: the basic strategy casts it with an enemy in the skill range (issue #4; the
|
||||
// deliberate deviation from the official 重装 row, DESIGN §21.29) —
|
||||
// the enemy here stands two tiles ahead and, being a speed-0 dummy, never touches her
|
||||
const h = run({ defs: { enemies: { e: dummy('e', { atk: 300, bat: 1 }) }, chess: noGarrison(id) }, units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, 6] }] });
|
||||
const u = h.unit(id);
|
||||
|
||||
@@ -456,7 +456,7 @@ test('2_17 折桠 S1 绝境抵抗: TAKE_DAMAGE; DEF +def and 抵抗 (control sta
|
||||
}
|
||||
});
|
||||
|
||||
test('2_18 灰毫 S2 专注轰击: cast when hit (重装 TAKE_DAMAGE), then blocks nothing, ranged splash bombs only, shorter interval, ATK +atk', () => {
|
||||
test('2_18 灰毫 S2 专注轰击: cast with an enemy in range (DEFAULT, the deliberate deviation from the 重装 row — DESIGN §21.29), then blocks nothing, ranged splash bombs only, shorter interval, ATK +atk', () => {
|
||||
for (const id of both('chess_char_2_18')) {
|
||||
const bb = bbAlt(id);
|
||||
const enemies = { w: enemyRec({ key: 'w', hp: 1e7, speed: 2, atk: 50, bat: 1 }) };
|
||||
@@ -465,15 +465,18 @@ test('2_18 灰毫 S2 专注轰击: cast when hit (重装 TAKE_DAMAGE), then bloc
|
||||
assert.ok(c.runUntil(() => c.enemies()[0]?.blockedBy === c.unit(id), 10), 'default: blocked');
|
||||
done(c);
|
||||
|
||||
// (a second walker comes by while the skill runs: she bombs it from range, unblocked)
|
||||
const h = run({ defs: { enemies }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'w', route: 0 }, { key: 'w', route: 0, time: 5 }] });
|
||||
// (a second walker, 2 s behind the first, comes by while the skill runs — 10 s from the cast, made as the first one
|
||||
// comes into range: she bombs it from range, unblocked)
|
||||
const h = run({ defs: { enemies }, units: [U(id, 9, 5, { carryState: READY })], enemies: [{ key: 'w', route: 0 }, { key: 'w', route: 0, time: 2 }] });
|
||||
const u = h.unit(id);
|
||||
usesAlt(u, id);
|
||||
assert.equal(u.skill.rule, 'TAKE_DAMAGE', 'a MANUAL 重装 skill: "不受技能范围影响,受到伤害时释放技能"');
|
||||
// the official 下半 重装 row (TAKE_DAMAGE) is overridden for this skill by the owner's decision (data rawRule keeps it)
|
||||
assert.equal(u.skill.rule, 'DEFAULT', 'an offensive 重装 skill of the deviation: the basic strategy');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 8));
|
||||
const st = h.b.time;
|
||||
const atk0 = h.eventsOf('atk').length;
|
||||
assert.ok(h.hooksOf('damaged').some((x) => x.target === u), 'cast by the hit of the enemy she blocked');
|
||||
assert.equal(started(h, u)[0].reason, 'DEFAULT', 'cast by the basic strategy');
|
||||
assert.ok(!h.hooksOf('damaged').some((x) => x.target === u), 'with the walker in range, before anything hit her');
|
||||
assert.ok(u.s.flags.noBlock);
|
||||
approx(u.s.atk, u.base.atk * (1 + u.findBuff('talent:ashlok').mods.atkPct + bb.atk), 'ATK (+ 炮术研习)');
|
||||
approx(u.s.interval, (u.base.bat + bb.base_attack_time) * 100 / u.s.aspd, 'base attack time shortened');
|
||||
|
||||
@@ -527,18 +527,19 @@ test('号角 S1 照明榴弹 (自动触发 ⇒ DEFAULT): the next ranged shot de
|
||||
}
|
||||
});
|
||||
|
||||
test('号角 S2 暴风号令: 10 rounds of attack@s2.atk_scale × ATK splash; the second half adds magic_atk_scale × ATK arts (过载)', () => {
|
||||
test('号角 S2 暴风号令: cast with an enemy in range (DEFAULT, DESIGN §21.29); 10 rounds of attack@s2.atk_scale × ATK splash; the second half adds magic_atk_scale × ATK arts (过载)', () => {
|
||||
for (const id of pair('08')) {
|
||||
const h = run({ defs: { enemies: { enemy_dummy: dummy('enemy_dummy') } }, units: [entry(id, 'skchr_horn_2', { row: 10, col: 2 })], enemies: [{ key: 'enemy_dummy', pos: [10, 6] }] });
|
||||
const u = sel(h, id, 'skchr_horn_2');
|
||||
const bb = bbOf(u);
|
||||
const e = h.b.enemies[0];
|
||||
h.step();
|
||||
// the owner's deliberate deviation from the 重装 TAKE_DAMAGE row (DESIGN §21.29): cast with an enemy in range
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
u.skill.gainSp(1000);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, '重装 TAKE_DAMAGE: not before a hit');
|
||||
h.b.dealDamage(e, u, { amount: 10, type: 'phys' });
|
||||
assert.equal(u.skill.activations, 1, 'cast by the hit');
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 1, 3), 'cast with the enemy in range');
|
||||
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.ok(!h.hooksOf('damaged').some((c) => c.target === u), 'no hit needed');
|
||||
assert.equal(u.skill.kind, 'ammo');
|
||||
assert.equal(u.skill.ammoLeft + h.hooksOf('ammoUsed').filter((c) => c.unit === u).length, bb['attack@s2.trigger_time']);
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 60));
|
||||
|
||||
@@ -132,7 +132,8 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
|
||||
});
|
||||
const u = h.unit(id);
|
||||
h.step();
|
||||
// 技能策略 (issue #4): an enemy inside her initial 2-2 range is enough — the basic strategy casts it, no hit required
|
||||
// 技能策略 (issue #4; the deliberate deviation of DESIGN §21.29): an enemy inside her initial 2-2 range is enough — the
|
||||
// basic strategy casts it, no hit required
|
||||
assert.equal(u.skill.rule, 'DEFAULT', '行动能力剥夺 is an offensive ranged skill, not a 重装 TAKE_DAMAGE one');
|
||||
h.run(3.5);
|
||||
assert.ok(u.skill.active);
|
||||
@@ -150,9 +151,9 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
|
||||
});
|
||||
|
||||
test('1_04 深巡 / 1_20 雷蛇 S2 技能策略: both 哨戒铁卫 S2s cast with an enemy in range, no hit needed (issue #4)', () => {
|
||||
// both S2s are offensive (深巡: range up + ATK/ASPD + piercing darts; 雷蛇: ATK +125 % + arts on 3 + stun), and the
|
||||
// mode's rule is "offensive skills activate when an enemy is in their skill range". The 重装 TAKE_DAMAGE row is
|
||||
// documented for skillIndex 0 only — reading it as a wildcard made both wait for a hit.
|
||||
// both S2s are offensive (深巡: range up + ATK/ASPD + piercing darts; 雷蛇: ATK +125 % + arts on 3 + stun); the official
|
||||
// 下半 重装 row (TAKE_DAMAGE for every MANUAL 重装 skill) made both wait for a hit, and the owner's deliberate deviation
|
||||
// from it (DESIGN §21.29, after community feedback) gives them the basic strategy — the data and the kit say DEFAULT.
|
||||
for (const id of ['chess_char_1_04_a', 'chess_char_1_04_b', 'chess_char_1_20_a', 'chess_char_1_20_b']) {
|
||||
const h = run({ units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, 6] }] });
|
||||
const u = h.unit(id);
|
||||
@@ -1116,7 +1117,7 @@ test('2_17 折桠: 生存决心 trembles ground enemies around, ATK/DEF up, hits
|
||||
done(h2);
|
||||
});
|
||||
|
||||
test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk); TAKE_DAMAGE skill ATK +atk; elite ×1.1 vs blocked', () => {
|
||||
test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk); skill ATK +atk cast with an enemy in range (DEFAULT, DESIGN §21.29); elite ×1.1 vs blocked', () => {
|
||||
const id = 'chess_char_2_18_a', bb = bbOf(id), t = tal(id);
|
||||
const h = run({ units: [{ chessId: id, row: 10, col: 5 }, { chessId: 'chess_char_2_18_b', row: 10, col: 3 }] });
|
||||
h.step();
|
||||
@@ -1126,7 +1127,11 @@ test('2_18 灰毫: 炮术研习 ATK +atk (ground surroundings: ashlok_t_1.atk);
|
||||
const h2 = run({ defs: { enemies: { e: dummy('e', { atk: 400 }) } }, units: [{ chessId: id, row: 10, col: 5, carryState: READY }], enemies: [{ key: 'e', pos: [10, 5] }] });
|
||||
const u = h2.unit(id);
|
||||
h2.runUntil(() => u.skill.active, 5);
|
||||
assert.equal(h2.hooksOf('skillStart').find((c) => c.unit === u).reason, 'TAKE_DAMAGE');
|
||||
// the owner's deliberate deviation from the 重装 TAKE_DAMAGE row: the basic strategy, no hit needed — she casts before the
|
||||
// enemy she holds has hit her
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
assert.equal(h2.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.ok(!h2.hooksOf('damaged').some((c) => c.target === u), 'not hit before the cast');
|
||||
approx(u.s.atk, u.base.atk * (1 + t['ashlok_t_1.atk'] + bb.atk));
|
||||
done(h2);
|
||||
const idb = 'chess_char_2_18_b', tb = tbOf(idb);
|
||||
|
||||
@@ -364,7 +364,7 @@ test('史尔特尔 S3 黄昏: full heal, max HP +5000, ATK +210 %, range +2, 3 t
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
test('号角 S3: ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss; T1 Defenders ATK +20 %; T2 血战', () => {
|
||||
test('号角 S3: cast with an enemy in range (DEFAULT, DESIGN §21.29); ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss; T1 Defenders ATK +20 %; T2 血战', () => {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_dummy: dummy() }, chess: { t_tank: ally('t_tank', { profession: 'TANK', stats: { atk: 100 } }) } },
|
||||
units: [{ chessId: 'chess_char_5_08_a', row: 9, col: 3 }, { chessId: 't_tank', row: 12, col: 3 }],
|
||||
@@ -376,10 +376,12 @@ test('号角 S3: ATK +25 %, BAT −1.2 s, then overload ATK +50 % with HP loss;
|
||||
approx(h.unit('t_tank').s.atk, 100 * (1 + t0.atk), 1e-6, '军事要塞 on another Defender');
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk), 1e-6, '… and on herself');
|
||||
u.skill.gainSp(1000);
|
||||
h.run(1);
|
||||
assert.equal(u.skill.activations, 0, '重装 TAKE_DAMAGE: not before a hit');
|
||||
h.b.dealDamage(h.enemies()[0], u, { amount: 10, type: 'phys' });
|
||||
// cast at her next attack with the enemy in range, no hit needed: DEFAULT — the owner's deliberate deviation from the
|
||||
// 重装 TAKE_DAMAGE row (DESIGN §21.29)
|
||||
assert.equal(u.skill.rule, 'DEFAULT');
|
||||
assert.ok(h.runUntil(() => u.skill.active, 5));
|
||||
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT');
|
||||
assert.equal(u.stats.taken, 0, 'nothing hit her');
|
||||
const start = h.b.time;
|
||||
const total = u.def.skill.duration, ov = bb['horn_s_3[overload_start].damage_duration'];
|
||||
approx(u.s.atk, u.base.atk * (1 + t0.atk + bb.atk));
|
||||
|
||||
@@ -469,6 +469,28 @@ test('chess: skills[] = every skill unlocked at the status, at the chess skill l
|
||||
assert.deepEqual(chess.chess_char_1_08_a.skills[1].trigger.customRangeGrid, chess.chess_char_1_08_a.skills[1].rangeGrid);
|
||||
assert.deepEqual(rules('chess_char_4_22_a'), ['DEFAULT', 'DEFAULT', 'DEFAULT']); // 银灰: "攻击范围缩小 / 扩大" = attack range
|
||||
assert.deepEqual(rules('chess_char_3_18_a'), ['DEFAULT', 'SKILL_RANGE', 'DEFAULT']); // 忍冬: S2 对周围…, S3 攻击距离+1
|
||||
// the deliberate deviation from the 重装 row (DESIGN §21.29, the owner's decision; tools/build-data.mjs
|
||||
// TRIGGER_DEVIATIONS, per chess): six skills cast with an enemy in range, rawRule keeps the official TAKE_DAMAGE
|
||||
const raws = (id) => chess[id].skills.map((s) => s.trigger.rawRule);
|
||||
for (const id of ['chess_char_1_04_a', 'chess_char_1_04_b']) { // 深巡: S1 keeps the row, S2 deviates
|
||||
assert.deepEqual(rules(id), ['TAKE_DAMAGE', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_1_20_a', 'chess_char_1_20_b']) { // 雷蛇: S1 AUTO, S2 deviates
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['DEFAULT', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_5_08_a', 'chess_char_5_08_b']) { // 号角: S1 AUTO, S2 / S3 deviate
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['DEFAULT', 'TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
for (const id of ['chess_char_2_18_a', 'chess_char_2_18_b']) { // 灰毫: S1 (generic skcom_atk_up[3]) and S2
|
||||
assert.deepEqual(rules(id), ['DEFAULT', 'DEFAULT']);
|
||||
assert.deepEqual(raws(id), ['TAKE_DAMAGE', 'TAKE_DAMAGE']);
|
||||
}
|
||||
const deviated = Object.values(chess).flatMap((c) => (c.skills || []).filter((s) => s.trigger.rawRule === 'TAKE_DAMAGE' && s.trigger.rule !== 'TAKE_DAMAGE').map((s) => `${c.baseId} ${s.skillId}`));
|
||||
assert.equal(deviated.length, 12, 'exactly the six skills, normal + elite');
|
||||
assert.deepEqual([...new Set(deviated)].sort(), ['chess_char_1_04_a skchr_udflow_2', 'chess_char_1_20_a skchr_liskam_2', 'chess_char_2_18_a skchr_ashlok_2', 'chess_char_2_18_a skcom_atk_up[3]', 'chess_char_5_08_a skchr_horn_2', 'chess_char_5_08_a skchr_horn_3']);
|
||||
for (const c of Object.values(chess)) {
|
||||
for (const s of c.skills || []) {
|
||||
if (s.skillType !== 'MANUAL') assert.ok(!['TAKE_DAMAGE', 'SEARCH', 'SKILL_RANGE'].includes(s.trigger.rule), `${c.chessId} ${s.skillId}: an AUTO / PASSIVE skill takes no strategy row`);
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
// 999 layer cap (one implementation) and 限伤 300000, the 假想敌:胄 kit — and the normative lines they rewrote; player
|
||||
// feedback after 0.1.0 (DESIGN §21, v0.1.1) including batch 6 (§21.21–§21.25: 坚固维式重锤 once per deployment, 起飞,
|
||||
// fenced tiles, knocked-out bodies, the dispatcher snapshot and the manifest shrink guard credited to PR #2 / PR #7) and
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2).
|
||||
// the 突变细胞 bench rule (§21.1: the carrier destroyed, its new operator gained into the 整备区 — official footage, PR #2),
|
||||
// the closing additions §21.26–§21.28 (GitHub issues #1 / #5 / #8) and the owner's deliberate trigger deviation for six
|
||||
// 重装 skills (§21.29, GitHub issue #4 / PR #12).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -791,3 +793,35 @@ test('突变细胞 after the WA merge (DESIGN §21.1): the carrier is destroyed,
|
||||
assert.match(PS, /const np = this\.acquireChess\(newId, \{ source: 'transform' \}\);/);
|
||||
assert.ok(!/fromKey/.test(doc('server/match/audit.js')), 'audit.js: no exception for a transformed carrier\'s tile');
|
||||
});
|
||||
|
||||
test('the deliberate trigger deviation (DESIGN §21.29): six 重装 skills DEFAULT in the builder, the data and every doc; research 03 keeps the history, not the wiki.gg summary', () => {
|
||||
const sub = DESIGN.slice(DESIGN.indexOf('### 21.29 '));
|
||||
for (const re of [/深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令 and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击/, /反馈的人太多了/, /public issue #4/, /PR #12/, /never by the skill id alone/, /`rawRule` keeps the official row/]) assert.match(sub, re);
|
||||
// the normative lines and the settled decision
|
||||
assert.match(DESIGN, /every MANUAL 重装 skill, §21\.12 — but the six of the deliberate deviation, §21\.29/);
|
||||
assert.match(DESIGN, /\*\*partly reverted by §21\.29\*\*/);
|
||||
assert.match(DESIGN, /\| Six 重装 skills' trigger \(§21\.29\) \|/);
|
||||
// the builder's table is per chess (灰毫 S1 is the generic skcom_atk_up[3]) and the data follows it
|
||||
const bd = doc('tools/build-data.mjs');
|
||||
assert.match(bd, /const TRIGGER_DEVIATIONS = Object\.freeze\(\{/);
|
||||
for (const k of ["chess_char_1_04_a: { skchr_udflow_2: 'DEFAULT' }", "chess_char_1_20_a: { skchr_liskam_2: 'DEFAULT' }", "chess_char_2_18_a: { 'skcom_atk_up[3]': 'DEFAULT', skchr_ashlok_2: 'DEFAULT' }", "chess_char_5_08_a: { skchr_horn_2: 'DEFAULT', skchr_horn_3: 'DEFAULT' }"]) assert.ok(bd.includes(k), k);
|
||||
for (const id of ['chess_char_1_04_b', 'chess_char_2_18_b', 'chess_char_5_08_b']) {
|
||||
for (const s of DATA.chess[id].skills.filter((x) => x.trigger.rawRule === 'TAKE_DAMAGE' && x.trigger.rule === 'DEFAULT')) assert.ok(bd.includes(s.skillId), `${id} ${s.skillId}`);
|
||||
}
|
||||
// research 03 §1.4: the community summary is gone (promised on PR #12); the activity_table history and the deviation stay
|
||||
const r03 = doc('docs/research/03-operators.md');
|
||||
assert.ok(!/arknights\.wiki\.gg/.test(r03), 'research 03: no wiki.gg summary');
|
||||
assert.ok(!/Offensive skills activate when an enemy/.test(r03));
|
||||
assert.match(r03, /上半 \(act1autochess, 2025-11\) shipped no TANK row/);
|
||||
assert.match(r03, /下半 \(act2autochess, 2026-03-14\) added `TANK \| \| \| 0 \| TAKE_DAMAGE` for every skill index/);
|
||||
assert.match(r03, /\*\*Deliberate deviation\*\* \(the owner, 2026-10-03/);
|
||||
// PR #12's kit lines stay; their comments give this reason, not the community summary
|
||||
const t1 = doc('server/sim/content/kits/tier1.js');
|
||||
assert.equal((t1.match(/trigger: 'DEFAULT',/g) || []).length, 2, "PR #12's two kit lines");
|
||||
assert.ok(!/offensive skills activate when an enemy is in their skill range/.test(t1));
|
||||
assert.ok(!/documented for skillIndex 0/.test(t1));
|
||||
assert.match(DATA_MD, /a deliberate deviation, `tools\/build-data\.mjs TRIGGER_DEVIATIONS`, DESIGN §21\.29/);
|
||||
assert.match(SIM, /the six of DESIGN §21\.29/);
|
||||
assert.match(PLAYING, /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放/);
|
||||
assert.match(doc('CHANGELOG.md'), /深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能改为攻击范围内有敌人时就释放/);
|
||||
});
|
||||
@@ -0,0 +1,143 @@
|
||||
// The owner's decision after 0.1.1's community feedback (2026-10-03, "反馈的人太多了"; GitHub issue #4, PR #12): six 重装
|
||||
// skills cast with an enemy in range — the basic strategy, DEFAULT — instead of the official 下半 TANK class row's
|
||||
// TAKE_DAMAGE. A deliberate, documented deviation (DESIGN §21.29): 深巡 S2 行动能力剥夺, 雷蛇 S2 反击电弧, 号角 S2 暴风号令
|
||||
// and S3 终极防线, 灰毫 S1 攻击力强化·γ型 and S2 专注轰击. It lives in the data (tools/build-data.mjs TRIGGER_DEVIATIONS,
|
||||
// keyed per chess: 灰毫 S1 is the generic skcom_atk_up[3] other chess carry too), `rawRule` keeps the official TAKE_DAMAGE.
|
||||
// Every other MANUAL 重装 skill — 深巡 S1 侵袭破坏应对 among them — still waits for a hit.
|
||||
//
|
||||
// The battle check is real: the chess records of data/chess.json with their kits, the stage 战场#01(下半)
|
||||
// (act2autochess_m01), a real enemy (萨卡兹枯朽前锋, melee, 0.8 tiles/s) walking the lane from the gate (9,10) to the
|
||||
// objective; the operator stands on (9,5) facing the gate with its skill fully charged.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
import { bodyInKeys } from '../../server/sim/body.js';
|
||||
|
||||
const CHESS = JSON.parse(fs.readFileSync(new URL('../../data/chess.json', import.meta.url), 'utf8'));
|
||||
const ds = getDefaultSource();
|
||||
|
||||
/** [normal chess id, skill id] — both records (normal + elite) of each chess. */
|
||||
const DEVIATED = [
|
||||
['chess_char_1_04_a', 'skchr_udflow_2'], // 深巡 S2 行动能力剥夺
|
||||
['chess_char_1_20_a', 'skchr_liskam_2'], // 雷蛇 S2 反击电弧
|
||||
['chess_char_5_08_a', 'skchr_horn_2'], // 号角 S2 暴风号令
|
||||
['chess_char_5_08_a', 'skchr_horn_3'], // 号角 S3 终极防线
|
||||
['chess_char_2_18_a', 'skcom_atk_up[3]'], // 灰毫 S1 攻击力强化·γ型
|
||||
['chess_char_2_18_a', 'skchr_ashlok_2'], // 灰毫 S2 专注轰击
|
||||
];
|
||||
const both = (normalId) => [normalId, CHESS[normalId].goldenId];
|
||||
const isDeviated = (c, skillId) => DEVIATED.some(([id, s]) => id === c.baseId && s === skillId);
|
||||
const ENEMY = 'enemy_1422_lrsldr';
|
||||
|
||||
/**
|
||||
* One real battle: `chessId` with skill `skillId` on (9,5) facing RIGHT, SP full; one 萨卡兹枯朽前锋 on the lane from
|
||||
* `spawnAt` s. Returns the first cast (time, reason, whether an enemy stood on the unit's initial range then, whether
|
||||
* the unit blocked it) and the time the unit first took damage (any `damaged` event: what TAKE_DAMAGE listens to).
|
||||
*/
|
||||
function firstCast(chessId, skillId, { spawnAt = 0, seconds = 40 } = {}) {
|
||||
const index = CHESS[chessId].skills.find((s) => s.skillId === skillId).index;
|
||||
const h = makeBattle({
|
||||
stageId: 'act2autochess_m01', seed: 3, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId, row: 9, col: 5, dir: 'RIGHT', skillIndex: index, carryState: { sp: 999 } }],
|
||||
enemies: [{ key: ENEMY, route: 0, time: spawnAt }],
|
||||
});
|
||||
const u = h.unit(chessId);
|
||||
assert.equal(u.skill.id, skillId, `${chessId} runs ${skillId}`);
|
||||
const out = { rule: u.skill.rule, cast: null, reason: null, enemyInRange: false, blocking: false, hurt: null };
|
||||
h.b.on('damaged', (ctx) => { if (ctx.target === u && out.hurt == null) out.hurt = h.b.time; }, { priority: 1000 });
|
||||
h.b.on('skillStart', (ctx) => {
|
||||
if (ctx.unit !== u || out.cast != null) return;
|
||||
out.cast = h.b.time;
|
||||
out.reason = ctx.reason;
|
||||
const keys = new Set(u.baseRangeKeys || u.rangeKeys);
|
||||
out.enemyInRange = h.b.enemies.some((e) => e.alive && e.deployed && bodyInKeys(e, keys));
|
||||
out.blocking = u.blocking.length > 0;
|
||||
}, { priority: 1000 });
|
||||
h.runUntil(() => out.cast != null && out.hurt != null, seconds);
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
return out;
|
||||
}
|
||||
|
||||
test('data: the six deviated 重装 skills are DEFAULT on the normal and the elite record, rawRule the official TAKE_DAMAGE', () => {
|
||||
let n = 0;
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
const s = CHESS[id].skills.find((x) => x.skillId === skillId);
|
||||
assert.ok(s, `${id} ${skillId}`);
|
||||
assert.equal(CHESS[id].profession, 'TANK');
|
||||
assert.deepEqual(s.trigger, { rule: 'DEFAULT', rawRule: 'TAKE_DAMAGE', customRangeGrid: null }, `${id} ${skillId}`);
|
||||
// the sim's loadout def carries the data rule
|
||||
assert.equal(ds.getChess(id, { skillIndex: s.index }).skill.trigger.rule, 'DEFAULT', `${id} ${skillId} loadout def`);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
assert.equal(n, 12);
|
||||
// nothing else changed: every other MANUAL 重装 skill keeps the row (深巡 S1 included), and the generic
|
||||
// skcom_atk_up[3] keeps each other carrier's own rule (the deviation is keyed per chess, never per skill id)
|
||||
for (const c of Object.values(CHESS)) {
|
||||
for (const s of c.skills || []) {
|
||||
if (isDeviated(c, s.skillId)) continue;
|
||||
if (s.trigger.rawRule === 'TAKE_DAMAGE') assert.equal(s.trigger.rule, 'TAKE_DAMAGE', `${c.chessId} ${s.skillId}`);
|
||||
if (c.profession === 'TANK' && s.skillType === 'MANUAL' && s.trigger.rawRule !== 'CUSTOM_RANGE_SEARCH_ENEMY') assert.equal(s.trigger.rule, 'TAKE_DAMAGE', `${c.chessId} ${s.skillId}`);
|
||||
}
|
||||
}
|
||||
for (const id of both('chess_char_1_04_a')) assert.equal(CHESS[id].skills.find((s) => s.skillId === 'skchr_udflow_1').trigger.rule, 'TAKE_DAMAGE', `${id} 深巡 S1`);
|
||||
const others = Object.values(CHESS).filter((c) => c.baseId !== 'chess_char_2_18_a' && c.skills?.some((s) => s.skillId === 'skcom_atk_up[3]'));
|
||||
assert.ok(others.length >= 10, 'skcom_atk_up[3] is shared');
|
||||
for (const c of others) assert.deepEqual(c.skills.find((s) => s.skillId === 'skcom_atk_up[3]').trigger, { rule: 'DEFAULT', rawRule: 'DEFAULT', customRangeGrid: null }, c.chessId);
|
||||
});
|
||||
|
||||
test('PR #12\'s kit lines agree with the data: 深巡 / 雷蛇 S2 specs say DEFAULT, the other four deviated skills read the data', async () => {
|
||||
const { KITS } = await import('../../server/sim/content/index.js');
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
const rec = CHESS[id];
|
||||
const s = rec.skills.find((x) => x.skillId === skillId);
|
||||
const def = ds.getChess(id, { skillIndex: s.index });
|
||||
const kit = KITS[rec.baseId]?.(def.skill.bb, def.raw, def);
|
||||
const spec = s.isDefault ? kit?.skill : kit?.skills?.[skillId];
|
||||
const specRule = typeof spec?.trigger === 'string' ? spec.trigger : spec?.trigger?.rule;
|
||||
assert.ok(specRule == null || specRule === s.trigger.rule, `${id} ${skillId}: kit ${specRule} vs data ${s.trigger.rule}`);
|
||||
if (skillId === 'skchr_udflow_2' || skillId === 'skchr_liskam_2') assert.equal(specRule, 'DEFAULT', `${id} ${skillId}: PR #12's trigger line`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('real battle: each deviated skill casts with an enemy in range before anything hits the operator (normal + elite)', () => {
|
||||
for (const [normalId, skillId] of DEVIATED) {
|
||||
for (const id of both(normalId)) {
|
||||
const r = firstCast(id, skillId, { spawnAt: 3 });
|
||||
const where = `${id} ${skillId}: ${JSON.stringify(r)}`;
|
||||
assert.equal(r.rule, 'DEFAULT', where);
|
||||
assert.ok(r.cast != null, `never cast — ${where}`);
|
||||
assert.ok(r.cast >= 3, `no cast before an enemy is on the field — ${where}`);
|
||||
assert.equal(r.reason, 'DEFAULT', where);
|
||||
assert.ok(r.enemyInRange, `an enemy on its initial range at the cast — ${where}`);
|
||||
assert.ok(r.hurt == null || r.hurt > r.cast, `cast before the first hit — ${where}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('real battle: 深巡 S1 and every other MANUAL 重装 skill still wait for a hit (TAKE_DAMAGE)', () => {
|
||||
let n = 0;
|
||||
for (const c of Object.values(CHESS)) {
|
||||
if (c.profession !== 'TANK' || c.isDiy) continue;
|
||||
for (const s of c.skills) {
|
||||
if (s.skillType !== 'MANUAL' || s.trigger.rule !== 'TAKE_DAMAGE') continue;
|
||||
const r = firstCast(c.chessId, s.skillId);
|
||||
const where = `${c.chessId} ${c.name} ${s.skillId}: ${JSON.stringify(r)}`;
|
||||
assert.equal(r.rule, 'TAKE_DAMAGE', where);
|
||||
assert.ok(r.hurt != null && r.cast != null, `hit and cast — ${where}`);
|
||||
assert.equal(r.reason, 'TAKE_DAMAGE', where);
|
||||
assert.equal(r.cast, r.hurt, `cast by the first hit, not before — ${where}`);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
assert.ok(n >= 40, `${n} TAKE_DAMAGE records checked`);
|
||||
const udflowS1 = firstCast('chess_char_1_04_a', 'skchr_udflow_1');
|
||||
assert.ok(udflowS1.cast > 10, `深巡 S1 waits for the enemy to reach and hit her: ${JSON.stringify(udflowS1)}`);
|
||||
});
|
||||
Reference in new issue
Block a user