feedback5: merge WE1 — fidelity (sim rules): 秘术师 store at the attack check (深靛; 维伊 / 黑键 plug into it), enemy attacks land at their damage frame so a short stun / freeze / sleep / 浮空 / 缴械 / 恐惧 before it cancels the swing, 武者 heal per damage instance through 禁疗, 庇护 the strongest only, PHY-X 不高于 50 %, 链愈师 jumps in the 3×3, a summon readied at its owner's redeploy, Misery's duration skills, 伺夜 S2 in 战术点形态, centurions hit up to block-count targets; merge fixes: 黑键's kit keeps the up() import WE2's S2 cast needs (WE1 dropped it with its tick store — the first regeneration had stored 9288 swallowed errors in diy-114), the 自选 refusal names 集成战略 / 生息演算, tools/golden.mjs prints a missing value, test/golden.test.js fails on any stored engine error; golden: vs WE1 only the W4B / WE2 scenarios differ (roster-042, bond-egirShip-high, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116)
This commit is contained in:
commit
e2bb090f44
60 files changed
+6888
-6103
No files matched your search
@@ -11,6 +11,7 @@ import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.
|
||||
import * as enemiesMod from '../../server/sim/content/enemies.js';
|
||||
import * as bossesMod from '../../server/sim/content/bosses.js';
|
||||
import { spawnYanyou } from '../../server/sim/content/tokens.js';
|
||||
import { attackWindup } from '../../server/sim/ai.js';
|
||||
|
||||
const E = JSON.parse(fs.readFileSync(new URL('../../data/enemies.json', import.meta.url), 'utf8'));
|
||||
const W = JSON.parse(fs.readFileSync(new URL('../../data/waves.json', import.meta.url), 'utf8'));
|
||||
@@ -291,8 +292,9 @@ for (const [key, n] of Object.entries(DEATH_SPAWN)) {
|
||||
|
||||
for (const key of ['enemy_1207_sfji', 'enemy_1207_sfji_2']) {
|
||||
// PRTS 天赋 "攻击力+X%,持有4个【断刃】 / 每次成功攻击后消耗1个【断刃】,攻击结束时若已耗尽【断刃】,则立刻切换为无断刃模式并失去攻击力
|
||||
// 加成" (popup: "清空当次攻击间隔" — it attacks again at once); GitHub #107: until 0.2.0 each attack stacked another layer
|
||||
test(`${nm(key)}: one ATK layer while it holds a blade, gone with the 4th attack (which is followed at once by the next); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
// 加成" (popup: "清空当次攻击间隔" — it attacks again at once: since 0.2.0 its next swing starts at once and strikes at the
|
||||
// clip's damage frame, ai.js attackWindup); GitHub #107: until 0.2.0 each attack stacked another layer
|
||||
test(`${nm(key)}: one ATK layer while it holds a blade, gone with the 4th attack (which is followed at once by the next swing); unspent blades become 矛头 on death (at least 1)`, () => {
|
||||
const per = tb(key, 'Atkup.atk') ?? tb(key, 'AtkUp.atk'), cnt = tb(key, 'DeadSpawn.cnt');
|
||||
const h = arena({ units: [{ chessId: 't_wall', row: 9, col: 5 }] });
|
||||
h.step();
|
||||
@@ -304,8 +306,10 @@ for (const key of ['enemy_1207_sfji', 'enemy_1207_sfji_2']) {
|
||||
h.runUntil(() => e.stats.attacks >= cnt, 20);
|
||||
approx(e.s.atk, base, 1e-6, 'the last blade spent: 无断刃模式, no bonus');
|
||||
const tLast = h.b.time;
|
||||
assert.ok(h.runUntil(() => e.stats.attacks >= cnt + 1, 0.2), 'the mode switch clears the attack interval: the next attack at once');
|
||||
assert.ok(h.b.time - tLast < e.s.interval / 2, `${h.b.time - tLast} s after the 4th attack`);
|
||||
const wind = attackWindup(e);
|
||||
assert.ok(wind > 0 && h.runUntil(() => e.stats.attacks >= cnt + 1, wind + 0.2), 'the mode switch clears the attack interval: the next swing at once');
|
||||
assert.ok(Math.abs(h.b.time - tLast - wind) <= 3 * h.TICK, `${h.b.time - tLast} s after the 4th attack: its wind-up (${wind} s)`);
|
||||
assert.ok(h.b.time - tLast < e.s.interval / 2);
|
||||
const child = E[key].talents.bbStr['DeadSpawn.enemy_key'];
|
||||
killed(h, e, null);
|
||||
assert.equal(alive(h, child).length, 1, 'no blade left: still one 矛头');
|
||||
@@ -1289,6 +1293,7 @@ test(`${nm('enemy_10027_vtsk')}: entrance barrage (${skb('enemy_10027_vtsk', 'Ap
|
||||
assert.ok(barrage.every((c) => c.target === h.unit('t_wall')), 'the highest-HP unit');
|
||||
approx(barrage[0].amount, e.s.atk);
|
||||
h.runUntil(() => e.stats.attacks >= 1, 10);
|
||||
h.run(1); // (its shot lands: since 0.2.0 the first strike comes at the clip's damage frame, after the barrage window)
|
||||
const d = h.hooksOf('damaged').find((c) => c.source === e && c.dmg.isAttack);
|
||||
approx(d.amount, e.s.atk * tb('enemy_10027_vtsk', 'range.attack@atk_scale_range'));
|
||||
h.run(skb('enemy_10027_vtsk', 'MultiCombat').initCooldown + 6);
|
||||
@@ -2412,7 +2417,8 @@ test('“斩胄之剑” / “破胄之锤” 初始模式 attack: every ally in
|
||||
const h = bossArena({ units: [{ chessId: 't_wall', row: 10, col: 9 }, { chessId: 't_wall2', row: 10, col: 7 }] });
|
||||
h.step();
|
||||
put(h, key, [3, 8], { tag: 'part' });
|
||||
h.run(1);
|
||||
// (the first attack: its 0.8 / 0.867 s wind-up, then the shots — since 0.2.0 an attack strikes at its damage frame)
|
||||
h.runUntil(() => h.unit('t_wall').stats.taken > 0 && h.unit('t_wall2').stats.taken > 0, 3);
|
||||
approx(h.unit('t_wall').stats.taken, E[key].stats.atk * scale, 1e-6, key);
|
||||
approx(h.unit('t_wall2').stats.taken, E[key].stats.atk * scale, 1e-6, key);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// test/content/feedback5-lowhp-heal.test.js — the module trait "治疗生命值低于50%的友方单位时治疗量提升15%" (PHY-X / GUA-X) compares the
|
||||
// target's HP the way the client's module buff does (follow-up 19): FilterByTargetHpRatio LT — strictly below — for 瑕光 /
|
||||
// 古米 / 塞雷娅's GUA-X (set_heal_scale_by_hpratio) and 录武官's PHY-X (reckpr_e_002_tr); LE — at or below — for the template
|
||||
// heal_scale_up[hpratio][LE] of 华法琳's and 闪灵's PHY-X (and 黍's GUA-X, 凯尔希's PHY-X, already so). The text's 「低于」 is the
|
||||
// display; the buff templates decide (the local client's [uc]equips.ab prefabs and buff_template_data, read 2026-10-06).
|
||||
// Run: node --test test/content/feedback5-lowhp-heal.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const WALL = chessRec({ id: 't_wall', stats: { maxHp: 1e6, atk: 0, def: 0 }, skill: null });
|
||||
|
||||
/** The bonus factor `healer` gives a heal on an ally at `ratio` of its max HP (1 = none). */
|
||||
function factorAt(unitSpec, ratio) {
|
||||
const h = makeBattle({ defs: { chess: { t_wall: WALL } }, units: [unitSpec, { chessId: 't_wall', row: 11, col: 5 }], autoFinish: false, timeLimit: 30, seed: 3 });
|
||||
h.step();
|
||||
const healer = h.b.allyUnits.find((u) => u.uid === 1), t = h.unit('t_wall');
|
||||
t.hp = t.s.maxHp * ratio;
|
||||
const before = t.hp;
|
||||
const base = 100 * healer.s.healingDealtMul * t.s.healingTakenMul;
|
||||
h.b.heal(healer, t, 100);
|
||||
checkInvariants(h.b);
|
||||
return (t.hp - before) / base;
|
||||
}
|
||||
const near = (a, b, msg) => assert.ok(Math.abs(a - b) < 1e-9, `${msg}: ${a} vs ${b}`);
|
||||
|
||||
test('LE (heal_scale_up[hpratio][LE]): 华法琳 (tier 4 and 5) and 闪灵 PHY-X heal an ally at exactly 50 % HP ×1.15', () => {
|
||||
for (const id of ['chess_char_4_26_b', 'chess_char_5_04_b']) {
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.5), 1.15, `${id} at 50 %`);
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.6), 1, `${id} at 60 %`);
|
||||
}
|
||||
const shining = { uid: 1, diy: { slot: 6, charId: 'char_147_shining', skillIndex: 0, uniEquipId: 'uniequip_003_shining' }, elite: true, row: 10, col: 5 };
|
||||
near(factorAt(shining, 0.5), 1.15, '闪灵 PHY-X at 50 %');
|
||||
near(factorAt(shining, 0.6), 1, '闪灵 PHY-X at 60 %');
|
||||
});
|
||||
|
||||
test('LT (set_heal_scale_by_hpratio, reckpr_e_002_tr): 塞雷娅 GUA-X and 录武官 PHY-X (tier 4 and 5) give nothing at exactly 50 %, ×1.15 below', () => {
|
||||
for (const id of ['chess_char_5_11_b', 'chess_char_4_15_b', 'chess_char_5_23_b']) {
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.5), 1, `${id} at 50 %`);
|
||||
near(factorAt({ uid: 1, chessId: id, row: 10, col: 5 }, 0.49), 1.15, `${id} at 49 %`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
// test/content/feedback5-protect.test.js — 庇护 is one effect per unit whoever grants it (gamedata_const ba.protect / PRTS 术语
|
||||
// 庇护 "受到的物理和法术伤害降低相应比例(同名效果取最高)"): the older kits — 宴, 余, 缪尔赛思 (chess), 赫拉格, 左乐, 赫德雷 (自选) —
|
||||
// hold it through the shared key the other 自选 kits use (kits/shared/tier1.js PROTECT / holdProtect → battle.applyStrongest),
|
||||
// so the strongest value applies instead of a product of every source. 遥's bubbles keep their own key (the client's
|
||||
// damage_resistance[bonus], which multiplies with the common damage_resistance[inf]).
|
||||
// Run: node --test test/content/feedback5-protect.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 { PROTECT, holdProtect } from '../../server/sim/content/kits/shared/tier1.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
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 done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
/** 宴 (elite: 庇护 below hp_ratio) at 20 % HP with nothing to hit her. */
|
||||
function utage() {
|
||||
const h = makeBattle({ units: [{ chessId: 'chess_char_1_18_b', row: 9, col: 5 }], autoFinish: false, timeLimit: 60, seed: 3 });
|
||||
h.step();
|
||||
const u = h.unit('chess_char_1_18_b');
|
||||
u.hp = u.s.maxHp * 0.2;
|
||||
h.step();
|
||||
return { h, u, dr: ds.rawChess('chess_char_1_18_b').talents.filter((t) => t.index !== -1)[0].bb.damage_resistance };
|
||||
}
|
||||
/** Physical damage she takes from a 1000-point hit (her DEF taken off). */
|
||||
const physTaken = (h, u) => { const hp = u.hp; h.b.dealDamage(null, u, { amount: 1000, type: 'phys', canDodge: false }); const d = hp - u.hp; u.hp = hp; return d; };
|
||||
|
||||
test('宴 below her threshold holds the shared 庇护; a stronger one from another source replaces it (no product), a weaker one waits under it', () => {
|
||||
const { h, u, dr } = utage();
|
||||
assert.ok(dr > 0 && dr < 0.5);
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === dr, 'her own 庇护, on the shared key');
|
||||
const base = 1000 - u.s.def;
|
||||
approx(physTaken(h, u), base * (1 - dr), 'hers alone');
|
||||
// a stronger 庇护 from another source: the highest counts
|
||||
holdProtect(h.b, u, 0.5, 10, { id: 'other' });
|
||||
h.step();
|
||||
approx(physTaken(h, u), base * (1 - 0.5), 'the stronger one only — not ×(1 − dr) as well');
|
||||
assert.equal(u.buffs.filter((x) => x.key === PROTECT).length, 1, 'one 庇护 effect');
|
||||
done(h);
|
||||
// a weaker one: hers holds
|
||||
const w = utage();
|
||||
holdProtect(w.h.b, w.u, 0.1, 10, { id: 'other' });
|
||||
w.h.step();
|
||||
approx(physTaken(w.h, w.u), (1000 - w.u.s.def) * (1 - w.dr), 'hers — the weaker 庇护 waits');
|
||||
// back above the threshold her own lapses within a tick; the weaker one resumes for its remaining time
|
||||
w.u.hp = w.u.s.maxHp;
|
||||
w.h.step(3);
|
||||
const after = w.u.findBuff(PROTECT);
|
||||
assert.ok(after && after.data.value === 0.1, 'the weaker 庇护 resumed');
|
||||
done(w.h);
|
||||
});
|
||||
|
||||
test('余 (blocking), 缪尔赛思\'s melee copies, 赫拉格 / 左乐 (SBL-X, below 50 %) and 赫德雷 (余火之氅) all hold 庇护 on the shared key', () => {
|
||||
const src = (f) => readFileSync(new URL(`../../server/sim/content/kits/ops/${f}`, import.meta.url), 'utf8');
|
||||
for (const f of ['chess_char_1_18-utage.js', 'chess_char_6_03-yu.js', 'chess_char_6_11-mlyss.js', 'op-helage.js', 'op-zuole.js', 'op-hodrer.js']) {
|
||||
assert.match(src(f), /holdProtect\(/, `${f} uses the shared 庇护`);
|
||||
assert.doesNotMatch(src(f), /physTakenMul: 1 - dr|dmg\.mul \*= 1 - (dr|protectCut|num\(t\.damage_resistance\))/, `${f}: no private 庇护 multiplier left`);
|
||||
}
|
||||
// 遥's bubble stays its own effect (damage_resistance[bonus])
|
||||
assert.doesNotMatch(src('op-haruka.js'), /holdProtect\(/);
|
||||
// 赫拉格 (SBL-X, tier 5) at 40 % HP: his 庇护 on the shared key
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } },
|
||||
units: [{ diy: { slot: 5, charId: 'char_188_helage', skillIndex: 0, uniEquipId: 'uniequip_002_helage' }, elite: true, row: 10, col: 5 }],
|
||||
autoFinish: false, timeLimit: 30, seed: 3,
|
||||
});
|
||||
h.step();
|
||||
const u = h.b.allyUnits.find((x) => x.def?.charId === 'char_188_helage');
|
||||
u.hp = u.s.maxHp * 0.4;
|
||||
h.step();
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === 0.25, 'his SBL-X 庇护');
|
||||
u.hp = u.s.maxHp;
|
||||
h.step(3);
|
||||
assert.equal(u.findBuff(PROTECT), null, 'gone above 50 %');
|
||||
done(h);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
// test/content/feedback5-summon-recharge.test.js — one reading of the summoners' charge_token[born] (follow-up 20): the client's
|
||||
// RechargeToken on the owner's birth runs with timing NORMAL — the summon is ready at once —, while the summon's own
|
||||
// charge_token[finish] (ON_FINISH) is the timer that runs from its deployment (PRTS 傀影 备注 "部署召唤物后立刻开始计算再部署时间").
|
||||
// 傀影's 镜中虚影 (op-phatom.js) now reads it as 凯尔希's Mon3tr (op-kalts.js) and the other summoner kits do: when the owner
|
||||
// is deployed again, a summon still waiting for its own timer comes back at once (paying its cost).
|
||||
// Run: node --test test/content/feedback5-summon-recharge.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
/** The owner (uid 1, a tier-5 自选 piece) and its placed summon (uid 2). */
|
||||
function field(charId, tokenId) {
|
||||
const h = makeBattle({
|
||||
timeLimit: 900, autoFinish: false, seed: 5, flags: { dpInit: 99, dpPerSec: 0, dpMax: 999 },
|
||||
units: [{ uid: 1, diy: { slot: 'chess_char_5_diy1_a', charId, skillIndex: 0 }, row: 10, col: 2 }, { uid: 2, kind: 'token', tokenId, ownerUid: 1, row: 10, col: 4 }],
|
||||
});
|
||||
h.step();
|
||||
return { h, u: h.unit(1), t: h.unit(2) };
|
||||
}
|
||||
|
||||
for (const [name, charId, tokenId] of [['傀影 / 镜中虚影', 'char_250_phatom', 'token_10007_phatom_twin'], ['凯尔希 / Mon3tr', 'char_003_kalts', 'token_10002_kalts_mon3tr']]) {
|
||||
test(`${name}: the owner's redeployment readies a waiting summon at once (charge_token[born], timing NORMAL); its own timer still holds it while the owner stands`, () => {
|
||||
const { h, u, t } = field(charId, tokenId);
|
||||
assert.ok(u.alive && t.alive, 'both deployed');
|
||||
h.run(2);
|
||||
// knocked out with the owner standing: its own timer (from its deployment) holds it
|
||||
h.b.kill(t, null);
|
||||
h.run(3);
|
||||
assert.ok(!t.alive, 'waits for its own timer');
|
||||
// the owner leaves and comes back long before that timer has run
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u, { free: true }), 'the owner back');
|
||||
const dp = h.b.players[0].dp;
|
||||
assert.ok(h.runUntil(() => t.alive, 1), 'the summon comes back at once with the owner');
|
||||
assert.ok(h.b.time < 10, `at ${h.b.time.toFixed(2)} s — its own timer (≥ 25 s) has not run`);
|
||||
assert.equal(h.b.players[0].dp, dp - t.base.cost, 'paying its cost');
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
});
|
||||
}
|
||||
@@ -1036,7 +1036,7 @@ test('3_19 伺夜 S2 领袖的馈赠: +DP, the pack recovers HP, its next attack
|
||||
}
|
||||
});
|
||||
|
||||
test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at full SP while the pack stands and holds no unused gift (PRTS 备注), never while it is in its 战术点形态 [ASSUMED] — cast once it is back (kit pack and prep piece)', () => {
|
||||
test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at full SP while the pack is on the field and holds no unused gift (PRTS 备注), in its 战术点形态 too [ASSUMED: S1\'s words] — the DP at once, the gift kept for the returning pack (kit pack and prep piece)', () => {
|
||||
for (const id of BOTH('chess_char_3_19_a')) for (const piece of [false, true]) {
|
||||
const b = SB(id, 'skchr_vigil_2'), tag = `${id} ${piece ? 'piece' : 'kit pack'}`;
|
||||
const h = makeBattle({ defs: { chess: noGarrison(id) }, timeLimit: 60, flags: { dpPerSec: 0 }, units: vigilUnits(id, 'skchr_vigil_2', piece) });
|
||||
@@ -1052,17 +1052,20 @@ test('3_19 伺夜 S2 领袖的馈赠 (自动触发): no enemy needed — cast at
|
||||
fill(u);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 1, '"狼群未获得此技能的充能时可触发": no second cast while the gift is unused');
|
||||
// the gift spent (as by a bite), the pack in its 战术点形态: no cast (the 备注 does not count that form for S2 —
|
||||
// [ASSUMED]); once it is back, the waiting cast fires
|
||||
// the gift spent (as by a bite), the pack in its 战术点形态: the cast fires there too (its 备注 uses S1's words, which count
|
||||
// that form — [ASSUMED]; until 0.2.0 it waited for the pack): +cost DP at once, the gift waits on the pack
|
||||
w.mem.vigilGift = null;
|
||||
for (let i = 0; i < 10 && w.alive; i++) h.b.dealDamage(null, w, { amount: 1e9, type: 'true' }); // a wolf is lost per KO
|
||||
assert.ok(wolfTacticalPoint(w), `${tag}: 战术点形态`);
|
||||
h.run(5);
|
||||
assert.equal(u.skill.activations, 1, `${tag}: no cast while the pack is in its 战术点形态`);
|
||||
assert.ok(u.skill.ready, 'the SP waits full');
|
||||
const dp1 = p.dp;
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 0.5), `${tag}: cast while the pack is in its 战术点形态`);
|
||||
approx(p.dp, dp1 + b.cost, 1e-6, `${tag}: +cost DP at once`);
|
||||
assert.ok(w.mem.vigilGift && !w.alive, `${tag}: the gift waits on the pack`);
|
||||
fill(u);
|
||||
h.run(3);
|
||||
assert.equal(u.skill.activations, 2, `${tag}: no second cast while that gift is unused`);
|
||||
assert.ok(h.runUntil(() => w.alive, WOLF_IV(id)), `${tag}: the pack is back`);
|
||||
assert.ok(h.runUntil(() => u.skill.activations === 2, 0.5), `${tag}: cast once it stands again`);
|
||||
assert.ok(w.mem.vigilGift, 'the returned pack holds the gift');
|
||||
assert.ok(w.mem.vigilGift, `${tag}: the returned pack holds the gift for its next attack`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -708,7 +708,11 @@ test('6_11 缪尔赛思 S2 生态耦合: +cost DP; melee copies regenerate and t
|
||||
const eco = tok().findBuff('mlyss:eco');
|
||||
assert.ok(eco, 'melee copy buffed');
|
||||
approx(eco.mods.hpRegenRatio, bb.hp_recovery_per_sec_by_max_hp_ratio);
|
||||
approx(eco.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
// 庇护: the shared effect of every source (同名效果取最高 — kits/shared/tier1.js holdProtect)
|
||||
const protect = tok().findBuff('protect');
|
||||
assert.ok(protect && protect.source === u, 'the 庇护 she grants');
|
||||
approx(protect.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
approx(protect.mods.artsTakenMul, 1 - bb.damage_resistance);
|
||||
done(h);
|
||||
|
||||
// ranged copy: a sniper to copy
|
||||
|
||||
@@ -281,8 +281,8 @@ test('余: blocked enemies take 40 % ATK arts + 12 % burn per second, 庇护 whi
|
||||
assert.ok(dot.length >= 2);
|
||||
const burn = h.dmg.filter((d) => d.src === u.id && d.element === 'burn');
|
||||
assert.ok(burn.length >= 2);
|
||||
// 庇护 (ba.protect): physical and arts damage only
|
||||
assert.ok(u.findBuff('yu:shelter') && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
// 庇护 (ba.protect): physical and arts damage only — the shared effect of every source (kits/shared/tier1.js holdProtect)
|
||||
assert.ok(u.findBuff('protect')?.source === u && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
assert.equal(u.s.trueTakenMul, 1, '庇护 does not reduce true damage');
|
||||
assert.equal(u.s.dmgTakenMul, 1);
|
||||
const base = u.base.maxHp;
|
||||
|
||||
@@ -20,7 +20,8 @@ const FORMS = BACKUPS.units[HOD].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const CRUX = 'uniequip_002_hodrer', CRUY = 'uniequip_003_hodrer';
|
||||
const S1 = 'skchr_hodrer_1', S2 = 'skchr_hodrer_2', S3 = 'skchr_hodrer_3';
|
||||
const PROTECT = 'talent:hodrer:protect';
|
||||
const PROTECT = 'protect'; // the shared 庇护 of every source (kits/shared/tier1.js holdProtect)
|
||||
const DMG_UP = 'talent:hodrer:dmgUp'; // CRU-X stage 3's 物理伤害 + (its own one instance per ally)
|
||||
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);
|
||||
@@ -173,7 +174,7 @@ test('T2 余火之氅: he and the ally on the tile behind him get 18 % 庇护 (C
|
||||
assert.ok(m, `${label(f)}: ${a.def.charId} protected`);
|
||||
approx(m.physTakenMul, 1 - dr, `${label(f)}: phys`);
|
||||
approx(m.artsTakenMul, 1 - dr, `${label(f)}: arts`);
|
||||
assert.equal(m.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
assert.equal(a.findBuff(DMG_UP)?.mods.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
}
|
||||
assert.equal(beside.findBuff(PROTECT), null, `${label(f)}: not beside him`);
|
||||
const hp = behind.hp;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// server/sim/content/kits/ops/standin-acspec.js) on every chess she replaces: 缄默德克萨斯 (tier 4, S2), 山 (tier 5, S3 +
|
||||
// EXE-X), 新约能天使 / 锏 (tier 6, S3 + EXE-X), normal (E2 Lv1, skill 4) and elite (E2 Lv60, skill 7). Her three skills
|
||||
// are ON_DEPLOY passives: they start at every deployment — at the battle start no enemy is on the field yet, so S3 hits
|
||||
// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). Numbers are read back from data/backups.json.
|
||||
// nothing there and matters on a redeploy (再部署时间大幅度减少: 18 s). S1 / S2 last 10 s: since 0.2.0 they are duration skills
|
||||
// started by every deployment (activateOnDeploy — PR #109's deploy-timed contract, their own skillStart / skillEnd), no
|
||||
// longer a passive holding a 10 s buff. Numbers are read back from data/backups.json.
|
||||
// Run: node --test test/content/standin_acspec.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
@@ -26,7 +28,7 @@ const ALLY = 'chess_char_1_02_a'; // 角峰, a melee blocker
|
||||
function battle(units, o = {}) {
|
||||
return makeBattle({
|
||||
defs: { enemies: ENEMIES }, units, timeLimit: o.timeLimit ?? 400, autoFinish: false, seed: o.seed ?? 5,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death'], captureNoisy: true,
|
||||
flags: { dpPerSec: 0 }, hooks: ['damaged', 'deploy', 'death', 'skillStart', 'skillEnd'], captureNoisy: true,
|
||||
});
|
||||
}
|
||||
const MISERY = (id, standIn = true) => ({ chessId: id, row: 10, col: 4, standIn });
|
||||
@@ -38,7 +40,7 @@ function done(h) {
|
||||
assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0]));
|
||||
}
|
||||
|
||||
test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill as a passive, this kit; EXE-X on the tier-5 / 6 elites only', () => {
|
||||
test('Misery on every chess she replaces (normal + elite): her body (再部署 18 s), the backup skill (S2 a deploy-timed duration skill, S3 a passive), this kit; EXE-X on the tier-5 / 6 elites only', () => {
|
||||
const ids = UNIT.standsIn.flatMap((a) => [a, a.replace(/_a$/, '_b')]);
|
||||
assert.equal(ids.length, 8);
|
||||
const withModule = [];
|
||||
@@ -48,7 +50,8 @@ test('Misery on every chess she replaces (normal + elite): her body (再部署 1
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
const sk = form.skills.find((s) => s.index === c.backup.skillIndex);
|
||||
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, 'passive', false, 'skills'], id);
|
||||
const kind = sk.skillId === 'skchr_acspec_3' ? 'passive' : 'duration';
|
||||
assert.deepEqual([u.def.charId, u.def.standInFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [CHAR, c.charId, sk.skillId, kind, false, 'skills'], id);
|
||||
assert.deepEqual([sk.skillType, sk.spType], ['PASSIVE', 'ON_DEPLOY'], id);
|
||||
const mod = c.backup.uniEquipId && c.status.equipLevel > 0 ? form.modules.find((m) => m.uniEquipId === c.backup.uniEquipId) : null;
|
||||
assert.equal(!!u.def.raw.module?.active, !!mod, `${id}: module`);
|
||||
@@ -137,7 +140,7 @@ test('Misery S1 物理的服从 (no chess names it — a 自选 slot may): for 1
|
||||
const h = battle([MISERY(id, { skillIndex: 0 })]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, sk.duration], ['skchr_acspec_1', 'passive', 10]);
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_1', 'duration', 10, 10]);
|
||||
const prob = () => { let p = null; u.profile.hitsFn({ rng: { chance: (x) => { p = x; return false; } } }, u); return p; };
|
||||
const on = () => { approx(u.s.dodgePhys, bb.prob, `${id}: dodge ${bb.prob}`); assert.equal(prob(), bb['attack@prob'], `${id}: chance ${bb['attack@prob']}`); };
|
||||
const off = () => { assert.equal(u.s.dodgePhys, 0, `${id}: no dodge`); assert.equal(prob(), talentBb(u, 0)['attack@prob'], `${id}: the talent's chance`); };
|
||||
@@ -162,9 +165,13 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy
|
||||
const h = battle([MISERY(id)]);
|
||||
h.step();
|
||||
const u = h.unit(1);
|
||||
assert.deepEqual([u.skill.id, sk.duration], ['skchr_acspec_2', 10]);
|
||||
const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); };
|
||||
const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); };
|
||||
assert.deepEqual([u.skill.id, u.skill.kind, u.skill.duration, sk.duration], ['skchr_acspec_2', 'duration', 10, 10]);
|
||||
// the deploy-timed contract (PR #109): the skill runs — skillStart now, skillEnd 10 s later — and then is off
|
||||
const on = () => { approx(u.s.atk, u.base.atk * (1 + bb.atk), `${id}: ATK +${bb.atk}`); assert.equal(u.s.aspd, 100 + bb.attack_speed, `${id}: ASPD`); assert.ok(u.skill.active, `${id}: running`); };
|
||||
const off = () => { approx(u.s.atk, u.base.atk, `${id}: ATK back`); assert.equal(u.s.aspd, 100, `${id}: ASPD back`); assert.ok(!u.skill.active, `${id}: over`); };
|
||||
const starts = () => h.hooksOf('skillStart').filter((c) => c.unit === u).length;
|
||||
const ends = () => h.hooksOf('skillEnd').filter((c) => c.unit === u).length;
|
||||
assert.deepEqual([starts(), ends()], [1, 0], `${id}: started by the deployment`);
|
||||
on();
|
||||
h.run(9.8);
|
||||
on();
|
||||
@@ -176,6 +183,7 @@ test('Misery S2 战争的恭顺 (缄默德克萨斯): for 10 s from every deploy
|
||||
on();
|
||||
h.run(10.1);
|
||||
off();
|
||||
assert.deepEqual([starts(), ends()], [2, 2], `${id}: one window per deployment (the retreat ends none — it had ended)`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user