feedback6: merge R21P — every operator at full potential (潜能 6; GitHub #252, PR #255 by @SpiritedAwayCN, the owner's decision of 2026-10-07)

- tools/build-data.mjs applies every potentialRanks attribute step (COST, RESPAWN_TIME, ATK, MAX_HP, DEF,
  MAGIC_RESISTANCE, ATTACK_SPEED) to the chess, elites, 补位 and 自选 units and picks talent / trait / module-talent
  candidates up to requiredPotentialRank 5; summons take the owner's potential for their talents, not its stat steps.
  data/chess.json, backups.json, tokens.json and the en / ja / ko / zh-TW data translations rebuilt; op-wang.js reads
  her stock cap (8 at full potential). 刺玫 435 ATK / cost 15, 砾 16 s, 至简 70 s.
- No conflicts outside the golden files (regenerated). Golden vs HEAD: roster 49, bonds 46, fields 22, matches 18,
  diy 133 (standins and diy-012 / diy-135…138 unchanged: no potential ranks or never hit). Union check against the
  branch and the merge base: every scenario only the branch moved equals the branch, except roster-048 and
  solo-ABYSS-2: at full potential a 频次 器物 is reached there (roster-048: one more enemy dies instead of leaking, 86 → 87
  units), so R21H's hit count (data × hpMul ÷ supplyHpMul) applies where it did not before — with only that rule put back
  (archetypes.js times as at the merge base) both equal the branch exactly.
- Tests: broad node --test 5147 / 5147; lint 0 errors.
This commit is contained in:
sganggs committed 2026-10-07 18:48:38 +08:00
commit b05845f507
178 files changed
+7667 -7317

No files matched your search

+6 -4
View File
@@ -45,7 +45,7 @@ test('chess: 133 base chess — 74 PRESET / 55 NORMAL / 4 DIY — and every reco
for (const c of ofType('PRESET')) assert.equal(c.backup.charId, c.charId, `${c.chessId}: 特许 — its own backup`);
for (const c of ofType('NORMAL')) {
assert.ok(c.backup.charId && c.backup.charId !== c.charId, `${c.chessId}: a NORMAL chess names another character`);
assert.equal(c.backup.potRank, 0, `${c.chessId}: stand-ins fight at potential 0`);
assert.equal(c.backup.potRank, 0, `${c.chessId}: the official row names potential 0 (moot: the 原型干员 have no potential ranks)`);
assert.ok(c.tier >= 3, `${c.chessId}: no NORMAL chess below tier 3`);
}
for (const c of ofType('DIY')) assert.deepEqual(c.backup, { charId: null, tmplId: null, skillIndex: 0, uniEquipId: null, potRank: 0 });
@@ -172,6 +172,8 @@ test('backups: the stand-in numbers re-derived from the raw official tables', {
for (const c of all.filter((x) => x.chessType === 'NORMAL')) {
const r = standInRecord(c, backups);
const ch = CT[c.backup.charId];
// full potential for every unit (the owner's decision of 2026-10-07) is the plain keyframes here: no 原型干员 has a potential rank
assert.equal((ch.potentialRanks || []).length, 0, `${c.backup.charId}: no potential ranks`);
const P = ch.phases[c.status.phase];
const k0 = P.attributesKeyFrames[0], k1 = P.attributesKeyFrames[P.attributesKeyFrames.length - 1];
const t = (c.status.level - k0.level) / (k1.level - k0.level || 1);
@@ -274,10 +276,10 @@ test('DIY: the 71 owned 6★ picks — a form at every slot status with all thre
assert.deepEqual(Object.keys(rec.variants), rec.owners, `${id}: owners = variant keys`);
for (const o of rec.owners) assert.ok(/^char_\w+@\d+\/\d+\/\d+\/\d+$/.test(o) && ownedPool.includes(o.split('@')[0]), `${id}: owner ${o}`);
}
// spot checks (zh_CN client data): 推进之王 E2 Lv60 = 2046 HP / 484 ATK, SOL-X stage 1 ATK +60 DEF +40 and its trait, stage 3
// the 万兽之王 change; 令 S1 / S2 / S3 summon 清平 / 逍遥 / 弦惊
// spot checks (zh_CN client data): 推进之王 E2 Lv60 = 2046 HP / 484 ATK (+ 攻击力+25 at full potential: 509; cost 2 lower),
// SOL-X stage 1 ATK +60 DEF +40 and its trait, stage 3 the 万兽之王 change; 令 S1 / S2 / S3 summon 清平 / 逍遥 / 弦惊
const siege = backups.units.char_112_siege.forms;
assert.deepEqual([siege['2/60/7/1'].stats.maxHp, siege['2/60/7/1'].stats.atk], [2046, 484]);
assert.deepEqual([siege['2/60/7/1'].stats.maxHp, siege['2/60/7/1'].stats.atk], [2046, 509]);
const solx1 = siege['2/60/7/1'].modules.find((m) => m.typeName === 'SOL-X');
assert.deepEqual([solx1.attr, solx1.traitOverride.bb, solx1.talentChanges], [{ atk: 60, def: 40 }, { atk: 0.08, def: 0.08 }, []]);
assert.deepEqual(siege['2/60/7/3'].modules.find((m) => m.typeName === 'SOL-X').talentChanges.map((t) => [t.talentIndex, t.bb]), [[0, { atk: 0.08, def: 0.08 }]]);
+4 -4
View File
@@ -715,11 +715,11 @@ test('阿戈尔 (community report, 2026-10-07) step 1: the bond HP is on every m
assert.equal(h.hooksOf('death').filter((c) => c.unit === h.unit(DEEP) && c.reason === 'killed').length, stands ? 0 : 1, `L ${L}`);
if (stands) close(post.get(DEEP).hp, 2904 * 1.5 - 4345, 1e-6, 'L 15: 4356 − 4345 = 11 HP left');
}
// an elite 幽灵鲨 at 62 layers: 2422 × (1 + 0.35 + 0.62 + 0.10 her talent) = 5013.54 HP, DEF 365 — one mark (4635) leaves
// 378.54; behind 浊心斯卡蒂 and 乌尔比安 she takes both marks and falls to the second (the kill is 乌尔比安's)
// an elite 幽灵鲨 at 62 layers: 2422 × (1 + 0.35 + 0.62 + 0.12 her talent at full potential) = 5061.98 HP, DEF 365 — one
// mark (4635) leaves 426.98; behind 浊心斯卡蒂 and 乌尔比安 she takes both marks and falls to the second (the kill is 乌尔比安's)
const one = fight([{ chessId: ULPIA, row: 10, col: 3 }, { chessId: GHOST, row: 10, col: 4 }], 62);
close(one.pre.get(GHOST).maxHp, 2422 * 2.07, 1e-6, 'over 5000 HP');
close(one.post.get(GHOST).hp, 2422 * 2.07 - (5000 - 365), 1e-6, 'one mark: she stands');
close(one.pre.get(GHOST).maxHp, 2422 * 2.09, 1e-6, 'over 5000 HP');
close(one.post.get(GHOST).hp, 2422 * 2.09 - (5000 - 365), 1e-6, 'one mark: she stands');
assert.equal(one.h.hooksOf('death').length, 0);
const two = fight([{ chessId: SKADI2, row: 10, col: 3 }, { chessId: ULPIA, row: 10, col: 4 }, { chessId: GHOST, row: 10, col: 5 }], 62);
assert.deepEqual(tagged(two.h, 'bond:egir:devour').filter((c) => c.target.defId === GHOST).map((c) => [c.source.defId, c.amount]),
@@ -1,5 +1,5 @@
// test/content/feedback5-swire2-revive-cost.test.js — 琳琅诗怀雅's 破财消灾 (「受到致命伤害时,若费用足够则消耗5点部署费用使生命恢复到
// 70%,每次触发该天赋时消耗的费用翻倍」): the doubled cost restarts at 5 with every deployment — PRTS 备注 「再部署时重置本天赋费用
// 70%,每次触发该天赋时消耗的费用翻倍」; 80 % at full potential, the owner's decision of 2026-10-07): the doubled cost restarts at 5 with every deployment — PRTS 备注 「再部署时重置本天赋费用
// 消耗」 (community report of 2026-10-06 「进入联防阶段琳琅诗怀雅所需的复活费用应该重置,正常对局中死亡之后再部署复活费用也应该
// 重置」). A 联防 field is a battle of its own, so it always started at 5; a redeploy used to keep the doubled cost.
// Run: node --test test/content/feedback5-swire2-revive-cost.test.js
@@ -26,7 +26,7 @@ function lethal(h, u, ps, dp = 90) {
test('破财消灾 doubles within one deployment and restarts at 5 DP after a redeploy', () => {
const { h, u, ps } = field();
assert.equal(lethal(h, u, ps), 5, 'first save: 5 DP');
assert.ok(u.alive && Math.abs(u.hp - u.s.maxHp * 0.7) < 1e-6, 'back to 70 % HP');
assert.ok(u.alive && Math.abs(u.hp - u.s.maxHp * 0.8) < 1e-6, 'back to 80 % HP (hp_ratio at full potential)');
assert.equal(lethal(h, u, ps), 10, 'second save of the same deployment: doubled');
h.b.retreat(u);
assert.ok(!u.alive);
@@ -3,7 +3,7 @@
// data's deploy limit / holding carry the tokens' own talent additions (tools/build-data.mjs tokenTalentDeckBonus: the
// hidden "TOKEN数" talent max_deploy_count / max_deck_stack_cnt on top of the attribute frame). Official counts: the owners'
// talents (麦哲伦 / 令 / 电弧 "最多同时部署3个", SUM-Y stage 2+ "最多同时部署4个", 白铁 / 娜斯提 "最多可部署2个", 望 "可以使用6枚
// 棋子", TRP-X +1), PRTS 幻影 备注 "最大可部署数量为3" (夜莺).
// 棋子" — 7 at full potential (望's 潜能 3 「第一天赋效果增强」, the owner's decision of 2026-10-07) —, TRP-X +1), PRTS 幻影 备注 "最大可部署数量为3" (夜莺).
// #24: a summon no owner shows (displayTokenDict: the variants' `display` source) is its skill's own object, never a hand
// card — 予愿安洁莉娜 S3's “一会儿见!” (PRTS 予愿安洁莉娜 S3 备注: the skill deploys it at her initial tile).
// Run: node --test test/content/feedback5-we2-summon-hand.test.js
@@ -33,7 +33,7 @@ function official(charId, mod, stage) {
case 'char_4072_ironmn': return 2;
case 'char_4212_nasti': return 2;
case 'char_179_cgbird': return 3; // PRTS 幻影 备注
case 'char_2027_wang': return mod === 'uniequip_002_wang' ? 7 : 6; // TRP-X +1
case 'char_2027_wang': return mod === 'uniequip_002_wang' ? 8 : 7; // 7 at full potential, TRP-X +1
case 'char_4048_doroth': return 9; // 10 / 13, the hand's 9 cap
default: return 1;
}
@@ -89,8 +89,8 @@ test('the data: deploy limit / holding = the attribute frame + the token talents
const ph = 'token_10003_cgbird_bird';
assert.equal(v(ph, 'char_179_cgbird@2/1/4/0').stats.deployLimit, 3);
assert.equal(v(ph, 'char_179_cgbird@2/60/7/3').byModule.uniequip_003_cgbird.stats.deployLimit, 3);
// 望's 棋子: the rank-2 talent (+1) is a potential talent — potential 0 keeps 6
assert.equal(v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deployLimit, 6);
// 望's 棋子: the rank-2 talent (+1) is a potential talent (望's 潜能 3) — at full potential 6 + 1 = 7, holding 7 + 1 = 8
assert.deepEqual([v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deployLimit, v('token_10064_wang_stone1', 'char_2027_wang@2/1/4/0').stats.deckStack], [7, 8]);
// the chess summons of tokens.json have no such talent: 凯瑟琳's 爬行号 2 of 3, everyone else 1
const tokens = load('tokens');
assert.equal(tokens.token_10041_cathy_catsld.variants.chess_char_4_11_a.stats.deployLimit, 2);
+4 -4
View File
@@ -1421,7 +1421,7 @@ test('module 维娜 秩序圣“球”: 战栗 6 s (elite / leader 12 s), ×1.15
done(d);
});
test('module 焰影苇草 “独属自己的一隅”: damage ×1.1 while an operator of her range is injured; 灼痕 38 % / 8 s', () => {
test('module 焰影苇草 “独属自己的一隅”: damage ×1.1 while an operator of her range is injured; 灼痕 40 % / 8 s (full potential)', () => {
const id = ELITE('chess_char_6_08'), M = 'uniequip_003_reed2';
const h = run({ defs: { chess: { a: plain('a') }, enemies: { e: dummy('e') } }, units: [{ chessId: id, row: 10, col: 4, moduleId: M, carryState: IDLE }, { chessId: 'a', row: 11, col: 5 }], enemies: [{ key: 'e', pos: [10, 6] }] });
const u = h.unit(id), a = h.unit('a');
@@ -1430,7 +1430,7 @@ test('module 焰影苇草 “独属自己的一隅”: damage ×1.1 while an ope
a.hp = a.s.maxHp * 0.5;
h.run(0.5);
approx(u.findBuff('reed2:corner')?.mods.dmgDealtMul ?? 1, 1.1, 'injured ally ⇒ ×1.1');
approx(tal(id, MOD(M)).damage_scale, 1.38);
approx(tal(id, MOD(M)).damage_scale, 1.4); // 1.38 + the potential step
done(h);
const d = run({ defs: { chess: { a: plain('a') } }, units: [{ chessId: id, row: 10, col: 4, carryState: IDLE }, { chessId: 'a', row: 11, col: 5 }] });
d.step();
@@ -1494,7 +1494,7 @@ test('module 迷迭香 特限证章: two hits (one aftershock) instead of three'
}
});
test('module 流明 “幸运”: far heals are not reduced; 应急处理 heals 100 % with a 10 s cooldown', () => {
test('module 流明 “幸运”: far heals are not reduced; 应急处理 heals 100 % with an 8 s cooldown (full potential)', () => {
const id = ELITE('chess_char_6_14'), M = 'uniequip_003_lumen';
for (const [moduleId, mul] of [[M, 1], [null, tbOf(id).heal_scale]]) {
const h = run({ defs: { chess: { f: plain('f', { stats: { maxHp: 1e5 } }) } }, units: [{ chessId: id, row: 10, col: 3, moduleId, carryState: IDLE }, { chessId: 'f', row: 10, col: 6 }] });
@@ -1506,7 +1506,7 @@ test('module 流明 “幸运”: far heals are not reduced; 应急处理 heals
done(h);
}
approx(tal(id, MOD(M), 1).heal_scale, 1);
approx(tal(id, MOD(M), 1).duration, 10);
approx(tal(id, MOD(M), 1).duration, 8); // 10 s − the potential step
});
test('module 仇白 欲雪时: the first hit on an enemy binds it 3 s; ASPD +12 with ≥2 enemies in range', () => {
+18 -18
View File
@@ -76,8 +76,8 @@ test('予愿安洁莉娜 in every 自选 form: her operator kit (all three skill
done(h);
}
}
// E2 Lv1 1884 / 628 / 410, E2 Lv60 2216 / 739 / 483
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk, FORMS['2/60/7/3'].stats.atk], [1884, 628, 739, 739]);
// E2 Lv1 1884 / 661 / 410, E2 Lv60 2216 / 772 / 483
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk, FORMS['2/60/7/3'].stats.atk], [1884, 661, 772, 772]);
assert.deepEqual(FORMS['2/60/7/3'].modules ?? [], []);
});
@@ -115,12 +115,12 @@ test('trait 巡空者: on the ground she blocks ground enemies, not flyers; airb
}
});
test('T1 飘浮大地之上: every damage she outputs adds ATK × 35 % arts on a target of weight ≤ 3, else × 25 % — dodged hits too, never on itself; airborne, every enemy of her range is 失重 (weight −1, not with another 失重), gone when she lands', () => {
test('T1 飘浮大地之上: every damage she outputs adds ATK × 45 % arts on a target of weight ≤ 3, else × 30 % — dodged hits too, never on itself; airborne, every enemy of her range is 失重 (weight −1, not with another 失重), gone when she lands', () => {
for (const f of FORMS_ALL) {
const [tier, elite] = f;
const t0 = formOf(tier, elite).talents[0].bb;
assert.deepEqual([t0.atk_scale_lo, t0.atk_scale_hi, t0.mass_level], [0.25, 0.35, 3], label(f));
// on the ground (S3 picked, not cast): weight 0 ⇒ 35 %, weight 4 ⇒ 25 %
assert.deepEqual([t0.atk_scale_lo, t0.atk_scale_hi, t0.mass_level], [0.3, 0.45, 3], label(f));
// on the ground (S3 picked, not cast): weight 0 ⇒ 45 %, weight 4 ⇒ 30 %
{
const { h, u } = field({ tier, elite, skill: 2 });
u.skill.sp = 0;
@@ -129,13 +129,13 @@ test('T1 飘浮大地之上: every damage she outputs adds ATK × 35 % arts on a
const hit = ANY_ATK(h, u)[0];
const ex = extras(h, u).filter((c) => c.target === a);
assert.equal(ex.length, 1, `${label(f)}: one addition per damage`);
approx(ex[0].amount, u.s.atk * 0.35, `${label(f)}: weight 0`, 1e-6);
approx(ex[0].amount, u.s.atk * 0.45, `${label(f)}: weight 0`, 1e-6);
assert.equal(ex[0].type, 'arts');
approx(hit.amount, u.s.atk, `${label(f)}: her attack 100 %`, 1e-6);
h.b.kill(a, null);
const heavy = h.spawn('enemy_heavy', { pos: [10, 5] });
assert.ok(h.runUntil(() => extras(h, u).some((c) => c.target === heavy), 3));
approx(extras(h, u).find((c) => c.target === heavy).amount, u.s.atk * 0.25, `${label(f)}: weight 4 ⇒ 25 %`, 1e-6);
approx(extras(h, u).find((c) => c.target === heavy).amount, u.s.atk * 0.3, `${label(f)}: weight 4 ⇒ 30 %`, 1e-6);
// a dodged hit still adds it
h.b.addBuff(heavy, { key: 'test:dodge', mods: { dodgePhys: 1 } });
const n0 = extras(h, u).length, d0 = h.hooksOf('dodge').length;
@@ -143,7 +143,7 @@ test('T1 飘浮大地之上: every damage she outputs adds ATK × 35 % arts on a
assert.equal(extras(h, u).length, n0 + 1, `${label(f)}: an output, dodged or not`);
done(h);
}
// airborne (S1): 失重 in range — the weight-4 enemy counts as 3 ⇒ 35 %
// airborne (S1): 失重 in range — the weight-4 enemy counts as 3 ⇒ 45 %
{
const { h, u } = field({ tier, elite, skill: 0 });
const heavy = h.spawn('enemy_heavy', { pos: [11, 6] });
@@ -156,7 +156,7 @@ test('T1 飘浮大地之上: every damage she outputs adds ATK × 35 % arts on a
assert.equal(marked.buffs.filter((b) => b.status === 'weightless').length, 1, `${label(f)}: 同名效果不可叠加`);
const k0 = extras(h, u).length;
assert.ok(h.runUntil(() => extras(h, u).slice(k0).some((c) => c.target === heavy), 3));
approx(extras(h, u).slice(k0).find((c) => c.target === heavy).amount, u.s.atk * 0.35, `${label(f)}: weight 3 (失重) ⇒ 35 %`, 1e-6);
approx(extras(h, u).slice(k0).find((c) => c.target === heavy).amount, u.s.atk * 0.45, `${label(f)}: weight 3 (失重) ⇒ 45 %`, 1e-6);
u.skill.end('test');
h.step();
assert.equal(heavy.weight, 4, `${label(f)}: 失重 gone when she lands`);
@@ -165,28 +165,28 @@ test('T1 飘浮大地之上: every damage she outputs adds ATK × 35 % arts on a
}
});
test('T2 天穹间的舞步: while she is on the field every airborne allied operator (her included) has ATK +13 % and, while it blocks, 生命回复速度 +8 % max HP (hpRegenRatio — no heal, 禁疗 does not stop it); none on the ground', () => {
test('T2 天穹间的舞步: while she is on the field every airborne allied operator (her included) has ATK +18 % and, while it blocks, 生命回复速度 +10 % max HP (hpRegenRatio — no heal, 禁疗 does not stop it); none on the ground', () => {
for (const f of FORMS_ALL) {
const [tier, elite] = f;
const t1 = formOf(tier, elite).talents[1].bb;
assert.deepEqual([t1.atk, t1.hp_recovery_per_sec_by_max_hp_ratio], [0.13, 0.08], label(f));
assert.deepEqual([t1.atk, t1.hp_recovery_per_sec_by_max_hp_ratio], [0.18, 0.1], label(f));
const others = [{ uid: 2, chessId: 'chess_char_1_02_a', row: 12, col: 3 }];
const { h, u } = field({ tier, elite, skill: 0, row: 9, col: 4, others });
const yak = h.unit(2);
h.run(0.3);
assert.deepEqual(u.findBuff('talent:aglna2:dance')?.mods, { atkPct: 0.13 }, `${label(f)}: airborne, not blocking`);
approx(u.s.atk, u.base.atk * (1 + skillOf(tier, elite, S1).bb.atk + 0.13), `${label(f)}: ATK`);
assert.deepEqual(u.findBuff('talent:aglna2:dance')?.mods, { atkPct: 0.18 }, `${label(f)}: airborne, not blocking`);
approx(u.s.atk, u.base.atk * (1 + skillOf(tier, elite, S1).bb.atk + 0.18), `${label(f)}: ATK`);
assert.equal(yak.findBuff('talent:aglna2:dance'), null, `${label(f)}: 角峰 on the ground: none`);
h.b.addBuff(u, { key: 'test:healFree', flags: { noHeal: true, healFree: true } });
const fl = h.spawn('enemy_flywalk', { routeIndex: 2 });
assert.ok(h.runUntil(() => fl.blockedBy === u, 30), 'she blocks a flyer');
h.run(0.3);
assert.deepEqual(u.findBuff('talent:aglna2:dance')?.mods, { atkPct: 0.13, hpRegenRatio: 0.08 }, `${label(f)}: blocking`);
approx(u.s.hpRegen, 0.08 * u.s.maxHp, `${label(f)}: 8 % max HP / s as 生命回复速度`);
assert.deepEqual(u.findBuff('talent:aglna2:dance')?.mods, { atkPct: 0.18, hpRegenRatio: 0.1 }, `${label(f)}: blocking`);
approx(u.s.hpRegen, 0.1 * u.s.maxHp, `${label(f)}: 10 % max HP / s as 生命回复速度`);
u.hp = u.s.maxHp / 2;
const hp0 = u.hp;
h.run(1);
approx(u.hp - hp0, 0.08 * u.s.maxHp, `${label(f)}: regenerated under 禁疗`, 0.05);
approx(u.hp - hp0, 0.1 * u.s.maxHp, `${label(f)}: regenerated under 禁疗`, 0.05);
done(h);
}
// S3 picked, before its cast: on the ground — no buff
@@ -254,7 +254,7 @@ test('S2 重力自定义 (MANUAL, data ACTIVE_RANGE on 3-10): takes off; 2.5 s
h.run(1.6);
assert.ok(!u.s.flags.disarm && !u.s.flags.invulnerable, `${label(f)}: the 吟唱 over at 2.5 s`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `${label(f)}: 3-10`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.13), `${label(f)}: ATK (+13 % of her T2)`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.18), `${label(f)}: ATK (+18 % of her T2)`);
approx(u.s.interval, (1.5 + sk.bb.base_attack_time) * 100 / u.s.aspd, `${label(f)}: interval`);
for (const p of [[9, 7], [11, 5], [9, 8], [11, 8]]) h.spawn('enemy_dummy', { pos: p });
const n0 = ANY_ATK(h, u).length;
@@ -285,7 +285,7 @@ test('S3 酸橙的心事 (MANUAL, data ACTIVE_RANGE on 3-9): 30 / 31 bullets, AT
const range = u.liveRangeGrid.map((p) => p.join(',')).sort();
for (const p of [...sk.rangeGrid, [1, -1], [0, -1], [-1, -1]]) assert.ok(range.includes(p.join(',')), `${label(f)}: ${p} in her range`);
h.run(0.25); // (her T2 refreshes every 0.2 s)
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.13), `${label(f)}: ATK`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.18), `${label(f)}: ATK`);
// 对空庇护
const flyer = h.spawn('enemy_fly', { pos: [12, 4] });
const taken = h.b.dealDamage(flyer, u, { amount: 1000, type: 'arts' });
+13 -13
View File
@@ -68,9 +68,9 @@ test('艾雅法拉 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1359 / 535 / 109, E2 Lv60 1614 / 608 / 118; CCR-X +130 / +40 → +180 / +65,
// CCR-Y +180 / +25 → +230 / +50 HP / ATK
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], [1359, 535, 1614, 608]);
// the numbers of the forms (zh_CN, full potential: ATK +27): E2 Lv1 1359 / 562 / 109, E2 Lv60 1614 / 635 / 118; CCR-X
// +130 / +40 → +180 / +65, CCR-Y +180 / +25 → +230 / +50 HP / ATK
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], [1359, 562, 1614, 635]);
assert.deepEqual([modOf(5, CCRX).attr, modOf(6, CCRX).attr, modOf(5, CCRY).attr, modOf(6, CCRY).attr], [{ maxHp: 130, atk: 40 }, { maxHp: 180, atk: 65 }, { maxHp: 180, atk: 25 }, { maxHp: 230, atk: 50 }]);
});
@@ -104,7 +104,7 @@ test('S1 二重咏唱 (MANUAL, data DEFAULT, 25 s): ASPD +38 / +45 on the first
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 5), `T${tier}: the second cast`);
assert.deepEqual(u.findBuff('skill:amgoat:duet')?.mods, { aspd: b, atkPct: atk }, `T${tier}: the second cast`);
approx(u.s.atk, u.base.atk * (1 + 0.14 + atk), `T${tier}: ATK with 炎息`);
approx(u.s.atk, u.base.atk * (1 + 0.16 + atk), `T${tier}: ATK with 炎息`);
h.b.retreat(u);
h.b.redeploy(u);
u.skill.gainSp(999);
@@ -218,7 +218,7 @@ test('S3 火山 (MANUAL, data ACTIVE_RANGE on x-3 — 「攻击范围增大」 i
done(h);
});
test('T1 炎息: every 【术师】 operator of the field ATK +14 % while she is deployed (惊蛰 yes, 德克萨斯 no); CCR-X stage 3 "携带时" +22 %, kept while she is knocked out', () => {
test('T1 炎息: every 【术师】 operator of the field ATK +16 % while she is deployed (惊蛰 yes, 德克萨斯 no); CCR-X stage 3 "携带时" +24 %, kept while she is knocked out', () => {
const others = [{ uid: 2, chessId: 'chess_char_1_03_a', row: 12, col: 3 }, { uid: 3, chessId: 'chess_char_1_08_a', row: 12, col: 8 }];
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -226,7 +226,7 @@ test('T1 炎息: every 【术师】 operator of the field ATK +14 % while she is
h.run(0.6);
const leizi = h.unit(2), texas = h.unit(3);
const carried = elite && tier === 6 && mod === CCRX;
const v = carried ? 0.22 : 0.14;
const v = carried ? 0.24 : 0.16;
const t0 = u.def.raw.talents.find((t) => t.index === 0);
assert.equal(t0.bb.atk, v, `${label(f)}: the data`);
assert.match(t0.desc, carried ? /^携带时/ : /^在场时/, `${label(f)}: the text`);
@@ -240,7 +240,7 @@ test('T1 炎息: every 【术师】 operator of the field ATK +14 % while she is
}
});
test('T2 乱火: 7–15 SP at every deployment (a floored draw); CCR-Y stage 3 also ASPD +6–15, both the maxima (15 / 15) with an elite or leader in range at the deployment; stage 1 and no module: no ASPD, no maximum rule', () => {
test('T2 乱火: 10–19 SP at every deployment (a floored draw); CCR-Y stage 3 also ASPD +6–15, both the maxima (19 / 15) with an elite or leader in range at the deployment; stage 1 and no module: no ASPD, no maximum rule', () => {
const seen = new Set(), seenAs = new Set();
for (let seed = 1; seed <= 40; seed++) {
const gifts = [];
@@ -250,7 +250,7 @@ test('T2 乱火: 7–15 SP at every deployment (a floored draw); CCR-Y stage 3 a
const sk = skillOf(6, true, S3);
const [sp, as] = gifts[0];
const k = sp - sk.initSp;
assert.ok(Number.isInteger(k) && k >= 7 && k <= 15, `seed ${seed}: +${k} SP`);
assert.ok(Number.isInteger(k) && k >= 10 && k <= 19, `seed ${seed}: +${k} SP`);
seen.add(k);
if (y3) { assert.ok(Number.isInteger(as) && as >= 6 && as <= 15, `seed ${seed}: ASPD +${as}`); seenAs.add(as); } else assert.equal(as, 0);
done(h);
@@ -270,10 +270,10 @@ test('T2 乱火: 7–15 SP at every deployment (a floored draw); CCR-Y stage 3 a
h.b.retreat(u);
h.b.redeploy(u);
const [sp, as] = gifts[1];
if (top) assert.deepEqual([sp - sk.initSp, as], [15, 15], `${lab} seed ${seed}: the maxima`);
if (top) assert.deepEqual([sp - sk.initSp, as], [19, 15], `${lab} seed ${seed}: the maxima`);
else {
assert.ok(sp - sk.initSp >= 7 && sp - sk.initSp <= 15, lab);
if (sp - sk.initSp === 15) maxed++;
assert.ok(sp - sk.initSp >= 10 && sp - sk.initSp <= 19, lab);
if (sp - sk.initSp === 19) maxed++;
if (mod !== CCRY || tier === 5) assert.equal(as, 0, `${lab}: no ASPD`);
}
done(h);
@@ -282,8 +282,8 @@ test('T2 乱火: 7–15 SP at every deployment (a floored draw); CCR-Y stage 3 a
}
// the talent's text and blackboard (stage 3 CCR-Y)
const t1 = formOf(6, true).modules.find((m) => m.uniEquipId === CCRY).talentChanges.find((t) => t.talentIndex === 1);
assert.deepEqual(t1.bb, { sp_min: 7, sp_max: 16, attack_speed_min: 6, attack_speed_max: 16, factor: 1 });
assert.match(t1.desc, /随机获得7~15点技力并随机提升6~15的攻击速度/);
assert.deepEqual(t1.bb, { sp_min: 10, sp_max: 20, attack_speed_min: 6, attack_speed_max: 16, factor: 1 });
assert.match(t1.desc, /随机获得10(\+3)~19(\+4)点技力并随机提升6~15的攻击速度/);
});
test('modules: CCR-X "无视目标10点法术抗性" at stages 1 and 3 (her normal attack on RES 50 deals × 0.6); CCR-Y "普通攻击命中精英或领袖敌人时获得1点技力" — not on a normal enemy, not from S2\'s explosion', () => {
+7 -7
View File
@@ -76,8 +76,8 @@ test('谬因 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// E2 Lv1 1261 / 727, E2 Lv60 1526 / 826, cost 34; BLA-Y −8 cost, +120 / +45 → +200 / +70 HP / ATK
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, FORMS['2/60/7/1'].stats.cost], [1261, 727, 1526, 826, 34]);
// E2 Lv1 1261 / 762, E2 Lv60 1526 / 861, cost 32 (full potential); BLA-Y −8 cost, +120 / +45 → +200 / +70 HP / ATK
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, FORMS['2/60/7/1'].stats.cost], [1261, 762, 1526, 861, 32]);
assert.deepEqual([modOf(5, BLAY).attr, modOf(6, BLAY).attr], [{ cost: -8, maxHp: 120, atk: 45 }, { cost: -8, maxHp: 200, atk: 70 }]);
});
@@ -138,13 +138,13 @@ test('the 中继器 (T1 链路协议): ATK +25 % (T6 BLA-Y +35 %) while it stand
}
});
test('T2 取样优化: every allied operator standing on her path — her own tile too — ignores 10 RES (法术穿透); none off it', () => {
test('T2 取样优化: every allied operator standing on her path — her own tile too — ignores 13 RES (法术穿透); none off it', () => {
for (const f of FORMS_ALL) {
const others = [{ uid: 3, chessId: 'chess_char_1_02_a', row: 10, col: 4 }, { uid: 4, chessId: 'chess_char_1_02_a', row: 9, col: 6 }];
const { h, u } = field({ tier: f[0], elite: f[1], mod: f[2], skill: 1, relay: UP4, others });
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.magic_resist_penetrate_fixed, 10, label(f));
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.magic_resist_penetrate_fixed, 13, label(f));
h.run(0.3);
assert.deepEqual([u.s.resIgnoreFlat, h.unit(3).s.resIgnoreFlat, h.unit(4).s.resIgnoreFlat], [10, 10, 0], `${label(f)}: herself, the one on the bent path, not the one on the straight line`);
assert.deepEqual([u.s.resIgnoreFlat, h.unit(3).s.resIgnoreFlat, h.unit(4).s.resIgnoreFlat], [13, 13, 0], `${label(f)}: herself, the one on the bent path, not the one on the straight line`);
done(h);
}
});
@@ -246,11 +246,11 @@ test('S3 混沌的本质 (MANUAL, data DEFAULT, 20 bullets): 3 s 强制缴械 fr
}
});
test('BLA-Y “第三相态” (stages 1 and 3): the 部署费用 attribute (34 → 26); stage 3 only: 35 s 中继器, its redeploy time 30 → 20 s (再部署时间变短), ATK +35 %', () => {
test('BLA-Y “第三相态” (stages 1 and 3): the 部署费用 attribute (32 → 24); stage 3 only: 35 s 中继器, its redeploy time 30 → 20 s (再部署时间变短), ATK +35 %', () => {
for (const tier of [5, 6]) {
for (const mod of [null, BLAY]) {
const { h, u, r } = field({ tier, elite: true, mod, relay: UP4 });
assert.equal(u.base.cost, mod ? 26 : 34, `T${tier} ${mod}: cost`);
assert.equal(u.base.cost, mod ? 24 : 32, `T${tier} ${mod}: cost`);
const y3 = tier === 6 && mod === BLAY;
assert.deepEqual(r.def.talents.map((t) => t.bb.duration), y3 ? [25, 35] : [25], `T${tier} ${mod}: the piece's talents`);
assert.equal(r.base.respawnTime, y3 ? 20 : 30, `T${tier} ${mod}: redeploy time`);
+3 -3
View File
@@ -159,14 +159,14 @@ test('T1 死亡拘审: every hit of her attacks adds a layer (≤ 3, the duratio
}
});
test('T2 噬光残影: ASPD +8; +6 more with 高台 (HIGHLAND) among the four tiles next to her — the ranged wall on her left, the forbidden rim below', () => {
test('T2 噬光残影: ASPD +10; +6 more with 高台 (HIGHLAND) among the four tiles next to her — the ranged wall on her left, the forbidden rim below', () => {
for (const [row, col, high] of [[10, 5, false], [10, 3, true], [12, 6, true], [11, 5, false]]) {
const { h, u } = field({ tier: 6, elite: true, skill: 1, row, col });
approx(u.s.aspd, 100 + 8 + (high ? 6 : 0), `(${row},${col}): ASPD`);
approx(u.s.aspd, 100 + 10 + (high ? 6 : 0), `(${row},${col}): ASPD`);
approx(u.s.interval, 3.5 * 100 / u.s.aspd, 'interval');
done(h);
}
assert.deepEqual(FORMS['2/1/4/0'].talents.find((t) => t.index === 1).bb, { attack_speed: 8, attack_speed_add: 6, cnt: 1 });
assert.deepEqual(FORMS['2/1/4/0'].talents.find((t) => t.index === 1).bb, { attack_speed: 10, attack_speed_add: 6, cnt: 1 });
});
test('S1 追袭 (AUTO, 2 / 3 charges, data DEFAULT): the next attack strikes every enemy of her range twice at 130 % / 170 % ATK — two 死亡拘审 layers', () => {
+3 -3
View File
@@ -280,20 +280,20 @@ test('“打字机”\'s skill is the copy of hers, free once per deployment: S1
}
});
test('弱点速记: every damage of the typewriter cuts the target\'s DEF 18 % for 4 s, 23 % on one of the 4 tiles next to 鸿雪 (not diagonal); ARC-X stage 3: 28 % / 33 % and the 8 tiles; 鸿雪 down ⇒ the base cut', () => {
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.28, 0.33, 4] : [0.18, 0.23, 4], label(f));
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, 4], `${label(f)} ${tile}: ${near ? 'near' : 'apart'}`);
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);
+7 -7
View File
@@ -72,8 +72,8 @@ test('刻俄柏 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// E2 Lv1 1220 / 553 / 114, E2 Lv60 1449 / 629 / 123; CCR-X +130 / +40 → +180 / +65, CCR-Y +80 / +40 / +17 → +120 / +56 / +23
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], [1220, 553, 1449, 629]);
// E2 Lv1 1220 / 580 / 114, E2 Lv60 1449 / 656 / 123; CCR-X +130 / +40 → +180 / +65, CCR-Y +80 / +40 / +17 → +120 / +56 / +23
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], [1220, 580, 1449, 656]);
assert.deepEqual([modOf(5, CCRX).attr, modOf(6, CCRX).attr, modOf(5, CCRY).attr, modOf(6, CCRY).attr], [{ maxHp: 130, atk: 40 }, { maxHp: 180, atk: 65 }, { maxHp: 80, atk: 40, def: 17 }, { maxHp: 120, atk: 56, def: 23 }]);
});
@@ -162,7 +162,7 @@ test('S3 “很重的枪” (MANUAL, data ACTIVE_RANGE on 3-3): 56 / 57 s, range
const sil = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'silence');
assert.ok(sil.length >= hits.length && sil.every((c) => c.target === lo), `T${tier}: silenced on every hit`);
approx(sil[0].duration, sk.bb['attack@silence'], `T${tier}: ${sk.bb['attack@silence']} s`);
// 剥壳 stays arts: DEF 100 × 40 %
// 剥壳 stays arts: DEF 100 × 44 %
const shells = hitsBy(h, u).slice(n0).filter(shellOf);
assert.ok(shells.length >= hits.length - 1 && shells.every((c) => c.type === 'arts'), `T${tier}: 剥壳 arts`);
u.skill.end('test');
@@ -173,13 +173,13 @@ test('S3 “很重的枪” (MANUAL, data ACTIVE_RANGE on 3-3): 56 / 57 s, range
}
});
test('T1 剥壳: each hit of her attacks adds DEF × 40 % arts (none on DEF 0); CCR-X stage 3: 50 % +5 % per hit on the same target up to 75 %, back to 50 % on another; stage 1 keeps 40 %', () => {
test('T1 剥壳: each hit of her attacks adds DEF × 44 % arts (none on DEF 0); CCR-X stage 3: 54 % +5 % per hit on the same target up to 79 %, back to 54 % on another; stage 1 keeps 44 %', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 1 });
const x3 = elite && tier === 6 && mod === CCRX;
const t0 = u.def.raw.talents.find((t) => t.index === 0);
assert.equal(t0.bb.atk_scale, x3 ? 0.5 : 0.4, `${label(f)}: the data`);
assert.equal(t0.bb.atk_scale, x3 ? 0.54 : 0.44, `${label(f)}: the data`);
u.skill.charges = 0;
const a = h.spawn('enemy_d400', { pos: [10, 6] });
const n0 = hitsBy(h, u).length;
@@ -187,7 +187,7 @@ test('T1 剥壳: each hit of her attacks adds DEF × 40 % arts (none on DEF 0);
const shells = hitsBy(h, u).slice(n0).filter((c) => shellOf(c) && c.target === a);
const plain = hitsBy(h, u).slice(n0).filter((c) => c.dmg.isAttack && c.target === a);
assert.ok(plain.length >= 7 && shells.length === plain.length, `${label(f)}: one 剥壳 per hit (${shells.length} / ${plain.length})`);
const want = x3 ? [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.75] : Array(7).fill(0.4);
const want = x3 ? [0.54, 0.59, 0.64, 0.69, 0.74, 0.79, 0.79] : Array(7).fill(0.44);
shells.slice(0, 7).forEach((c, i) => {
approx(c.amount, 400 * want[i], `${label(f)}: hit ${i + 1}`);
assert.deepEqual([c.type, !!c.dmg.isAttack], ['arts', false], `${label(f)}: a talent arts instance`);
@@ -199,7 +199,7 @@ test('T1 剥壳: each hit of her attacks adds DEF × 40 % arts (none on DEF 0);
h.run(1.7);
const sb = hitsBy(h, u).slice(n1).filter((c) => shellOf(c) && c.target === b);
assert.ok(sb.length >= 1);
approx(sb[0].amount, 200 * (x3 ? 0.5 : 0.4), `${label(f)}: a new target — the base ratio`);
approx(sb[0].amount, 200 * (x3 ? 0.54 : 0.44), `${label(f)}: a new target — the base ratio`);
h.b.kill(b, null);
const z = h.spawn('enemy_dummy', { pos: [10, 6] });
const n2 = hitsBy(h, u).length;
+5 -5
View File
@@ -193,8 +193,8 @@ test('S3 圣域 (MANUAL, data ACTIVE_RANGE on y-4): cast for an injured ally ins
for (const uid of [2, 6, 7]) assert.deepEqual(a(uid).findBuff('skill:cgbird:sanctuary')?.mods, { resMul: 1 + sk.bb.magic_resistance, dodgeArts: sk.bb.prob }, `${label(f)}: uid ${uid} in y-4`);
assert.deepEqual(u.findBuff('skill:cgbird:sanctuary')?.mods, { resMul: 1 + sk.bb.magic_resistance, dodgeArts: sk.bb.prob }, `${label(f)}: herself`);
assert.equal(a(8).findBuff('skill:cgbird:sanctuary'), null, `${label(f)}: outside`);
// RES = (10 + 15 talent) × (1 + v) on an ally (base 10)
approx(a(7).s.res, Math.min(100, (10 + 15) * (1 + sk.bb.magic_resistance)), `${label(f)}: RES`);
// RES = (10 + 17 talent) × (1 + v) on an ally (base 10)
approx(a(7).s.res, Math.min(100, (10 + 17) * (1 + sk.bb.magic_resistance)), `${label(f)}: RES`);
approx(a(7).s.dodgeArts, sk.bb.prob, `${label(f)}: arts dodge`);
u.skill.end('test');
h.run(0.3);
@@ -204,18 +204,18 @@ test('S3 圣域 (MANUAL, data ACTIVE_RANGE on y-4): cast for an injured ally ins
}
});
test('T1 白恶魔的庇护: allies in her range RES +15 (herself too, not outside); RIN-X: its range tile too, stage 3 also 受到的治疗效果 ×1.05', () => {
test('T1 白恶魔的庇护: allies in her range RES +17 (herself too, not outside); RIN-X: its range tile too, stage 3 also 受到的治疗效果 ×1.05', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u, a } = field({ tier, elite, mod, skill: 0 });
h.run(0.3);
const rinx = elite && mod === RINX, x3 = rinx && tier === 6;
const want = x3 ? { resFlat: 15, healingTakenMul: 1.05 } : { resFlat: 15 };
const want = x3 ? { resFlat: 17, healingTakenMul: 1.05 } : { resFlat: 17 };
for (const uid of [2, 3, 4, 5]) assert.deepEqual(a(uid).findBuff('talent:cgbird:res')?.mods, want, `${label(f)}: uid ${uid}`);
assert.deepEqual(u.findBuff('talent:cgbird:res')?.mods, want, `${label(f)}: herself`);
assert.deepEqual(a(6).findBuff('talent:cgbird:res')?.mods ?? null, rinx ? want : null, `${label(f)}: (11,5) — the RIN-X tile`);
for (const uid of [7, 8]) assert.equal(a(uid).findBuff('talent:cgbird:res'), null, `${label(f)}: uid ${uid} outside`);
approx(a(2).s.res, 10 + 15, `${label(f)}: RES`);
approx(a(2).s.res, 10 + 17, `${label(f)}: RES`);
if (x3) {
a(2).hp = 1000;
const n0 = h.hooksOf('heal').length;
+4 -4
View File
@@ -70,9 +70,9 @@ test('陈 in every 自选 form: her operator kit (all three skills authored), th
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2188 / 469 / 288, E2 Lv60 2647 / 562 / 330; SWO-X +50 ATK +5 ASPD → +80 / +7,
// the numbers of the forms (zh_CN, full potential): E2 Lv1 2188 / 492 / 288, E2 Lv60 2647 / 585 / 330; SWO-X +50 ATK +5 ASPD → +80 / +7,
// SWO-Y +52 / +28 → +85 / +42 ATK / 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], [2188, 469, 2647, 562]);
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], [2188, 492, 2647, 585]);
assert.deepEqual([modOf(5, SWOX).attr, modOf(6, SWOX).attr, modOf(5, SWOY).attr, modOf(6, SWOY).attr], [{ atk: 50, aspd: 5 }, { atk: 80, aspd: 7 }, { atk: 52, def: 28 }, { atk: 85, def: 42 }]);
});
@@ -280,12 +280,12 @@ test('T1 呵斥: every 4 s on the field (3 s with SWO-X stage 3) every attack /
assert.match(modOf(6, SWOX).talentChanges.find((t) => !t.hidden).desc, /每3秒回复全场友方角色1点攻击\/受击技力,自身额外回复1点技力/);
});
test('T2 持刀格斗术: ATK +5 %, DEF +5 %, 物理闪避 10 %; SWO-Y stage 3: +15 % / +15 % / 18 %', () => {
test('T2 持刀格斗术: ATK +6 %, DEF +6 %, 物理闪避 13 %; SWO-Y stage 3: +16 % / +16 % / 21 %', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 1 });
const y3 = elite && mod === SWOY && tier === 6;
const [a, d, p] = y3 ? [0.15, 0.15, 0.18] : [0.05, 0.05, 0.1];
const [a, d, p] = y3 ? [0.16, 0.16, 0.21] : [0.06, 0.06, 0.13];
assert.deepEqual(u.findBuff('talent:chen:knife')?.mods, { atkPct: a, defPct: d, dodgePhys: p }, label(f));
approx(u.s.atk, u.base.atk * (1 + a), `${label(f)}: ATK`);
approx(u.s.def, u.base.def * (1 + d), `${label(f)}: DEF`);
+10 -9
View File
@@ -75,7 +75,7 @@ test('假日威龙陈 in every 自选 form: her kit (all three skills authored),
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'ACTIVE_RANGE']);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk, FORMS['2/1/4/0'].stats.cost], [649, 731, 32]);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk, FORMS['2/1/4/0'].stats.cost], [682, 764, 30]);
assert.deepEqual([modOf(5, RX).attr, modOf(6, RX).attr, modOf(5, RY).attr, modOf(6, RY).attr],
[{ atk: 30, def: 37 }, { atk: 60, def: 45 }, { cost: -8, maxHp: 135, atk: 55 }, { cost: -8, maxHp: 180, atk: 100 }]);
const data = { chess: CHESS, backups: BACKUPS };
@@ -259,8 +259,8 @@ test('S3 “假日风暴” (MANUAL, data ACTIVE_RANGE on 2-6): an enemy only on
}
});
test('T1 节约风气: her ammo-skill attacks spare their bullets 20 % of the time (RPR-X stage 3: 25 %) — one roll per attack, S3 spares both bullets, every ammoUsed still fires', () => {
for (const [tier, elite, mod, p] of [[5, false, null, 0.2], [5, true, RX, 0.2], [6, true, RX, 0.25], [6, true, RY, 0.2]]) {
test('T1 节约风气: her ammo-skill attacks spare their bullets 22 % of the time (RPR-X stage 3: 27 %) — one roll per attack, S3 spares both bullets, every ammoUsed still fires', () => {
for (const [tier, elite, mod, p] of [[5, false, null, 0.22], [5, true, RX, 0.22], [6, true, RX, 0.27], [6, true, RY, 0.22]]) {
for (const skill of [1, 2]) {
const { h, u } = field({ tier, elite, mod, skill, seed: 17 });
h.spawn('enemy_dummy', { pos: [10, 5] });
@@ -287,11 +287,11 @@ test('T1 节约风气: her ammo-skill attacks spare their bullets 20 % of the ti
}
});
test('T1 节约风气 on others while she is on the field: another 【狙击】 operator\'s 弹药类技能 20 % (PRTS 修正 over the text\'s 10 %; RPR-X stage 3 too), another 远程 operator\'s 0 % / 12 % (RPR-X stage 3), never a 技能 without "攻击装有X发" nor a summon; none once she leaves; the highest of two 假日威龙陈', () => {
test('T1 节约风气 on others while she is on the field: another 【狙击】 operator\'s 弹药类技能 22 %, her own (PRTS 修正 over the text\'s 10 %; RPR-X stage 3: 20 %), another 远程 operator\'s 0 % / 12 % (RPR-X stage 3), never a 技能 without "攻击装有X发" nor a summon; none once she leaves; the highest of two 假日威龙陈', () => {
const t0 = FORMS['2/1/4/0'].talents.find((t) => t.index === 0);
assert.deepEqual(t0.bb, { 'spareshot_chen.prob': 0.2, prob: 0.1 }, 'the base row: 20 % / the text\'s 10 %');
assert.deepEqual(modOf(6, RX).talentChanges[0].bb, { 'e_spareshot_chen.prob': 0.25, 'chen2_t_002[spareshot][other].prob': 0.12, 'chen2_t_002[spareshot][sniper].prob': 0.2 });
for (const [tier, elite, mod, sniper, other] of [[5, false, null, 0.2, 0], [5, true, RX, 0.2, 0], [6, true, RX, 0.2, 0.12], [6, true, RY, 0.2, 0]]) {
assert.deepEqual(t0.bb, { 'spareshot_chen.prob': 0.22, prob: 0.1 }, 'the base row: 22 % / the text\'s 10 %');
assert.deepEqual(modOf(6, RX).talentChanges[0].bb, { 'e_spareshot_chen.prob': 0.27, 'chen2_t_002[spareshot][other].prob': 0.12, 'chen2_t_002[spareshot][sniper].prob': 0.2 });
for (const [tier, elite, mod, sniper, other] of [[5, false, null, 0.22, 0], [5, true, RX, 0.22, 0], [6, true, RX, 0.2, 0.12], [6, true, RY, 0.22, 0]]) {
const others = [
{ uid: 2, chessId: 'chess_char_1_01_a', row: 11, col: 3 }, // 隐现 (狙击): S2 "攻击装有14发弹药"
{ uid: 3, chessId: 'chess_char_5_03_a', row: 12, col: 3 }, // 烛煌 (术师, 远程): S3 "攻击装有18发弹药"
@@ -326,7 +326,8 @@ test('T1 节约风气 on others while she is on the field: another 【狙击】
assert.deepEqual([spareChance(h.b, inside), spareChance(h.b, blaze)], [0, 0], 'only while she is on the field');
done(h);
}
// two 假日威龙陈 (a shared field): each takes the highest — the RPR-X stage-3 one gives the other 20 % (≥ her own 20 %)
// two 假日威龙陈 (a shared field): each takes the highest — the RPR-X stage-3 one gives the other 20 % (below her own 22 %,
// which she keeps), the other gives her 22 % (below her own 27 %)
const h = makeBattle({
defs: { enemies: ENEMIES }, timeLimit: 60, autoFinish: false, seed: 3, flags: { dpPerSec: 0 },
units: [{ uid: 1, diy: { slot: 6, charId: CHEN, skillIndex: 0, uniEquipId: RX }, elite: true, row: 10, col: 4 },
@@ -334,7 +335,7 @@ test('T1 节约风气 on others while she is on the field: another 【狙击】
{ uid: 3, chessId: 'chess_char_5_03_a', row: 12, col: 3 }],
});
h.step();
assert.deepEqual([spareChance(h.b, h.unit(1)), spareChance(h.b, h.unit(2)), spareChance(h.b, h.unit(3))], [0.25, 0.2, 0.12]);
assert.deepEqual([spareChance(h.b, h.unit(1)), spareChance(h.b, h.unit(2)), spareChance(h.b, h.unit(3))], [0.27, 0.22, 0.12]);
done(h);
});
+13 -13
View File
@@ -80,8 +80,8 @@ test('赤刃明霄陈 in every 自选 form: her operator kit (all three skills a
done(h);
}
}
// E2 Lv1 2211 / 542 / 352, E2 Lv60 2674 / 627 / 400; AFT-X +240 / +30 → +420 / +60 (HP / ATK)
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], [2211, 542, 2674, 627]);
// E2 Lv1 2211 / 570 / 352, E2 Lv60 2674 / 655 / 400; AFT-X +240 / +30 → +420 / +60 (HP / ATK)
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], [2211, 570, 2674, 655]);
assert.deepEqual([modOf(5, AFTX).attr, modOf(6, AFTX).attr], [{ maxHp: 240, atk: 30 }, { maxHp: 420, atk: 60 }]);
});
@@ -93,15 +93,15 @@ test('a 自选 pick: 赤刃明霄陈 is offered at tiers 5 and 6 (she has a kit)
{ ok: true, picks: { [SLOT[5]]: { charId: CHEN3, skillIndex: 1, uniEquipId: AFTX } } });
});
test('T1 形意洞照: ATK +13 %, ASPD +13; every physical / arts damage of hers is 弱点伤害 — physical on a high-RES enemy, arts on a high-DEF one', () => {
test('T1 形意洞照: ATK +16 %, ASPD +16; every physical / arts damage of hers is 弱点伤害 — physical on a high-RES enemy, arts on a high-DEF one', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t0 = tOf(tier, elite, mod, 0);
assert.deepEqual([t0.atk, t0.attack_speed], [0.13, 13]);
assert.deepEqual([t0.atk, t0.attack_speed], [0.16, 16]);
const { h, u } = field({ tier, elite, mod, skill: 0 });
approx(u.s.atk, u.base.atk * 1.13, `${label(f)}: ATK`);
approx(u.s.atk, u.base.atk * 1.16, `${label(f)}: ATK`);
const as = elite && mod === AFTX ? 8 : 0; // she blocks nobody yet: AFT-X's +8 on top
assert.equal(u.s.aspd, 100 + 13 + as, `${label(f)}: ASPD`);
assert.equal(u.s.aspd, 100 + 16 + as, `${label(f)}: ASPD`);
u.skill.sp = 0;
const mage = h.spawn('enemy_mage', { pos: [10, 6] });
h.runUntil(() => from(h, u, (c) => c.target === mage).length > 0, 3);
@@ -129,17 +129,17 @@ test('AFT-X “记忆残页” (stages 1 / 3): ASPD +8 while she blocks nobody
assert.ok(h.runUntil(() => u.blocking.length > 0, 30), `${label(f)}: the walker reaches her`);
h.step();
assert.equal(u.findBuff('trait:chen3:unblocked'), null, `${label(f)}: blocking`);
assert.equal(u.s.aspd, 113, `${label(f)}: ASPD while blocking`);
assert.equal(u.s.aspd, 116, `${label(f)}: ASPD while blocking`);
done(h);
}
});
test('T2 寒暑觉知: every 7 s (AFT-X stage 3: 6 s) without damage a self-heal of ATK × a whole random percent of [30, 161) ([50, 201)) and one held dodge; any damage restarts the count', () => {
test('T2 寒暑觉知: every 7 s (AFT-X stage 3: 6 s) without damage a self-heal of ATK × a whole random percent of [35, 166) ([55, 206)) and one held dodge; any damage restarts the count', () => {
for (const f of [[5, false, null], [6, true, null], [5, true, AFTX], [6, true, AFTX]]) {
const [tier, elite, mod] = f;
const t1 = tOf(tier, elite, mod, 1);
const st = t1.stack_time, lo = t1.heal_atk_scale_min, hi = t1.heal_atk_scale_max;
assert.deepEqual([st, lo, hi], elite && mod === AFTX && tier === 6 ? [6, 50, 201] : [7, 30, 161], label(f));
assert.deepEqual([st, lo, hi], elite && mod === AFTX && tier === 6 ? [6, 55, 206] : [7, 35, 166], label(f));
const { h, u } = field({ tier, elite, mod, skill: 0 });
u.hp = 100;
const heals = () => h.hooksOf('heal').filter((c) => c.target === u && c.source === u && !c.opts?.regen);
@@ -196,7 +196,7 @@ test('S1 赤霄·奔夜 (MANUAL, data DEFAULT): ATK +65 % / +80 % for 18 s, ever
u.skill.gainSp(999);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast`);
approx(u.s.atk, u.base.atk * (1 + 0.13 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.atk, u.base.atk * (1 + 0.16 + sk.bb.atk), `T${tier}: ATK`);
const n0 = from(h, u).length;
h.runUntil(() => from(h, u).length >= n0 + 2, 3);
const two = from(h, u).slice(n0, n0 + 2);
@@ -246,7 +246,7 @@ test('S2 赤霄·绝影-驰 (MANUAL, 技能范围 x-1, data SKILL_RANGE): 10 sla
assert.ok(cuts.every((c) => c.target === near), `T${tier}: on the nearest`);
cuts.forEach((c, i) => approx(c.t - cuts[0].t, 0.4 * i, `T${tier}: slash ${i + 1}`, 0.04));
approx(cuts[0].t, t0, `T${tier}: the first at the cast`, 0.05);
for (const c of cuts) { approx(c.amount, u.base.atk * 1.13 * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`); assert.deepEqual([c.type, c.dmg.isSkill], ['arts', true]); }
for (const c of cuts) { approx(c.amount, u.base.atk * 1.16 * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} % ATK`); assert.deepEqual([c.type, c.dmg.isSkill], ['arts', true]); }
// the move onto the target's tile, the 6 s state
const moves = h.hooksOf('deploy').filter((c) => c.unit === u && c.move);
assert.equal(moves.length, 1, `T${tier}: one 【移动】`);
@@ -254,12 +254,12 @@ test('S2 赤霄·绝影-驰 (MANUAL, 技能范围 x-1, data SKILL_RANGE): 10 sla
assert.equal(from(h, u, (c) => c.dmg.isAttack && c.t < moves[0].t).length, 0, `T${tier}: no attack while slashing`);
assert.deepEqual([u.tileR, u.tileC], [11, 6], `T${tier}: moved onto its tile`);
assert.ok(!u.s.flags.invulnerable && !u.s.flags.noBlock, `T${tier}: the slashing is over`);
approx(u.s.atk, u.base.atk * (1 + 0.13 + sk.bb['chen3_s2[respawn_buff].atk']), `T${tier}: ATK`);
approx(u.s.atk, u.base.atk * (1 + 0.16 + sk.bb['chen3_s2[respawn_buff].atk']), `T${tier}: ATK`);
approx(u.s.dodgePhys, sk.bb['chen3_s2[respawn_buff].prob'], `T${tier}: physical dodge`);
approx(u.s.dodgeArts, sk.bb['chen3_s2[respawn_buff].prob'], `T${tier}: arts dodge`);
approx(u.skill.timeLeft, 6, `T${tier}: 6 s from the move`, 0.05);
h.runUntil(() => !u.skill.active, 7);
approx(u.s.atk, u.base.atk * 1.13, `T${tier}: back`);
approx(u.s.atk, u.base.atk * 1.16, `T${tier}: back`);
assert.equal(from(h, u, (c) => tagged(c, 'chen3:slash') && c.target === far).length, 0, `T${tier}: the farther one is never slashed`);
done(h);
}
+14 -12
View File
@@ -75,9 +75,9 @@ test('重岳 in every 自选 form: his operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2160 / 477 / 304, E2 Lv60 2475 / 552 / 343; FGT-X +210 / +25 / +20 →
// the numbers of the forms (zh_CN, full potential): E2 Lv1 2160 / 501 / 304, E2 Lv60 2475 / 576 / 343; FGT-X +210 / +25 / +20 →
// +260 / +45 / +30, FGT-Y +52 / +25 → +75 / +35 ATK / 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], [2160, 477, 2475, 552]);
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], [2160, 501, 2475, 576]);
assert.deepEqual([modOf(5, FGTX).attr, modOf(6, FGTX).attr, modOf(5, FGTY).attr, modOf(6, FGTY).attr],
[{ maxHp: 210, atk: 25, def: 20 }, { maxHp: 260, atk: 45, def: 30 }, { atk: 52, def: 25 }, { atk: 75, def: 35 }]);
assert.deepEqual([modOf(5, FGTX).traitOverride.bb, modOf(6, FGTY).traitOverride.bb], [{ prob: 0.15 }, { attack_speed: 10, hp_ratio: 0.5 }]);
@@ -109,7 +109,7 @@ test('S1 冲盈 (MANUAL, attack SP 5 / 4, 3 charges, data DEFAULT): 250 % / 290
for (const c of strike) {
assert.equal(c.target, e, `T${tier}: the ground target`);
approx(c.dmg.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} %`);
assert.ok(c.dmg.mul === 1 || Math.abs(c.dmg.mul - 1.65) < 1e-9, `T${tier}: ×1 or ×1.65 (止戈)`);
assert.ok(c.dmg.mul === 1 || Math.abs(c.dmg.mul - 1.7) < 1e-9, `T${tier}: ×1 or ×1.7 (止戈)`);
approx(c.amount, c.dmg.amount * c.dmg.mul, `T${tier}: physical, DEF 0`);
}
assert.deepEqual([u.skill.charges, u.skill.sp], [0, 0], `T${tier}: every charge spent`);
@@ -125,7 +125,7 @@ test('S1 冲盈 (MANUAL, attack SP 5 / 4, 3 charges, data DEFAULT): 250 % / 290
}
});
test('S1 冲盈: exact damage — 290 % ATK per strike, ×1.65 on a 止戈-marked target; the strike rolls no 止戈 and gives no attack SP', () => {
test('S1 冲盈: exact damage — 290 % ATK per strike, ×1.7 on a 止戈-marked target; the strike rolls no 止戈 and gives no attack SP', () => {
const { h, u } = field({ tier: 6, elite: true, skill: 0 });
const sk = skillOf(6, true, S1);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
@@ -141,7 +141,7 @@ test('S1 冲盈: exact damage — 290 % ATK per strike, ×1.65 on a 止戈-marke
assert.ok(h.runUntil(() => u.skill.activations === 2, 4));
h.step();
const c2 = hitsBy(h, u).filter((x) => x.dmg.isSkill)[1];
approx(c2.amount, u.s.atk * sk.bb.atk_scale * 1.65, '×1.65 on the marked target');
approx(c2.amount, u.s.atk * sk.bb.atk_scale * 1.7, '×1.7 on the marked target');
done(h);
});
@@ -172,7 +172,7 @@ test('S2 拂尘 (MANUAL, 2 charges, data SKILL_RANGE x-4): ≤ 4 enemies around
assert.equal(first.length, 4, `T${tier}: four targets`);
assert.deepEqual([m1, m2, flyM, blocked].map((e) => hitSet.has(e)), [true, true, true, true], `T${tier}: the marked (flyer included), then the blocked one`);
assert.ok(![plain1, plain2, outside].some((e) => hitSet.has(e)), `T${tier}: no room for the rest`);
for (const c of first) approx(c.amount, u.s.atk * sk.bb.atk_scale * (markOf(u, c.target) ? 1.65 : 1), `T${tier}: ${sk.bb.atk_scale * 100} %`);
for (const c of first) approx(c.amount, u.s.atk * sk.bb.atk_scale * (markOf(u, c.target) ? 1.7 : 1), `T${tier}: ${sk.bb.atk_scale * 100} %`);
assert.deepEqual([m1, m2, flyM, blocked].map((e) => !!e.s.flags.levitate), [true, true, false, false], `T${tier}: the marked ground ones 浮空 (not a flyer, not the unmarked)`);
approx(m1.findBuff('levitate').timeLeft, 2, `T${tier}: 2 s`, 0.05);
assert.equal(blocked.blockedBy, u, 'still blocked');
@@ -182,7 +182,7 @@ test('S2 拂尘 (MANUAL, 2 charges, data SKILL_RANGE x-4): ≤ 4 enemies around
h.step();
const second = hitsBy(h, u).filter((c) => c.dmg.isSkill).slice(4);
assert.deepEqual(new Set(second.map((c) => c.target)), new Set([m1, m2, lifted]), `T${tier}: every 浮空 enemy around (the other source's too)`);
for (const c of second) approx(c.amount, u.s.atk * sk.bb.atk_scale_down * (c.target === lifted ? 1 : 1.65), `T${tier}: ${sk.bb.atk_scale_down * 100} % (its own mark comes after the damage)`);
for (const c of second) approx(c.amount, u.s.atk * sk.bb.atk_scale_down * (c.target === lifted ? 1 : 1.7), `T${tier}: ${sk.bb.atk_scale_down * 100} % (its own mark comes after the damage)`);
for (const e of [m1, m2, lifted]) {
assert.ok(!e.s.flags.levitate, `T${tier}: 浮空 over`);
approx(markOf(u, e).timeLeft, 2.5, `T${tier}: marked 2.5 s`, 0.1);
@@ -212,7 +212,7 @@ test('S3 我无 (MANUAL, attack SP 11 / 10, data DEFAULT): its strike hits the t
assert.deepEqual(new Set(pre.map((c) => c.target)), new Set([e, near]), `T${tier}: not the flyer, not the one 1.4 away`);
for (const c of pre) {
approx(c.dmg.amount, u.s.atk * sk.bb.atk_scale, `T${tier}: ${sk.bb.atk_scale * 100} %`);
approx(c.amount, c.dmg.amount * c.dmg.mul, `T${tier}: physical (×1.65 on a marked one)`);
approx(c.amount, c.dmg.amount * c.dmg.mul, `T${tier}: physical (×1.7 on a marked one)`);
}
assert.deepEqual(u.liveRangeGrid, X6, `T${tier}: x-6`);
assert.ok(!hitsBy(h, u).some((c) => c.target === fly || c.target === far), `T${tier}: never the flyer / far one before`);
@@ -247,13 +247,15 @@ test('S3 我无 (MANUAL, attack SP 11 / 10, data DEFAULT): its strike hits the t
}
});
test('T1 止戈: each normal-attack damage instance rolls 23 % to mark its target for 2.5 s (not that instance), every damage of his to a marked target ×1.65 — FGT-Y stage 3 ×1.75 (stage 1 ×1.65)', () => {
test('T1 止戈: each normal-attack damage instance rolls 25 % to mark its target for 2.5 s (not that instance), every damage of his to a marked target ×1.7 — FGT-Y stage 3 ×1.8 at 23 % (stage 1 ×1.7)', () => {
for (const f of [[5, false, null], [5, true, FGTY], [6, true, FGTY]]) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 21 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
const scale = tier === 6 && mod === FGTY ? 1.75 : 1.65;
assert.deepEqual([t0.prob, t0.damage_scale, t0.up_duration], [0.23, scale, 2.5], label(f));
// full potential: 25 % / ×1.7; FGT-Y stage 3: ×1.8, its blackboard keeps prob 0.23 (its text reads 25%(+2%))
const y3 = tier === 6 && mod === FGTY;
const [prob, scale] = y3 ? [0.23, 1.8] : [0.25, 1.7];
assert.deepEqual([t0.prob, t0.damage_scale, t0.up_duration], [prob, scale, 2.5], label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
let marked = 0, plainHits = 0, prevMarked = false;
h.b.on('hit', (c) => { if (c.source === u && c.target === e && c.dmg.isAttack && !c.dmg.isSkill) prevMarked = !!markOf(u, e); }, { priority: 100 });
@@ -268,7 +270,7 @@ test('T1 止戈: each normal-attack damage instance rolls 23 % to mark its targe
h.run(400);
const unmarkedHits = amounts.filter(([, m]) => !m).length;
const rate = marked / unmarkedHits;
assert.ok(rate > 0.18 && rate < 0.28, `${label(f)}: ${marked} / ${unmarkedHits} ≈ 23 %`);
assert.ok(rate > prob - 0.05 && rate < prob + 0.05, `${label(f)}: ${marked} / ${unmarkedHits} ≈ ${prob * 100} %`);
for (const [amt, m] of amounts.slice(0, 50)) approx(amt, u.s.atk * (m ? scale : 1), `${label(f)}: ×${scale} while marked`);
assert.ok(plainHits > 400);
done(h);
+5 -4
View File
@@ -223,7 +223,7 @@ test('S2 模型扩展: +cost DP at once and 15 one per 2 s; ATK +atk, 2 targets;
const atks = h.hooksOf('attack').slice(n0).filter((c) => c.attacker === u);
assert.ok(atks.length >= 2 && atks.every((c) => c.targets.length === 2), `${label(f)}: 2 targets per attack (${atks.map((c) => c.targets.length)})`);
for (const e of es) h.b.kill(e, null);
// the refund: 角峰 (cost 19) redeployed on the x-4 ⇒ +8; 波登可 outside ⇒ nothing; a 【移动】 ⇒ nothing
// the refund: 角峰 (cost 17) redeployed on the x-4 ⇒ +7; 波登可 outside ⇒ nothing; a 【移动】 ⇒ nothing
h.step();
const d0 = dpOf(h);
h.b.kill(yak, null);
@@ -257,7 +257,7 @@ test('S2 模型扩展: +cost DP at once and 15 one per 2 s; ATK +atk, 2 targets;
done(h);
});
test('S3 Q.E.D.: 18 DP one per 1.66 s; attack interval 1.0 − 0.5 s; hits of attack@atk_scale × ATK with one 迟钝 stack each (4 % / 5 %, ≤ 10 stacks, one 3 s timer); one more target per 9 attacks (≤ 6); her range adds the 指挥中心\'s x-6 and her 援军\'s ranges', () => {
test('S3 Q.E.D.: 18 DP one per 1.66 s; attack interval (1.0 − 0.5 s) / ASPD 1.08 (full potential); hits of attack@atk_scale × ATK with one 迟钝 stack each (4 % / 5 %, ≤ 10 stacks, one 3 s timer); one more target per 9 attacks (≤ 6); her range adds the 指挥中心\'s x-6 and her 援军\'s ranges', () => {
for (const f of [[5, false, null], [6, true, TX]]) {
const [tier, elite, mod] = f;
const sk = skillOf(tier, elite, S3);
@@ -272,7 +272,8 @@ test('S3 Q.E.D.: 18 DP one per 1.66 s; attack interval 1.0 − 0.5 s; hits of at
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
const t0 = h.b.time;
approx(u.s.interval, 0.5, `${label(f)}: interval 0.5 s`);
assert.equal(formOf(tier, elite).stats.aspd, 108, `${label(f)}: ASPD 108 (潜能 攻速 +8)`);
approx(u.s.interval, 0.5 / 1.08, `${label(f)}: interval 0.5 s / 1.08 (ASPD 108)`);
let attacks = 0;
const perAttack = [];
h.b.on('attack', (c) => { if (c.attacker === u && u.skill.active) { attacks++; perAttack.push(c.targets.length); } });
@@ -317,7 +318,7 @@ test('T2 极限调度: from the battle start the 【罗德岛】 operators of he
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 1).bb;
assert.deepEqual(tb, tier === 6 && elite && mod === TX ? { cost: -5, atk: 0.08 } : { cost: -3, atk: 0.04 }, label(f));
assert.deepEqual(tb, tier === 6 && elite && mod === TX ? { cost: -6, atk: 0.08 } : { cost: -4, atk: 0.04 }, label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 3, chessId: PODEGO, row: 12, col: 3 }, { uid: 4, chessId: YAK, row: 11, col: 3 }] });
const podego = h.unit(3), yak = h.unit(4);
assert.equal(podego.def.raw.nationId, 'rhodes');
+6 -6
View File
@@ -82,7 +82,7 @@ test('W in every 自选 form: her kit (all three skills authored), the form\'s s
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => [s.skillType, s.trigger.rule]), [['MANUAL', 'DEFAULT'], ['AUTO', 'DEFAULT'], ['MANUAL', 'DEFAULT']]);
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], [1284, 711, 1497, 844]);
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], [1284, 746, 1497, 879]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr], [{ maxHp: 130, atk: 54 }, { maxHp: 210, atk: 78 }, { maxHp: 160, atk: 45 }, { maxHp: 200, atk: 85 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(W) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(W)));
@@ -244,10 +244,10 @@ test('S3 D12 (MANUAL, data DEFAULT, 42 / 39 SP from 14 / 17): bombs on the 3 ene
assert.ok(victims.has(side), `T${tier}: and what stands within 1.2 of a bomb`);
assert.ok(!victims.has(lo) && !victims.has(e3), `T${tier}: the others untouched`);
// the ATK cached at the cast (her +100 % since then counts for nothing); a victim of her range that an earlier bomb
// stunned takes 落井下石's ×1.18 on top, the one outside her range never
// stunned takes 落井下石's ×1.21 on top, the one outside her range never
const base = atk * sk.bb.atk_scale;
for (const c of blasts) {
const stunnedBefore = c.target !== side && Math.abs(c.amount - base * 1.18) < 1e-6 * base;
const stunnedBefore = c.target !== side && Math.abs(c.amount - base * 1.21) < 1e-6 * base;
assert.ok(Math.abs(c.amount - base) < 1e-6 * base || stunnedBefore, `T${tier}: the cached ATK × ${sk.bb.atk_scale} (${c.amount} vs ${base})`);
assert.deepEqual([c.type, c.source, c.credit, !!c.dmg.sourceless], ['phys', null, u, true], `T${tier}: 无来源, credited to her`);
}
@@ -331,7 +331,7 @@ test('T1 设伏: 10 s after each deployment 60 % physical and arts dodge, taunt
done(h);
});
test('T2 落井下石: a stunned enemy in her range takes ×1.18 physical damage from anyone (×1.24 with ART-X stage 3); not arts, not unstunned, not frozen, not outside her range; two W keep the strongest', () => {
test('T2 落井下石: a stunned enemy in her range takes ×1.21 physical damage from anyone (×1.27 with ART-X stage 3); not arts, not unstunned, not frozen, not outside her range; two W keep the strongest', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const x3 = elite && mod === X && tier === 6;
@@ -339,7 +339,7 @@ test('T2 落井下石: a stunned enemy in her range takes ×1.18 physical damage
const ally = h.unit(2);
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } });
const inR = h.spawn('enemy_dummy', { pos: [11, 7] }), outR = h.spawn('enemy_dummy', { pos: [12, 7] });
const scale = x3 ? 1.24 : 1.18;
const scale = x3 ? 1.27 : 1.21;
assert.equal(u.def.raw.talents.find((t) => t.index === 1).bb.damage_scale, scale, label(f));
const deal = (e, type) => h.b.dealDamage(ally, e, { amount: 1000, type, canDodge: false });
assert.equal(deal(inR, 'phys'), 1000, `${label(f)}: not stunned`);
@@ -359,7 +359,7 @@ test('T2 落井下石: a stunned enemy in her range takes ×1.18 physical damage
h.step();
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
h.b.applyStatus(e, 'stun', { duration: 5 });
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'phys', canDodge: false }), 1240, 'two W: ×1.24, not ×1.18 × 1.24');
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'phys', canDodge: false }), 1270, 'two W: ×1.27, not ×1.21 × 1.27');
checkInvariants(h.b);
});
+4 -4
View File
@@ -72,7 +72,7 @@ function watch(h, hitter) {
return () => ({ n: seen.size, rate: seen.size ? 1 - passed.size / seen.size : 0 });
}
test('弑君者 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, melee ground-only, 22 s redeploy, 叙拉古, no 特质; each skill a timed deployment skill (被动 ON_DEPLOY), running from the deployment', () => {
test('弑君者 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, melee ground-only, 18 s redeploy, 叙拉古, no 特质; each skill a timed deployment skill (被动 ON_DEPLOY), running from the deployment', () => {
assert.equal(OPERATOR_KITS[CROSLY], KITS[CROSLY]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -81,7 +81,7 @@ test('弑君者 in every 自选 form: her operator kit (all three skills authore
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, rec = form.skills[skill];
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [CROSLY, SLOT[tier], rec.skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.respawnTime, u.base.cost],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 22, 12], `${label(f)}: stats`);
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 18, 9], `${label(f)}: stats`);
assert.deepEqual([u.profile.attack, u.profile.dmgType, u.profile.canHitFly], ['melee', 'phys', false], `${label(f)}: 处决者`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 1-1 (S3 too)`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['siracusaShip'], []], `${label(f)}: bonds / 特质`);
@@ -244,12 +244,12 @@ test('T1 吞咽苦厄: while her skill runs, the attacks of ground enemies in he
}
});
test('T2 弑君者威名: physical damage she deals to a ground enemy that has not damaged her this deployment ×1.2 (EXE-Y stage 3: ×1.35, and her 晕眩 on it ×1.5); a 持续伤害 tick marks nothing; the marks go when she leaves the field', () => {
test('T2 弑君者威名: physical damage she deals to a ground enemy that has not damaged her this deployment ×1.22 (EXE-Y stage 3: ×1.37, and her 晕眩 on it ×1.5); a 持续伤害 tick marks nothing; the marks go when she leaves the field', () => {
for (const f of [[5, false, null], [6, true, EY], [5, true, EY]]) {
const [tier, elite, mod] = f;
const t1 = talentOf(tier, elite, mod, 1), hidden = talentOf(tier, elite, mod, -1);
const y3 = elite && mod === EY && tier === 6;
assert.deepEqual([t1.damage_scale, hidden.one_minus_status_resistance ?? 0], y3 ? [1.35, 0.5] : [1.2, 0], label(f));
assert.deepEqual([t1.damage_scale, hidden.one_minus_status_resistance ?? 0], y3 ? [1.37, 0.5] : [1.22, 0], label(f));
const { h, u } = field({ tier, elite, mod, skill: 0 });
h.run(10.1); // S1 over: plain attacks
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
+4 -4
View File
@@ -70,8 +70,8 @@ test('贝洛内 in every 自选 form: his operator kit (all three skills authore
done(h);
}
}
// E2 Lv1 2152 / 490 / 298, E2 Lv60 2466 / 566 / 336; FGT-Y +140 / +50 → +230 / +80
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], [2152, 490, 2466, 566]);
// full potential: E2 Lv1 2152 / 514 / 298, E2 Lv60 2466 / 590 / 336; FGT-Y +140 / +50 → +230 / +80
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], [2152, 514, 2466, 590]);
assert.deepEqual([modOf(5, FGTY).attr, modOf(6, FGTY).attr], [{ maxHp: 140, atk: 50 }, { maxHp: 230, atk: 80 }]);
});
@@ -101,7 +101,7 @@ test('FGT-Y “实用的工具”: ASPD +10 while his HP is above 50 % (stages 1
}
});
test('T1 家族手段: each attack hit puts one more 【手段】 stack on its target before its damage (DEF ×(1 − 7 % × n), FGT-Y stage 3: 8 %; at most 5) for 10 s; and his damage ×1 → ×1.28 (×1.42) linearly as the target falls from 100 % to 20 % HP', () => {
test('T1 家族手段: each attack hit puts one more 【手段】 stack on its target before its damage (DEF ×(1 − 7 % × n), FGT-Y stage 3: 8 %; at most 5) for 10 s; and his damage ×1 → ×1.36 (×1.5) linearly as the target falls from 100 % to 20 % HP', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
@@ -109,7 +109,7 @@ test('T1 家族手段: each attack hit puts one more 【手段】 stack on its t
const t = talentOf(u, 0);
const y3 = elite && mod === FGTY && tier === 6;
assert.deepEqual([t['attack@def'], t['attack@limited_stack_cnt'], t['attack@s2_limited_stack_cnt'], t['attack@def_dec_duration'], t.min_hp_ratio, t.max_hp_ratio, t.max_add_on_scale],
[y3 ? -0.08 : -0.07, 5, 8, 10, 1, 0.2, y3 ? 0.42 : 0.28], `${label(f)}: the talent of the form`);
[y3 ? -0.08 : -0.07, 5, 8, 10, 1, 0.2, y3 ? 0.5 : 0.36], `${label(f)}: the talent of the form`);
const pct = -t['attack@def'];
const e = h.spawn('enemy_armor', { pos: [10, 6] });
const n0 = atkHits(h, u).length;
+25 -16
View File
@@ -83,8 +83,8 @@ test('多萝西 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// E2 Lv1 1186 / 476 / 142, E2 Lv60 1395 / 546 / 162; TRP-Y +120 / +42 → +155 / +57, TRP-X +36 / +12 → +60 / +30
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [476, 546]);
// E2 Lv1 1186 / 503 / 142, E2 Lv60 1395 / 573 / 162; TRP-Y +120 / +42 → +155 / +57, TRP-X +36 / +12 → +60 / +30
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [503, 573]);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr, modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 120, atk: 42 }, { maxHp: 155, atk: 57 }, { atk: 36, def: 12 }, { atk: 60, def: 30 }]);
// the trap card: cost 3 (TRP-X 2), redeploy 5 s, deploy limit 10 (TRP-X 13)
assert.deepEqual([tokOf(6, true, 0, null).stats.cost, tokOf(6, true, 0, X).stats.cost, tokOf(6, true, 0, null).stats.respawnTime, tokOf(6, true, 0, null).stats.deployLimit, tokOf(6, true, 0, X).stats.deployLimit], [3, 2, 5, 10, 13]);
@@ -98,17 +98,17 @@ test('a 自选 pick: 多萝西 is offered at tiers 5 and 6 (she has a kit) and a
{ ok: true, picks: { [SLOT[6]]: { charId: DOR, skillIndex: 2, uniEquipId: X } } });
});
test('T1 共振装置: at her deployment her stock is cnt (8) and attack@max_cnt traps (2; TRP-X stage 3: 3) appear on free melee tiles of her range (the enemies\' ground paths first) — no stock spent, inside the limit (10; TRP-X 13) with room left for the placed pieces', () => {
test('T1 共振装置: at her deployment her stock is cnt (10) and attack@max_cnt traps (2; TRP-X stage 3: 3) appear on free melee tiles of her range (the enemies\' ground paths first) — no stock spent, inside the limit (10; TRP-X 13) with room left for the placed pieces', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t0 = talentOf(tier, elite, mod, 0);
const lim = tokOf(tier, elite, 0, mod).stats.deployLimit;
assert.deepEqual([t0.cnt, t0['attack@max_cnt'], lim], [8, mod === X && tier === 6 ? 3 : 2, mod === X ? 13 : 10], label(f));
assert.deepEqual([t0.cnt, t0['attack@max_cnt'], lim], [10, mod === X && tier === 6 ? 3 : 2, mod === X ? 13 : 10], label(f));
{
const { h, u } = field({ tier, elite, mod });
const made = alive(h);
assert.equal(made.length, t0['attack@max_cnt'], `${label(f)}: ${t0['attack@max_cnt']} at her deployment`);
assert.deepEqual([D(u).stock, D(u).limit], [8, lim], `${label(f)}: stock 8, limit ${lim}`);
assert.deepEqual([D(u).stock, D(u).limit], [10, lim], `${label(f)}: stock 10, limit ${lim}`);
const range = new Set(u.rangeKeys), path = h.b.groundPathTiles();
for (const t of made) {
const k = t.tileR * 21 + t.tileC;
@@ -125,7 +125,7 @@ test('T1 共振装置: at her deployment her stock is cnt (8) and attack@max_cnt
assert.equal(alive(h).filter((t) => t.uid != null).length, 9, `${label(f)}: every piece deployed at the start`);
if (!(mod === X && tier === 6)) assert.equal(summoned.length, Math.min(t0['attack@max_cnt'], lim - 9), `${label(f)}: summons within the limit`);
assert.ok(alive(h).length <= lim, `${label(f)}: never past the limit`);
assert.equal(D(u).stock, 8, `${label(f)}: the start deployment spends no trap`);
assert.equal(D(u).stock, 10, `${label(f)}: the start deployment spends no trap`);
done(h);
}
}
@@ -138,9 +138,18 @@ test('the skills\' 主动效果 "立即获得一个陷阱": +1 to her stock as s
assert.deepEqual([sk.skillType, sk.spCost, sk.initSp, sk.bb.cnt], ['AUTO', elite ? 16 : 19, 0, 1], label(f));
const { h, u } = field({ tier, elite, mod });
const lim = D(u).limit;
// full potential: her deployment stock (cnt 10) fills the limit 10 — 阻回 from the deployment until a trap is spent
// (one spent here: 9); TRP-X's limit 13 leaves room (10)
assert.deepEqual([D(u).stock, lim], [10, mod === X ? 13 : 10], `${label(f)}: stock 10, limit ${lim}`);
if (mod !== X) {
assert.ok(u.findBuff('talent:doroth:full') && u.s.flags.noSp, `${label(f)}: 阻回 from her deployment`);
D(u).stock--;
h.step();
}
const s0 = mod === X ? 10 : 9;
assert.ok(h.runUntil(() => u.skill.activations === 1, sk.spCost + 1), `${label(f)}: cast at full SP`);
assert.equal(D(u).stock, 9, `${label(f)}: +1`);
h.run(sk.spCost * (lim - 9) + 1);
assert.equal(D(u).stock, s0 + 1, `${label(f)}: +1`);
h.run(sk.spCost * (lim - s0 - 1) + 1);
assert.equal(D(u).stock, lim, `${label(f)}: full`);
assert.ok(u.findBuff('talent:doroth:full') && u.s.flags.noSp, `${label(f)}: 阻回`);
const sp = u.skill.sp, casts = u.skill.activations;
@@ -242,11 +251,11 @@ test('S3 高速共振排障: every selectable ground enemy on its x-6 takes atk_
}
});
test('T2 梦想家: every trap that goes off gives her ATK +2 % (TRP-Y stage 3: 4 %), up to 10 stacks, until she leaves — with S1 / S2 before the trap\'s damage, with S3 after it', () => {
test('T2 梦想家: every trap that goes off gives her ATK +2 % (TRP-Y stage 3: 4 %), up to 12 stacks, until she leaves — with S1 / S2 before the trap\'s damage, with S3 after it', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t1 = talentOf(tier, elite, mod, 1);
assert.deepEqual([t1.atk, t1.max_stack_cnt], [mod === Y && tier === 6 ? 0.04 : 0.02, 10], label(f));
assert.deepEqual([t1.atk, t1.max_stack_cnt], [mod === Y && tier === 6 ? 0.04 : 0.02, 12], label(f));
for (const skill of [0, 2]) {
const { h, u } = field({ tier, elite, mod, skill, pieces: [[9, 6]] });
for (const t of alive(h)) if (t.uid == null) h.b.retreat(t, { reason: 'expired', permanent: true });
@@ -263,17 +272,17 @@ test('T2 梦想家: every trap that goes off gives her ATK +2 % (TRP-Y stage 3:
done(h);
}
}
// ten at most, gone when she leaves
// twelve at most, gone when she leaves
const { h, u } = field({ tier: 5, skill: 0, pieces: [[9, 6]] });
const trap = piece(h, 2);
for (let i = 0; i < 12; i++) {
for (let i = 0; i < 14; i++) {
const e = h.spawn('enemy_dummy', { pos: [9, 6] });
h.step();
h.b.kill(e, null);
trap.mem.dorFired = false;
if (!trap.alive) h.b.redeploy(trap, { free: true });
}
assert.equal(u.findBuff('talent:doroth:dream').stacks, 10, 'ten stacks at most');
assert.equal(u.findBuff('talent:doroth:dream').stacks, 12, 'twelve stacks at most');
h.b.retreat(u);
assert.equal(u.findBuff('talent:doroth:dream'), null, 'gone when she leaves');
done(h);
@@ -315,7 +324,7 @@ test('a used-up piece comes back on its tile once the card is ready (5 s after t
h.run(8);
assert.equal(alive(h).filter((t) => t.uid != null).length, 0, `${label(f)}: not while she is off the field`);
h.b.redeploy(u, { free: true });
assert.equal(D(u).stock, 8, `${label(f)}: the stock refilled at her deployment`);
assert.equal(D(u).stock, 10, `${label(f)}: the stock refilled at her deployment`);
h.runUntil(() => alive(h).filter((t) => t.uid != null).length === 2, 20);
assert.equal(alive(h).filter((t) => t.uid != null).length, 2, `${label(f)}: both pieces back`);
done(h);
@@ -368,11 +377,11 @@ test('TRP-X 梦中人: traps cost 2, limit / stock 13; stage 3: 3 traps at her d
// her T2 full: every placed piece standing rolls again
const u0 = h0.unit(1);
const before = alive(h0).filter((t) => t.uid == null).length;
for (let i = 0; i < 9; i++) h0.b.addBuff(u0, { key: 'talent:doroth:dream', refresh: 'stack', stacks: 1, maxStacks: 10, mods: { atkPct: 0.02 } });
for (let i = 0; i < 11; i++) h0.b.addBuff(u0, { key: 'talent:doroth:dream', refresh: 'stack', stacks: 1, maxStacks: 12, mods: { atkPct: 0.02 } });
const spare = alive(h0).find((t) => t.uid == null);
h0.spawn('enemy_dummy', { pos: [spare.tileR, spare.tileC] });
h0.step();
assert.equal(u0.findBuff('talent:doroth:dream').stacks, 10);
assert.equal(u0.findBuff('talent:doroth:dream').stacks, 12);
const after = alive(h0).filter((t) => t.uid == null).length;
assert.equal(after, Math.min(13 - 3, before - 1 + 3), 'one more per placed piece standing (inside the limit)');
checkInvariants(h0.b);
+4 -4
View File
@@ -94,9 +94,9 @@ test('黑键 in every 自选 form: his operator kit (all three skills authored),
done(h);
}
}
// the numbers (zh_CN): E2 Lv1 1359 / 1134 / 120 / 20, E2 Lv60 1570 / 1310 / 130 / 20; MSC-X +58 ATK +3 ASPD → +90 / +5,
// the numbers (zh_CN, full potential): E2 Lv1 1359 / 1186 / 120 / 20, E2 Lv60 1570 / 1362 / 130 / 20; MSC-X +58 ATK +3 ASPD → +90 / +5,
// MSC-Y +80 HP +88 ATK → +175 / +135, MSC-Δ +76 ATK +4 RES → +124 / +5
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [1359, 1134, 1310]);
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [1359, 1186, 1362]);
assert.deepEqual([modOf(5, MX).attr, modOf(6, MX).attr, modOf(5, MY).attr, modOf(6, MY).attr, modOf(5, MD).attr, modOf(6, MD).attr],
[{ atk: 58, aspd: 3 }, { atk: 90, aspd: 5 }, { maxHp: 80, atk: 88 }, { maxHp: 175, atk: 135 }, { atk: 76, res: 4 }, { atk: 124, res: 5 }]);
// he hits a flyer
@@ -177,12 +177,12 @@ test('the volley: his next attack releases every normal energy at its target, AT
done(h);
});
test('T2 倚音: per projectile (the main bolt and every energy) a target with no other enemy within range_radius 1.1 takes ATK × atk_scale arts (15 %; MSC-Y 20 %, MSC-Δ 30 % at stage 3); with one there: nothing — MSC-Y stage 3 ATK × 36 % arts splash on the others instead; MSC-Δ stage 3: + ATK × 30 % 元素伤害 on a target in its 凋亡 burst', () => {
test('T2 倚音: per projectile (the main bolt and every energy) a target with no other enemy within range_radius 1.1 takes ATK × atk_scale arts (17 %; MSC-Y 22 %, MSC-Δ 32 % at stage 3); with one there: nothing — MSC-Y stage 3 ATK × 36 % arts splash on the others instead; MSC-Δ stage 3: + ATK × 30 % 元素伤害 on a target in its 凋亡 burst', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t1 = talentOf(tier, elite, mod, 1);
assert.equal(t1.range_radius, 1.1);
assert.equal(t1.atk_scale, tier === 6 && mod === MY ? 0.2 : tier === 6 && mod === MD ? 0.3 : 0.15, `${label(f)}: atk_scale`);
assert.equal(t1.atk_scale, tier === 6 && mod === MY ? 0.22 : tier === 6 && mod === MD ? 0.32 : 0.17, `${label(f)}: atk_scale`);
// alone: the main bolt + 2 energies ⇒ 3 additions
{
const { h, u } = field({ tier, elite, mod, skill: 0 });
+4 -4
View File
@@ -48,7 +48,7 @@ function done(h) {
const mine = (h, u) => h.hooksOf('damaged').filter((c) => c.source === u);
const droneHits = (h, u, e = null) => mine(h, u).filter((c) => c.dmg.tags?.includes(DRONE_TAG) && (!e || c.target === e));
test('澄闪 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, 驭械术师 (ranged arts, hits air, blocks 1; the trait ramp of the form — FUN-X init 0.35, FUN-Y cap 1.2), ground-targetable; S1 DEFAULT, S2 at full SP (AUTO on herself), S3 GDGLOW_SKILL_2; 精准导流 15 (FUN-Y stage 3 20)', () => {
test('澄闪 in every 自选 form: her kit (all three skills authored), the form\'s stats + module attributes, 3-1, 驭械术师 (ranged arts, hits air, blocks 1; the trait ramp of the form — FUN-X init 0.35, FUN-Y cap 1.2), ground-targetable; S1 DEFAULT, S2 at full SP (AUTO on herself), S3 GDGLOW_SKILL_2; 精准导流 18 (FUN-Y stage 3 23)', () => {
assert.equal(OPERATOR_KITS[GD], KITS[GD]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -69,7 +69,7 @@ test('澄闪 in every 自选 form: her kit (all three skills authored), the form
}
assert.deepEqual([skillOf(5, false, S1).trigger.rule, skillOf(5, false, S2).trigger.rule, skillOf(5, false, S2).skillType], ['DEFAULT', 'DEFAULT', 'AUTO']);
assert.deepEqual([modOf(5, FX).attr, modOf(6, FX).attr, modOf(5, FY).attr, modOf(6, FY).attr], [{ maxHp: 90, atk: 22 }, { maxHp: 125, atk: 38 }, { atk: 30, aspd: 5 }, { atk: 45, aspd: 7 }]);
assert.deepEqual([talentOf(6, true, FY, 1).bb.magic_resist_penetrate_fixed, talentOf(6, true, FX, 0).bb['attack@atk_scale_2'], talentOf(5, true, FX, 0).bb['attack@atk_scale_2']], [20, 3.6, 3]);
assert.deepEqual([talentOf(6, true, FY, 1).bb.magic_resist_penetrate_fixed, talentOf(6, true, FX, 0).bb['attack@atk_scale_2'], talentOf(5, true, FX, 0).bb['attack@atk_scale_2']], [23, 3.75, 3.15]);
});
test('a 自选 pick: 澄闪 is offered at tiers 5 and 6 and a roster with her passes validateDiyPicks', () => {
@@ -188,11 +188,11 @@ test('S3 澄净闪耀 (GDGLOW_SKILL_2: an enemy anywhere): 30 s, no attack of he
}
});
test('T1 信标的愤怒: per drone 1.5 % × its stacks (from 1, +1 per miss, sure past 40): ~10 % of the drone attacks; the blast — atk_scale_2 × ATK arts within 1.1 of the target (air too), no ramp (FUN-X stage 3 360 %), 停顿 under S3 — sends the drone back and restarts its count; reset at a deployment', () => {
test('T1 信标的愤怒: per drone 1.5 % × its stacks (from 1, +1 per miss, sure past 40): ~10 % of the drone attacks; the blast — atk_scale_2 × ATK arts within 1.1 of the target (air too), no ramp (FUN-X stage 3 375 %), 停顿 under S3 — sends the drone back and restarts its count; reset at a deployment', () => {
for (const f of [[5, false, null], [6, true, FX]]) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 0).bb;
assert.deepEqual([tb['attack@prob'], tb['attack@atk_scale_2'], tb['attack@max_stack_cnt']], [0.015, elite && mod === FX && tier === 6 ? 3.6 : 3, 40], label(f));
assert.deepEqual([tb['attack@prob'], tb['attack@atk_scale_2'], tb['attack@max_stack_cnt']], [0.015, elite && mod === FX && tier === 6 ? 3.75 : 3.15, 40], label(f));
// a sure blast: both drones past the cap
const { h, u } = field({ tier, elite, mod, skill: 2 });
const e = h.spawn('enemy_dummy', { pos: [10, 6] }), near = h.spawn('enemy_fly', { pos: [11, 6] }), far = h.spawn('enemy_dummy', { pos: [10, 8] });
+5 -5
View File
@@ -67,7 +67,7 @@ test('阿 in every 自选 form: his operator kit (all three skills authored), th
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], [HAAK, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, 100 + (m?.attr.aspd ?? 0), 70 + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def, 100 + (m?.attr.aspd ?? 0), 66 + (m?.attr.respawnTime ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.profile.projectile, u.profile.hpDrain], [1, 'ranged', 'phys', true, 'arrow', 0.01], `${label(f)}: 怪杰`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-3`);
assert.equal(form.rangeId, '3-3');
@@ -77,9 +77,9 @@ test('阿 in every 自选 form: his operator kit (all three skills authored), th
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1627 / 583 / 121, E2 Lv60 1897 / 663 / 142; GEE-X +135 / +37 → +240 / +57 HP / ATK,
// the numbers of the forms (zh_CN): E2 Lv1 1777 / 583 / 121, E2 Lv60 2047 / 663 / 142; GEE-X +135 / +37 → +240 / +57 HP / ATK,
// GEE-Y +43 / +3 → +75 / +5 ATK / ASPD and respawn −15
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], [1627, 583, 1897, 663]);
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], [1777, 583, 2047, 663]);
assert.deepEqual([modOf(5, GX).attr, modOf(6, GX).attr, modOf(5, GY).attr, modOf(6, GY).attr],
[{ maxHp: 135, atk: 37 }, { maxHp: 240, atk: 57 }, { atk: 43, aspd: 3, respawnTime: -15 }, { atk: 75, aspd: 5, respawnTime: -15 }]);
});
@@ -171,13 +171,13 @@ test('T1 混合药物射击: every attack on an enemy rolls one of four with equ
}
});
test('T2 药剂扩散: healing he receives ×1.2 (GEE-Y stage 3: ×1.25); GEE-Y stage 3 heals one ally of his range at each deployment for 150 % ATK — the straight line ahead first, never a 禁疗 one', () => {
test('T2 药剂扩散: healing he receives ×1.25 (GEE-Y stage 3: ×1.3); GEE-Y stage 3 heals one ally of his range at each deployment for 150 % ATK — the straight line ahead first, never a 禁疗 one', () => {
const others = [{ uid: 2, chessId: YAK, row: 10, col: 6 }, { uid: 3, chessId: TEXAS, row: 11, col: 4 }, { uid: 4, chessId: LISKAM, row: 10, col: 5 }];
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t1 = talentOf(tier, elite, mod, 1);
const y3 = elite && mod === GY && tier === 6;
assert.deepEqual([t1.heal_scale, t1['healinrange.heal_scale'] ?? 0], y3 ? [1.25, 1.5] : [1.2, 0], label(f));
assert.deepEqual([t1.heal_scale, t1['healinrange.heal_scale'] ?? 0], y3 ? [1.3, 1.5] : [1.25, 0], label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, others });
assert.equal(u.s.healingTakenMul, t1.heal_scale, `${label(f)}: ×${t1.heal_scale}`);
const yak = h.unit(2), texas = h.unit(3), liskam = h.unit(4);
+4 -4
View File
@@ -76,7 +76,7 @@ test('遥 in every 自选 form: her kit (all three skills authored), the form\'s
done(h);
}
}
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], [1415, 398, 1728, 452]);
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], [1415, 423, 1728, 477]);
assert.deepEqual([modOf(5, Y).attr, modOf(6, Y).attr], [{ maxHp: 130, atk: 30 }, { maxHp: 210, atk: 52 }]);
});
@@ -144,7 +144,7 @@ test('T1 浮光泡影: with each attack a bubble goes to the ally of her range w
done(h);
});
test('T1 / T2: an enemy hit that lowers the holder\'s HP breaks its bubble 0.5 s later (once; a fully shielded hit does not count) and heals it 25 % of her ATK as it was when she gave it; BLS-Y stage 3: 20 % to give the holder 1 SP; stage 1: never', () => {
test('T1 / T2: an enemy hit that lowers the holder\'s HP breaks its bubble 0.5 s later (once; a fully shielded hit does not count) and heals it 28 % of her ATK as it was when she gave it; BLS-Y stage 3: 20 % to give the holder 1 SP; stage 1: never', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, Y], [5, true, Y]]) {
const { h, u } = field({ tier, elite, mod, others: [ally(2, 11, 6)] });
const a = h.unit(2);
@@ -171,7 +171,7 @@ test('T1 / T2: an enemy hit that lowers the holder\'s HP breaks its bubble 0.5 s
assert.notEqual(a.findBuff(BUBBLE), b, 'broken 0.5 s after the first hit');
const hs = heals(h, u, n0).filter((x) => x.target === a);
assert.equal(hs.length, 1, 'one break, one heal');
approx(hs[0].amount, atk0 * 0.25, '25 % of the ATK of the grant');
approx(hs[0].amount, atk0 * 0.28, '28 % of the ATK of the grant');
void u;
done(h);
}
@@ -340,6 +340,6 @@ test('outside S3 a bubble levitates nobody; a bubble outlasts her leaving and st
h.b.dealDamage(e, a, { amount: 10, type: 'true' });
assert.equal(h.hooksOf('statusApplied').filter((c) => c.status === 'levitate').length, 0);
h.run(0.6);
approx(heals(h, u, n0)[0].amount, atk * 0.25, 'her heal after she left');
approx(heals(h, u, n0)[0].amount, atk * 0.28, 'her heal after she left');
done(h);
});
+19 -18
View File
@@ -74,8 +74,9 @@ test('怒潮凛冬 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// zh_CN: E2 Lv1 2301 / 1060 / 346, E2 Lv60 2731 / 1194 / 387; HAM-X +250 / +63 / +21 → +380 / +83 / +30
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], [2301, 1060, 2731, 1194]);
// zh_CN (full potential: ATK +40): E2 Lv1 2301 / 1100 / 346, E2 Lv60 2731 / 1234 / 387; HAM-X +250 / +63 / +21 →
// +380 / +83 / +30
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], [2301, 1100, 2731, 1234]);
assert.deepEqual([modOf(5, HAMX).attr, modOf(6, HAMX).attr], [{ maxHp: 250, atk: 63, def: 21 }, { maxHp: 380, atk: 83, def: 30 }]);
});
@@ -115,12 +116,12 @@ test('trait + T1 汹涌怒火 + HAM-X: the target takes ATK, the others within 1
}
});
test('T1 汹涌怒火 高台: every 高台 the 1.0 circle around her target touches hits the ground enemies of its x-5 0.1 s later — 24 % ATK physical 溅射 (no dodge, 无来源, 远程途径) + 停顿 0.5 s; HAM-X: ×1.15 with 3 there', () => {
test('T1 汹涌怒火 高台: every 高台 the 1.0 circle around her target touches hits the ground enemies of its x-5 0.1 s later — 27 % ATK physical 溅射 (no dodge, 无来源, 远程途径) + 停顿 0.5 s; HAM-X: ×1.15 with 3 there', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, noSp: true, rows: ONE_HIGH });
const t0 = t0Of(u);
assert.deepEqual([t0['attack@splash_atk_scale'], t0['attack@sluggish']], [0.24, 0.5], label(f));
assert.deepEqual([t0['attack@splash_atk_scale'], t0['attack@sluggish']], [0.27, 0.5], label(f));
assert.equal(h.b.grid.tile(11, 7).height, 'HIGH');
const a = h.spawn('enemy_dummy', { pos: [10, 6] });
const b = h.spawn('enemy_dummy', { pos: [12, 7] }); // only on (11, 7)'s x-5
@@ -136,7 +137,7 @@ test('T1 汹涌怒火 高台: every 高台 the 1.0 circle around her target touc
assert.equal(hb.length, 1, `${label(f)}: the 高台 splash`);
const d = hb[0];
approx(d.t - atkT, 0.1, `${label(f)}: 0.1 s after the attack`, 0.4);
approx(d.amount, atk * 0.24, `${label(f)}: 24 % ATK`);
approx(d.amount, atk * 0.27, `${label(f)}: 27 % ATK`);
assert.deepEqual([d.type, d.source, d.credit, d.dmg.isSplash, d.dmg.canDodge, d.dmg.sourceless, d.dmg.isProjectile], ['phys', null, u, true, false, true, true], `${label(f)}: physical 溅射, 无来源 credited to her, 远程途径`);
const st = h.hooksOf('statusApplied').filter((s) => s.target === b && s.status === 'sluggish');
assert.ok(st.length === 1 && Math.abs(st[0].duration - 0.5) < 1e-9, `${label(f)}: 停顿 0.5 s`);
@@ -149,7 +150,7 @@ test('T1 汹涌怒火 高台: every 高台 the 1.0 circle around her target touc
h.run(u.s.interval + 0.2);
const next = dmgOn(h, b, highland).slice(n0);
assert.ok(next.length >= 1, `${label(f)}: the next attack's 高台 splash`);
approx(next[0].amount, u.s.atk * 0.24 * (elite && mod === HAMX ? 1.15 : 1), `${label(f)}: ×${elite && mod === HAMX ? 1.15 : 1} with 3 on the x-5`);
approx(next[0].amount, u.s.atk * 0.27 * (elite && mod === HAMX ? 1.15 : 1), `${label(f)}: ×${elite && mod === HAMX ? 1.15 : 1} with 3 on the x-5`);
done(h);
}
});
@@ -179,18 +180,18 @@ test('S2 绝不罢休\'s passive: +1 SP per 高台 that triggers 汹涌怒火 (n
done(h);
});
test('T2 万众巨潮: while a skill of hers runs every operator of the field ATK / DEF +14 % — 【乌萨斯学生自治团】 (her, 古米) +28 % — and nothing before or after it', () => {
test('T2 万众巨潮: while a skill of hers runs every operator of the field ATK / DEF +18 % — 【乌萨斯学生自治团】 (her, 古米) +36 % — and nothing before or after it', () => {
for (const [tier, elite, skill] of [[5, false, 0], [6, true, 1]]) {
const others = [{ uid: 2, chessId: 'chess_char_1_10_a', row: 11, col: 3 }, { uid: 3, chessId: 'chess_char_1_08_a', row: 12, col: 3 }];
const { h, u } = field({ tier, elite, skill, others });
const gummy = h.unit(2), texas = h.unit(3);
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1.atk, t1.def, t1.scale_bonus], [0.14, 0.14, 2]);
assert.deepEqual([t1.atk, t1.def, t1.scale_bonus], [0.18, 0.18, 2]);
h.run(0.5);
for (const o of [u, gummy, texas]) assert.equal(o.findBuff(TIDE), null, `T${tier}: nothing before her skill (${o.def.charId})`);
u.skill.gainSp(999);
if (skill === 0) { h.spawn('enemy_dummy', { pos: [10, 6] }); assert.ok(h.runUntil(() => u.skill.active, 3)); } else assert.ok(h.runUntil(() => u.skill.active, 1), 'S2 at full SP');
for (const [o, v] of [[u, 0.28], [gummy, 0.28], [texas, 0.14]]) {
for (const [o, v] of [[u, 0.36], [gummy, 0.36], [texas, 0.18]]) {
const m = o.findBuff(TIDE)?.mods;
assert.ok(m, `T${tier}: ${o.def.charId} in her skill`);
approx(m.atkPct, v, `T${tier}: ${o.def.charId} ATK`);
@@ -204,7 +205,7 @@ test('T2 万众巨潮: while a skill of hers runs every operator of the field AT
}
});
test('S1 誓不低头 (MANUAL, DEFAULT): 30 s, ATK +32 % / 39 %, ASPD +35 / 40 (her own 万众巨潮 +28 % on top); all back after', () => {
test('S1 誓不低头 (MANUAL, DEFAULT): 30 s, ATK +32 % / 39 %, ASPD +35 / 40 (her own 万众巨潮 +36 % on top); all back after', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const { h, u } = field({ tier, elite, skill: 0 });
@@ -215,7 +216,7 @@ test('S1 誓不低头 (MANUAL, DEFAULT): 30 s, ATK +32 % / 39 %, ASPD +35 / 40 (
h.spawn('enemy_dummy', { pos: [10, 6] });
assert.ok(h.runUntil(() => u.skill.active, 3), `T${tier}: cast with an enemy in range`);
approx(u.skill.timeLeft, 30, `T${tier}: 30 s`, 0.01);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.28), `T${tier}: ATK`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.36), `T${tier}: ATK`);
approx(u.s.aspd, 100 + sk.bb.attack_speed, `T${tier}: ASPD`);
h.runUntil(() => !u.skill.active, 40);
approx(u.s.atk, u.base.atk, `T${tier}: ATK back`);
@@ -232,8 +233,8 @@ test('S2 绝不罢休 (AUTO, SP_FULL — a self buff): at full SP with no enemy,
assert.equal(u.skill.activations, 0, `T${tier}: not before full SP`);
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: at full SP, no enemy on the field`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 2-5 while it runs`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.28), `T${tier}: ATK (+ her 万众巨潮)`);
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.28), `T${tier}: DEF`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.36), `T${tier}: ATK (+ her 万众巨潮)`);
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.36), `T${tier}: DEF`);
h.run(15.9);
assert.ok(u.skill.active, `T${tier}: 16 s`);
h.run(0.2);
@@ -241,8 +242,8 @@ test('S2 绝不罢休 (AUTO, SP_FULL — a self buff): at full SP with no enemy,
assert.deepEqual(u.liveRangeGrid, [[0, 0], [0, 1]], `T${tier}: 1-1 again`);
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: the second cast`);
approx(u.s.atk, u.base.atk * (1 + sk.bb['headb2_s_2[second].atk'] + 0.28), `T${tier}: ATK doubled`);
approx(u.s.def, u.base.def * (1 + sk.bb['headb2_s_2[second].def'] + 0.28), `T${tier}: DEF doubled`);
approx(u.s.atk, u.base.atk * (1 + sk.bb['headb2_s_2[second].atk'] + 0.36), `T${tier}: ATK doubled`);
approx(u.s.def, u.base.def * (1 + sk.bb['headb2_s_2[second].def'] + 0.36), `T${tier}: DEF doubled`);
h.run(200);
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
h.b.retreat(u);
@@ -250,7 +251,7 @@ test('S2 绝不罢休 (AUTO, SP_FULL — a self buff): at full SP with no enemy,
h.step();
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 1));
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.28), `T${tier}: a new deployment's first cast`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + 0.36), `T${tier}: a new deployment's first cast`);
done(h);
}
});
@@ -275,7 +276,7 @@ test('S3 无可抵挡 (MANUAL, data CUSTOM_RANGE): five hammers on the Skill_3 c
h.runUntil(() => !u.skill.active, 12);
approx(h.b.time - t0, SKILL3_T, `T${tier}: over with the clip`, 0.02);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u && c.t > t0 && c.t < t0 + SKILL3_T - 0.1).length, 0, `T${tier}: no normal attack`);
const atkK = (k) => u.base.atk * (1 + sk.bb.atk_base + 0.28 + sk.bb.atk_step * (k - 1));
const atkK = (k) => u.base.atk * (1 + sk.bb.atk_base + 0.36 + sk.bb.atk_step * (k - 1));
const mh = dmgOn(h, main, (c) => c.t >= t0 && hammerHit(c));
assert.equal(mh.length, 5, `T${tier}: five hammers on the target ahead`);
mh.forEach((c, i) => { approx(c.t - t0, HAMMER_T[i], `T${tier}: hammer ${i + 1} at ${HAMMER_T[i].toFixed(3)} s`, 0.02); approx(c.amount, atkK(i + 1) * sk.bb.atk_scale, `T${tier}: hammer ${i + 1} ${sk.bb.atk_scale * 100} %`); });
@@ -286,7 +287,7 @@ test('S3 无可抵挡 (MANUAL, data CUSTOM_RANGE): five hammers on the Skill_3 c
const counts = under.map((e) => dmgOn(h, e, (c) => c.t >= t0 && highland(c)).length);
assert.deepEqual(counts, [5, 4, 3, 2], `T${tier}: (11, 7) every hammer, (11, 8) from the 2nd, (11, 9) the 3rd, (11, 10) the 4th`);
const first = dmgOn(h, under[0], (c) => c.t >= t0 && highland(c));
first.forEach((c, i) => approx(c.amount, atkK(i + 1) * 0.24 * 3.5, `T${tier}: 高台 ×3.5 at hammer ${i + 1}`));
first.forEach((c, i) => approx(c.amount, atkK(i + 1) * 0.27 * 3.5, `T${tier}: 高台 ×3.5 at hammer ${i + 1}`));
const last = dmgOn(h, under[3], (c) => c.t >= t0 && highland(c));
approx(last[0].t - (t0 + HAMMER_T[3]), 0.1 + 3 * 0.125, `T${tier}: 0.1 s, then three spread steps of 0.125 s after the 4th hammer`, 0.03);
const binds = h.hooksOf('statusApplied').filter((s) => s.target === under[0] && s.status === 'bind' && s.t >= t0);
+5 -5
View File
@@ -68,9 +68,9 @@ test('赫拉格 in every 自选 form: his operator kit (all three skills authore
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2868 / 632 / 283, E2 Lv60 3502 / 718 / 317; SBL-X +230 HP +55 ATK → +350 / +90,
// the numbers of the forms (zh_CN): E2 Lv1 2868 / 658 / 283, E2 Lv60 3502 / 744 / 317; SBL-X +230 HP +55 ATK → +350 / +90,
// SBL-Y +55 ATK +50 DEF → +90 / +60
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], [2868, 632, 3502, 718]);
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], [2868, 658, 3502, 744]);
assert.deepEqual([modOf(5, SBLX).attr, modOf(6, SBLX).attr, modOf(5, SBLY).attr, modOf(6, SBLY).attr], [{ maxHp: 230, atk: 55 }, { maxHp: 350, atk: 90 }, { atk: 55, def: 50 }, { atk: 90, def: 60 }]);
});
@@ -195,11 +195,11 @@ test('T1 月盈星亏: 坚忍 ASPD up to +100 at 70 % of his HP lost (linear); S
}
});
test('T2 运筹帷幄: 60 HP / s (生命回复速度) while he blocks nobody, none while blocking; SBL-Y stage 3: 80 / s, and also while blocking below 30 % HP', () => {
test('T2 运筹帷幄: 70 HP / s (生命回复速度) while he blocks nobody, none while blocking; SBL-Y stage 3: 90 / s, and also while blocking below 30 % HP', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const y3 = elite && mod === SBLY && tier === 6;
const regen = y3 ? 80 : 60;
const regen = y3 ? 90 : 70;
const { h, u } = field({ tier, elite, mod, skill: 2, row: 9, col: 5 });
h.step();
assert.equal(u.s.hpRegen, regen, `${label(f)}: blocking nobody`);
@@ -214,7 +214,7 @@ test('T2 运筹帷幄: 60 HP / s (生命回复速度) while he blocks nobody, no
assert.equal(u.s.hpRegen, 0, `${label(f)}: none while blocking above 30 % HP`);
u.hp = u.s.maxHp * 0.25;
h.step();
assert.equal(u.s.hpRegen, y3 ? 80 : 0, `${label(f)}: blocking below 30 % HP`);
assert.equal(u.s.hpRegen, y3 ? 90 : 0, `${label(f)}: blocking below 30 % HP`);
done(h);
}
});
+14 -14
View File
@@ -73,9 +73,9 @@ test('霍尔海雅 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// E2 Lv1 1380 / 525 / 116, E2 Lv60 1639 / 597 / 125; CCR-X +120 / +40 → +175 / +65, CCR-Y +105 / +32 / +15 → +140 / +45 /
// E2 Lv1 1380 / 552 / 116, E2 Lv60 1639 / 624 / 125; CCR-X +120 / +40 → +175 / +65, CCR-Y +105 / +32 / +15 → +140 / +45 /
// +25, ISW-A +100 / +55 → +130 / +80
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], [1380, 525, 1639, 597]);
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], [1380, 552, 1639, 624]);
assert.deepEqual([modOf(5, CCRX).attr, modOf(6, CCRX).attr, modOf(5, CCRY).attr, modOf(6, CCRY).attr, modOf(5, ISWA).attr, modOf(6, ISWA).attr],
[{ maxHp: 120, atk: 40 }, { maxHp: 175, atk: 65 }, { maxHp: 105, atk: 32, def: 15 }, { maxHp: 140, atk: 45, def: 25 }, { maxHp: 100, atk: 55 }, { maxHp: 130, atk: 80 }]);
});
@@ -90,11 +90,11 @@ test('a 自选 pick: 霍尔海雅 is offered at tiers 5 and 6 and a roster with
{ ok: true, picks: { [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: CCRX } } });
});
test('S1 但为求索 (AUTO, 1 / 2 charges, data DEFAULT): the next attack hits two enemies at 200 % / 240 % ATK arts; with one enemy only it lifts it 2.5 / 3 s — before 传承终焉, so that hit is × 1.2 and silences', () => {
test('S1 但为求索 (AUTO, 1 / 2 charges, data DEFAULT): the next attack hits two enemies at 200 % / 240 % ATK arts; with one enemy only it lifts it 2.5 / 3 s — before 传承终焉, so that hit is × 1.23 and silences', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const [ts, sil] = t0Of(tier, elite, null);
assert.deepEqual([sk.skillType, sk.spCost, sk.maxChargeTime, sk.bb.atk_scale, sk.bb.levitate, ts, sil], ['AUTO', 8, elite ? 2 : 1, elite ? 2.4 : 2, elite ? 3 : 2.5, 1.2, 3], `T${tier}`);
assert.deepEqual([sk.skillType, sk.spCost, sk.maxChargeTime, sk.bb.atk_scale, sk.bb.levitate, ts, sil], ['AUTO', 8, elite ? 2 : 1, elite ? 2.4 : 2, elite ? 3 : 2.5, 1.23, 3], `T${tier}`);
// two enemies: two shots, no lift
{
const { h, u } = field({ tier, elite, skill: 0 });
@@ -134,7 +134,7 @@ test('S1 但为求索 (AUTO, 1 / 2 charges, data DEFAULT): the next attack hits
}
});
test('S2 群星逶迤 (MANUAL, data DEFAULT): 16 s, each attack 9 shots of 27 % / 35 % ATK at random enemies of her range, each rolling 10 % / 12 % to lift 1 s; a lifted or flying target takes × 1.2', () => {
test('S2 群星逶迤 (MANUAL, data DEFAULT): 16 s, each attack 9 shots of 27 % / 35 % ATK at random enemies of her range, each rolling 10 % / 12 % to lift 1 s; a lifted or flying target takes × 1.23', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
const sc = sk.bb['attack@atk_scale'], p = sk.bb['attack@prob'];
@@ -169,14 +169,14 @@ test('S2 群星逶迤 (MANUAL, data DEFAULT): 16 s, each attack 9 shots of 27 %
const all = hitsBy(h, u).slice(n0);
all.forEach((c, i) => {
if (!c.dmg.isSkill) return;
approx(c.amount, u.s.atk * sc * (fl[i] ? 1.2 : 1), `T${tier}: shot ${i} (${fl[i] ? 'air' : 'ground'})`);
approx(c.amount, u.s.atk * sc * (fl[i] ? 1.23 : 1), `T${tier}: shot ${i} (${fl[i] ? 'air' : 'ground'})`);
});
assert.ok(fl.filter(Boolean).length > shots.filter((c) => c.target === fly).length, `T${tier}: lifted ground enemies took × 1.2 too`);
assert.ok(fl.filter(Boolean).length > shots.filter((c) => c.target === fly).length, `T${tier}: lifted ground enemies took × 1.23 too`);
done(h);
}
});
test('S3 博览者的狂语 (MANUAL, data ACTIVE_RANGE on 3-10): 45 s, interval 1.6 + 1.4 = 3.0 s, range 3-10 while on; each attack three whirlwinds (her row and the rows beside), 1 tile/s, each lifting the first enemy it touches then dealing 2.33→3.5 / 2.53→3.8 × ATK over 1.77 s (× 1.2: lifted)', () => {
test('S3 博览者的狂语 (MANUAL, data ACTIVE_RANGE on 3-10): 45 s, interval 1.6 + 1.4 = 3.0 s, range 3-10 while on; each attack three whirlwinds (her row and the rows beside), 1 tile/s, each lifting the first enemy it touches then dealing 2.33→3.5 / 2.53→3.8 × ATK over 1.77 s (× 1.23: lifted)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const lo = sk.bb['attack@min_atk_scale'], hi = sk.bb['attack@max_atk_scale'], lev = sk.bb['attack@levitate'];
@@ -196,9 +196,9 @@ test('S3 博览者的狂语 (MANUAL, data ACTIVE_RANGE on 3-10): 45 s, interval
const hits = hitsBy(h, u).slice(n0);
const first = (e) => hits.find((c) => c.target === e);
const scale = (age) => lo + (hi - lo) * Math.min(1, age / 1.77);
approx(first(near).amount, u.s.atk * scale(0.5) * 1.2, `T${tier}: 0.5 tile flown`, 1e-3);
approx(first(mid).amount, u.s.atk * hi * 1.2, `T${tier}: 2.5 tiles — the maximum`);
approx(first(low).amount, u.s.atk * hi * 1.2, `T${tier}: 3.5 tiles`);
approx(first(near).amount, u.s.atk * scale(0.5) * 1.23, `T${tier}: 0.5 tile flown`, 1e-3);
approx(first(mid).amount, u.s.atk * hi * 1.23, `T${tier}: 2.5 tiles — the maximum`);
approx(first(low).amount, u.s.atk * hi * 1.23, `T${tier}: 3.5 tiles`);
assert.ok(!hits.some((c) => c.target === far), `T${tier}: (10,9) behind (10,8) in its lane is never reached`);
for (const c of hits) assert.deepEqual([c.type, c.dmg.isAttack, c.dmg.isSkill], ['arts', true, true]);
const lifts = statusBy(h, u, 'levitate');
@@ -239,7 +239,7 @@ test('S3 whirlwinds: they fly 5 tiles at most, a heavy (mass > 3) target is lift
const s0 = statusBy(h, u, 'silence').length;
assert.ok(h.runUntil(() => hitsBy(h, u).some((c) => c.target === e), 6), 'the whirlwind flies on');
const c = hitsBy(h, u).find((x) => x.target === e);
approx(c.amount, arts(atk * 3.8, 50, 0), 'no 传承终焉 (× 1.2) and no RES ignore');
approx(c.amount, arts(atk * 3.8, 50, 0), 'no 传承终焉 (× 1.38) and no RES ignore');
assert.equal(statusBy(h, u, 'silence').length, s0, 'no silence');
assert.equal(statusBy(h, u, 'levitate').filter((x) => x.target === e).length, 1, 'still lifted');
done(h);
@@ -259,11 +259,11 @@ test('S3 whirlwinds: they fly 5 tiles at most, a heavy (mass > 3) target is lift
}
});
test('T1 传承终焉: hits on air units × 1.2 and silence 3 s (CCR-X stage 3: × 1.35 / 5 s, ISW-A stage 3: × 1.45 / 5 s; stage 1 modules keep 1.2 / 3); ground targets: neither', () => {
test('T1 传承终焉: hits on air units × 1.23 and silence 3 s (CCR-X stage 3: × 1.38 / 5 s, ISW-A stage 3: × 1.48 / 5 s; stage 1 modules keep 1.23 / 3); ground targets: neither', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const [ts, sil] = t0Of(tier, elite, elite ? mod : null);
const want = elite && tier === 6 && mod === CCRX ? [1.35, 5] : elite && tier === 6 && mod === ISWA ? [1.45, 5] : [1.2, 3];
const want = elite && tier === 6 && mod === CCRX ? [1.38, 5] : elite && tier === 6 && mod === ISWA ? [1.48, 5] : [1.23, 3];
assert.deepEqual([ts, sil], want, `${label(f)}: the data`);
const { h, u } = field({ tier, elite, mod, skill: 2 });
const fly = h.spawn('enemy_fly', { pos: [10, 6] });
+5 -4
View File
@@ -75,8 +75,9 @@ test('赫德雷 in every 自选 form: his operator kit (all three skills authore
done(h);
}
}
// zh_CN: E2 Lv1 4670 / 1323 / 0, E2 Lv60 5544 / 1491 / 0; CRU-X +290 / +92 → +450 / +130, CRU-Y +300 / +110 → +400 / +160
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], [4670, 1323, 5544, 1491]);
// zh_CN (full potential: HP +250, ATK +45): E2 Lv1 4920 / 1368 / 0, E2 Lv60 5794 / 1536 / 0; CRU-X +290 / +92 → +450 / +130,
// CRU-Y +300 / +110 → +400 / +160
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], [4920, 1368, 5794, 1536]);
assert.deepEqual([modOf(5, CRUX).attr, modOf(6, CRUX).attr, modOf(5, CRUY).attr, modOf(6, CRUY).attr], [{ maxHp: 290, atk: 92 }, { maxHp: 450, atk: 130 }, { maxHp: 300, atk: 110 }, { maxHp: 400, atk: 160 }]);
});
@@ -159,7 +160,7 @@ test('CRU-Y stage 3: his first damage on an enemy he blocks stuns it 2.5 s — b
}
});
test('T2 余火之氅: he and the ally on the tile behind him get 18 % 庇护 (CRU-X stage 3: 28 % and 物理伤害 +10 %); not the ally beside him; gone once he leaves', () => {
test('T2 余火之氅: he and the ally on the tile behind him get 21 % 庇护 (CRU-X stage 3: 31 % and 物理伤害 +10 %); not the ally beside him; gone once he leaves', () => {
const others = [{ uid: 2, chessId: 'chess_char_1_08_a', row: 10, col: 4 }, { uid: 3, chessId: 'chess_char_1_10_a', row: 11, col: 5 }];
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -168,7 +169,7 @@ test('T2 余火之氅: he and the ally on the tile behind him get 18 % 庇护 (C
h.run(0.5);
const x3 = elite && tier === 6 && mod === CRUX;
const dr = t1Of(u).damage_resistance;
assert.equal(dr, x3 ? 0.28 : 0.18, label(f));
assert.equal(dr, x3 ? 0.31 : 0.21, label(f));
for (const a of [u, behind]) {
const m = a.findBuff(PROTECT)?.mods;
assert.ok(m, `${label(f)}: ${a.def.charId} protected`);
+6 -6
View File
@@ -69,7 +69,7 @@ test('斩业星熊 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2592 / 533 / 431, E2 Lv60 3228 / 622 / 517; AST-X +190 / +55 → +325 / +80
// the numbers of the forms (zh_CN): E2 Lv1 2592 / 533 / 461, E2 Lv60 3228 / 622 / 547; AST-X +190 / +55 → +325 / +80
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], [2592, 533, 3228, 622]);
assert.deepEqual([modOf(5, ASTX).attr, modOf(6, ASTX).attr], [{ maxHp: 190, atk: 55 }, { maxHp: 325, atk: 80 }]);
});
@@ -300,10 +300,10 @@ test('S3 临死模式 (a 主动关闭 only — no strategy of this mode closes i
}
});
test('T1 业火 我执: a lethal hit leaves her on the field (HP floor) with the excess as negative HP; later damage goes there (受击回复 SP still), 禁疗 meanwhile; 200 % of max HP knocks her out; 5 s without damage ⇒ 生命回复速度 5 %/s clears it and she leaves 我执', () => {
test('T1 业火 我执: a lethal hit leaves her on the field (HP floor) with the excess as negative HP; later damage goes there (受击回复 SP still), 禁疗 meanwhile; 200 % of max HP knocks her out; 4 s without damage ⇒ 生命回复速度 5 %/s clears it and she leaves 我执', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t0 = formOf(tier, elite).talents.find((t) => t.index === 0).bb;
assert.deepEqual([t0.max_minus_hp_ratio, t0['hsgma2_t_1[heal].interval'], t0['hsgma2_t_1[heal].hp_recovery_per_sec_by_max_hp_ratio']], [2, 5, 0.05], `T${tier}`);
assert.deepEqual([t0.max_minus_hp_ratio, t0['hsgma2_t_1[heal].interval'], t0['hsgma2_t_1[heal].hp_recovery_per_sec_by_max_hp_ratio']], [2, 4, 0.05], `T${tier}`);
const { h, u } = field({ tier, elite, skill: 0, others: [{ uid: 2, chessId: TEXAS, row: 12, col: 9 }] });
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.step();
@@ -323,9 +323,9 @@ test('T1 业火 我执: a lethal hit leaves her on the field (HP floor) with the
assert.deepEqual([u.hp, Math.round(u.mem.hsEgo.pool)], [1, 600], `T${tier}: a heal that ignores 禁疗 clears negative HP first`);
h.b.loseHp(u, 200, { source: e });
assert.deepEqual([u.alive, u.hp, Math.round(u.mem.hsEgo.pool)], [true, 1, 800], `T${tier}: a 流失 adds to it`);
// quiet: the regeneration starts at 5 s and clears the pool at 5 % max HP / s
h.run(4.9);
assert.equal(u.findBuff('talent:hsgma2:egoRegen'), null, `T${tier}: not before 5 s`);
// quiet: the regeneration starts at 4 s and clears the pool at 5 % max HP / s
h.run(3.9);
assert.equal(u.findBuff('talent:hsgma2:egoRegen'), null, `T${tier}: not before 4 s`);
h.run(0.2);
assert.deepEqual(u.findBuff('talent:hsgma2:egoRegen')?.mods, { hpRegenRatio: 0.05 }, `T${tier}: 生命回复速度(百分比)`);
const t1 = h.b.time;
+19 -19
View File
@@ -66,9 +66,9 @@ test('煌 in every 自选 form: her operator kit (all three skills authored), th
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2115 / 581 / 303, E2 Lv60 2583 / 703 / 347; CEN-X +240 / +50 → +285 / +86
// the numbers of the forms (zh_CN, full potential): E2 Lv1 2115 / 609 / 303, E2 Lv60 2583 / 731 / 347; CEN-X +240 / +50 → +285 / +86
// HP / ATK, CEN-Y +300 / +40 → +345 / +66
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], [2115, 581, 2583, 703]);
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], [2115, 609, 2583, 731]);
assert.deepEqual([modOf(5, CENX).attr, modOf(6, CENX).attr, modOf(5, CENY).attr, modOf(6, CENY).attr], [{ maxHp: 240, atk: 50 }, { maxHp: 285, atk: 86 }, { maxHp: 300, atk: 40 }, { maxHp: 345, atk: 66 }]);
});
@@ -207,12 +207,12 @@ test('S3 沸腾爆裂: the 流失 never takes her below 1 HP; a 晕眩 ends the
done(r.h);
});
test('T1 紧急除颤: 不死 until her HP first reaches ≤ 25 % — then +50 % max HP and for 6 s no damage takes her below 50 % (a 流失 still does); once per deployment, again after a redeploy', () => {
test('T1 紧急除颤: 不死 until her HP first reaches ≤ 25 % — then +50 % max HP and for 7 s no damage takes her below 50 % (a 流失 still does); once per deployment, again after a redeploy', () => {
for (const f of [[5, false, null], [6, true, CENX], [5, true, CENY]]) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
assert.deepEqual([t0.hp_ratio, t0['huang_t_1[heal].hp_ratio'], t0['huang_t_1[lock].min_hp_ratio'], t0['huang_t_1[lock].duration']], [0.25, 0.5, 0.5, 6], label(f));
assert.deepEqual([t0.hp_ratio, t0['huang_t_1[heal].hp_ratio'], t0['huang_t_1[lock].min_hp_ratio'], t0['huang_t_1[lock].duration']], [0.25, 0.5, 0.5, 7], label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
const max = u.s.maxHp;
// a lethal hit from full HP: 不死 keeps her at 1 HP, the talent heals 50 %
@@ -220,14 +220,14 @@ test('T1 紧急除颤: 不死 until her HP first reaches ≤ 25 % — then +50 %
assert.ok(u.alive, `${label(f)}: 不死`);
approx(u.hp, 1 + max * 0.5, `${label(f)}: 1 HP + 50 %`);
const lockStart = h.b.time;
// inside 6 s: a 60 % hit stops at exactly 50 %
// inside 7 s: a 60 % hit stops at exactly 50 %
h.b.dealDamage(e, u, { amount: max * 0.6, type: 'true' });
approx(u.hp, max * 0.5, `${label(f)}: held at 50 %`);
h.b.dealDamage(e, u, { amount: max * 0.3, type: 'true' });
approx(u.hp, max * 0.5, `${label(f)}: below 50 % nothing passes`);
h.b.loseHp(u, max * 0.1, { source: e });
approx(u.hp, max * 0.4, `${label(f)}: a 流失 is not held`);
h.run(6 - (h.b.time - lockStart) + 0.1);
h.run(7 - (h.b.time - lockStart) + 0.1);
h.b.dealDamage(e, u, { amount: max * 0.3, type: 'true' });
approx(u.hp, max * 0.1, `${label(f)}: the lock is over`);
h.b.dealDamage(e, u, { amount: max, type: 'true' });
@@ -241,19 +241,19 @@ test('T1 紧急除颤: 不死 until her HP first reaches ≤ 25 % — then +50 %
}
});
test('T2 严酷训练: 15 s after each deployment 抵抗 (control statuses last half as long); CEN-X stage 3 adds ATK +6 % after 30 s and ASPD +12 after 45 s (stage 1: nothing more)', () => {
test('T2 严酷训练: 12 s after each deployment 抵抗 (control statuses last half as long); CEN-X stage 3 adds ATK +6 % after 30 s and ASPD +12 after 45 s (stage 1: nothing more)', () => {
for (const f of [[5, false, null], [5, true, CENX], [6, true, CENX], [6, true, CENY]]) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const x3 = tier === 6 && mod === CENX;
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.run(14.8);
assert.equal(h.b.resistOf(u), 0, `${label(f)}: no 抵抗 before 15 s`);
h.run(11.8);
assert.equal(h.b.resistOf(u), 0, `${label(f)}: no 抵抗 before 12 s`);
h.run(0.4);
assert.equal(h.b.resistOf(u), 0.5, `${label(f)}: 抵抗 at 15 s`);
assert.equal(h.b.resistOf(u), 0.5, `${label(f)}: 抵抗 at 12 s`);
h.b.applyStatus(u, 'stun', { duration: 2, source: e });
approx(u.findBuff('stun').timeLeft, 1, `${label(f)}: a 2 s stun lasts 1 s`);
h.run(15);
h.run(18);
assert.equal(!!u.findBuff('talent:huang:atk'), x3, `${label(f)}: ATK +6 % after 30 s`);
if (x3) approx(u.s.atk, u.base.atk * 1.06, 'ATK +6 %');
h.run(15);
@@ -265,8 +265,8 @@ test('T2 严酷训练: 15 s after each deployment 抵抗 (control statuses last
h.b.redeploy(u);
h.step();
assert.deepEqual([h.b.resistOf(u), !!u.findBuff('talent:huang:atk')], [0, false], `${label(f)}: reset by the redeploy`);
h.run(15.2);
assert.equal(h.b.resistOf(u), 0.5, `${label(f)}: 抵抗 again 15 s later`);
h.run(12.2);
assert.equal(h.b.resistOf(u), 0.5, `${label(f)}: 抵抗 again 12 s later`);
done(h);
}
});
@@ -295,7 +295,7 @@ test('CEN-X (stages 1 and 3): ×1.1 ATK scale on every damage of hers to a block
}
});
test('CEN-Y stage 1: −20 % physical damage taken above 50 % HP (arts untouched); stage 3: 紧急除颤 at 50 % then a second trigger at 25 % (its own 不死, 8 s), and 150 DEF ignored while above 50 %', () => {
test('CEN-Y stage 1: −20 % physical damage taken above 50 % HP (arts untouched); stage 3: 紧急除颤 at 50 % then a second trigger at 25 % (its own 不死, 9 s), and 150 DEF ignored while above 50 %', () => {
for (const tier of [5, 6]) {
const { h, u } = field({ tier, elite: true, mod: CENY, skill: 0 });
assert.deepEqual(u.def.raw.trait.bb, { hp_ratio: 0.5, damage_resistance: 0.2 });
@@ -319,18 +319,18 @@ test('CEN-Y stage 1: −20 % physical damage taken above 50 % HP (arts untouched
approx(hp - u.hp, Math.max(1000 - u.s.def, 50), `T${tier}: at 50 % (not above) the full physical damage`);
done(h);
}
// stage 3: the talent fires at 50 % (8 s), the hidden part at 25 % — only once the first is spent
// stage 3: the talent fires at 50 % (9 s), the hidden part at 25 % — only once the first is spent
const { h, u } = field({ tier: 6, elite: true, mod: CENY, skill: 0 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
const hid = Object.assign({}, ...u.def.raw.talents.filter((t) => t.index === -1).map((t) => t.bb));
assert.deepEqual([t0.hp_ratio, t0['huang_t_1[lock].duration'], hid.def_penetrate_fixed, hid['huang_e_003[lock].check_hp_ratio'], hid['huang_e_003[lock].duration']], [0.5, 8, 150, 0.25, 8]);
assert.deepEqual([t0.hp_ratio, t0['huang_t_1[lock].duration'], hid.def_penetrate_fixed, hid['huang_e_003[lock].check_hp_ratio'], hid['huang_e_003[lock].duration']], [0.5, 9, 150, 0.25, 9]);
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.step();
assert.equal(u.s.defIgnoreFlat, 150, 'above 50 %: 150 DEF ignored');
const max = u.s.maxHp;
h.b.dealDamage(e, u, { amount: max * 0.6, type: 'true' }); // 100 % → 40 %: the 50 % trigger
approx(u.hp, max * 0.9, '50 % trigger: +50 %');
h.run(8.2);
h.run(9.2);
h.b.dealDamage(e, u, { amount: max * 0.3, type: 'true' }); // 90 % → 60 %
h.step();
assert.equal(u.s.defIgnoreFlat, 150);
@@ -342,8 +342,8 @@ test('CEN-Y stage 1: −20 % physical damage taken above 50 % HP (arts untouched
assert.ok(u.alive, 'the second 不死');
approx(u.hp, 1 + max * 0.5, '25 % trigger: +50 %');
h.b.dealDamage(e, u, { amount: max * 0.4, type: 'true' });
approx(u.hp, max * 0.5, 'its 8 s lock holds 50 %');
h.run(8.2);
approx(u.hp, max * 0.5, 'its 9 s lock holds 50 %');
h.run(9.2);
h.b.dealDamage(e, u, { amount: max, type: 'true' });
assert.ok(!u.alive, 'both spent');
done(h);
+12 -12
View File
@@ -67,8 +67,8 @@ test('伊芙利特 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// E2 Lv1 1276 / 722, E2 Lv60 1544 / 820; BLA-X +50 ATK +5 ASPD → +72 / +7, BLA-D +120 HP +45 ATK → +200 / +70
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], [1276, 722, 1544, 820]);
// E2 Lv1 1276 / 757, E2 Lv60 1544 / 855; BLA-X +50 ATK +5 ASPD → +72 / +7, BLA-D +120 HP +45 ATK → +200 / +70
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], [1276, 757, 1544, 855]);
assert.deepEqual([modOf(5, BLAX).attr, modOf(6, BLAX).attr, modOf(5, BLAD).attr, modOf(6, BLAD).attr], [{ atk: 50, aspd: 5 }, { atk: 72, aspd: 7 }, { maxHp: 120, atk: 45 }, { maxHp: 200, atk: 70 }]);
});
@@ -157,14 +157,14 @@ test('S3 灼地 (MANUAL, data DEFAULT, 20 s, 维持技能状态): no normal atta
assert.ok(h.runUntil(() => u.skill.active, 4), `T${tier}: cast`);
const n0 = atkHits(h, u).length, hp0 = u.hp, cast = h.hooksOf('skillStart').at(-1).t;
h.run(1.02);
// RES 50 × 0.6 (精神融解) − 7 / −10: (50 − cut) × 0.6
approx(g.s.res, (50 + sk.bb.magic_resistance) * 0.6, `T${tier}: RES cut + 精神融解`, 1e-9);
// RES 50 × 0.56 (精神融解) − 7 / −10: (50 − cut) × 0.56
approx(g.s.res, (50 + sk.bb.magic_resistance) * 0.56, `T${tier}: RES cut + 精神融解`, 1e-9);
approx(off.s.res, 50, `T${tier}: off the line`);
assert.deepEqual(u.liveRangeGrid, formOf(tier, elite).rangeGrid, `T${tier}: 5-1 while it runs`);
const ticks = () => tagged(h, u, 'ifrit:scorch').filter((c) => c.type === 'arts');
assert.equal(ticks().length, 1, `T${tier}: one tick after 1 s`);
approx(ticks()[0].t - cast, 0.4, `T${tier}: the first tick at the Begin clip's attack event`, 0.05);
approx(ticks()[0].amount, u.s.atk * sk.bb.atk_scale * (1 - (50 + sk.bb.magic_resistance) * 0.6 / 100), `T${tier}: ${sk.bb.atk_scale * 100} % ATK arts`);
approx(ticks()[0].amount, u.s.atk * sk.bb.atk_scale * (1 - (50 + sk.bb.magic_resistance) * 0.56 / 100), `T${tier}: ${sk.bb.atk_scale * 100} % ATK arts`);
assert.ok(!ticks().some((c) => c.target === fly || c.target === off), `T${tier}: ground enemies of her line only`);
approx(hp0 - u.hp, u.s.maxHp * 0.02, `T${tier}: 2 % max HP`);
h.runUntil(() => !u.skill.active, 25);
@@ -184,15 +184,15 @@ test('S3 灼地 (MANUAL, data DEFAULT, 20 s, 维持技能状态): no normal atta
}
});
test('T1 精神融解: RES ×0.6 on every enemy of her line (gone once it leaves), none off it; BLA-D stage 3: her attacks and 灼地 ticks on an enemy in its 灼燃 burst add 50 % ATK 元素伤害 (a 持续伤害)', () => {
test('T1 精神融解: RES ×0.56 on every enemy of her line (gone once it leaves), none off it; BLA-D stage 3: her attacks and 灼地 ticks on an enemy in its 灼燃 burst add 50 % ATK 元素伤害 (a 持续伤害)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const t0 = u.def.raw.talents.find((t) => t.index === 0);
assert.equal(t0.bb.magic_resistance, -0.4, label(f));
assert.equal(t0.bb.magic_resistance, -0.44, label(f));
const on = h.spawn('enemy_res', { pos: [10, 7] }), off = h.spawn('enemy_res', { pos: [11, 7] });
h.run(0.3);
approx(on.s.res, 30, `${label(f)}: 50 → 30`);
approx(on.s.res, 28, `${label(f)}: 50 → 28`);
approx(off.s.res, 50, `${label(f)}: off the line`);
h.b.retreat(u);
h.run(0.6);
@@ -225,18 +225,18 @@ test('T1 精神融解: RES ×0.6 on every enemy of her line (gone once it leaves
}
});
test('T2 莱茵回路: +2 SP every 6 s of a deployment; BLA-X stage 3: a 30 % roll for +5 SP more each 6 s (none in any other form)', () => {
test('T2 莱茵回路: +2 SP every 5.5 s of a deployment; BLA-X stage 3: a 30 % roll for +5 SP more each 5.5 s (none in any other form)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 21 });
const dice = elite && tier === 6 && mod === BLAX;
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1.sp, t1.interval, t1['ifrit_e_002[dice_sp].prob'] ?? 0, t1['ifrit_e_002[dice_sp].sp'] ?? 0], [2, 6, dice ? 0.3 : 0, dice ? 5 : 0], label(f));
assert.deepEqual([t1.sp, t1.interval, t1['ifrit_e_002[dice_sp].prob'] ?? 0, t1['ifrit_e_002[dice_sp].sp'] ?? 0], [2, 5.5, dice ? 0.3 : 0, dice ? 5 : 0], label(f));
const gains = () => h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'talent');
for (let i = 0; i < 60; i++) { u.skill.sp = 0; h.run(6); }
for (let i = 0; i < 60; i++) { u.skill.sp = 0; h.run(5.5); }
const g = gains();
const twos = g.filter((c) => c.amount === 2).length, fives = g.filter((c) => c.amount === 5).length;
assert.ok(twos >= 59 && twos <= 60, `${label(f)}: ${twos} gifts of 2 SP in 360 s`);
assert.ok(twos >= 59 && twos <= 60, `${label(f)}: ${twos} gifts of 2 SP in 330 s`);
if (dice) assert.ok(fives >= 9 && fives <= 30, `${label(f)}: ${fives} / 60 rolls of +5 ≈ 30 %`);
else assert.equal(fives, 0, `${label(f)}: no roll`);
done(h);
+6 -6
View File
@@ -73,9 +73,9 @@ test('艾丽妮 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2201 / 484 / 282, E2 Lv60 2688 / 575 / 326; SWO-Y +50 / +35 → +65 / +50
// the numbers of the forms (zh_CN, full potential): E2 Lv1 2201 / 507 / 282, E2 Lv60 2688 / 598 / 326; SWO-Y +50 / +35 → +65 / +50
// ATK / DEF, SWO-X +50 ATK +5 ASPD → +80 / +7, ISW-A +240 HP +40 ATK → +300 / +65
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], [2201, 484, 2688, 575]);
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], [2201, 507, 2688, 598]);
assert.deepEqual([SWOY, SWOX, ISWA].map((m) => [modOf(5, m).attr, modOf(6, m).attr]),
[[{ atk: 50, def: 35 }, { atk: 65, def: 50 }], [{ atk: 50, aspd: 5 }, { atk: 80, aspd: 7 }], [{ maxHp: 240, atk: 40 }, { maxHp: 300, atk: 65 }]]);
});
@@ -149,21 +149,21 @@ test('T1 审判之火 vs a ground enemy: one draw per physical instance at 50 %;
}
});
test('T2 净化之剑: ASPD +18 (SWO-Y stage 3: ATK +5 % too), doubled while a 【海怪】 enemy is on the field', () => {
test('T2 净化之剑: ASPD +21 (SWO-Y stage 3: ATK +5 % too), doubled while a 【海怪】 enemy is on the field', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
const y3 = elite && mod === SWOY && tier === 6;
const atk = y3 ? 0.05 : 0;
approx(u.s.aspd, u.base.aspd + 18, `${label(f)}: ASPD +18`);
approx(u.s.aspd, u.base.aspd + 21, `${label(f)}: ASPD +21`);
approx(u.s.atk, u.base.atk * (1 + atk), `${label(f)}: ATK`);
const sea = h.spawn('enemy_sea', { pos: [9, 9] });
h.step();
approx(u.s.aspd, u.base.aspd + 36, `${label(f)}: ×2 with a 海怪 on the field`);
approx(u.s.aspd, u.base.aspd + 42, `${label(f)}: ×2 with a 海怪 on the field`);
approx(u.s.atk, u.base.atk * (1 + 2 * atk), `${label(f)}: ATK ×2`);
h.b.kill(sea, null);
h.step();
approx(u.s.aspd, u.base.aspd + 18, `${label(f)}: back once it is gone`);
approx(u.s.aspd, u.base.aspd + 21, `${label(f)}: back once it is gone`);
done(h);
}
});
+2 -2
View File
@@ -167,8 +167,8 @@ test('战地工程师 / the device rules: stock 3 (CRA-X 4) after his deployment
}
});
test('节约经费: a device of his leaving his x-4 ⇒ prob (0.7; CRA-X stage 3 0.9) ⇒ +1 stock (capped); none when the roll fails, outside his x-4 or when it goes with him', () => {
for (const [tier, elite, mod, prob] of [[5, false, null, 0.7], [5, true, X, 0.7], [6, true, X, 0.9], [6, true, Y, 0.7]]) {
test('节约经费: a device of his leaving his x-4 ⇒ prob (0.8; CRA-X stage 3 1) ⇒ +1 stock (capped); none when the roll fails, outside his x-4 or when it goes with him', () => {
for (const [tier, elite, mod, prob] of [[5, false, null, 0.8], [5, true, X, 0.8], [6, true, X, 1], [6, true, Y, 0.8]]) {
const f = label([tier, elite, mod]);
const { h, u, devs } = field({ tier, elite, mod, skill: 0, pieces: [[11, 6], [10, 8]], dp: 99 });
const [near, far] = devs;
+9 -9
View File
@@ -85,9 +85,9 @@ test('涤火杰西卡 in every 自选 form: her operator kit (all three skills a
done(h);
}
}
// E2 Lv1 2589 / 433 / 522, E2 Lv60 3265 / 492 / 651; SPT-X +180 / +50 / +25 → +250 / +65 / +37, SPT-Y +180 / +52 → +325 / +83;
// the shield 3041 / 619, 3545 / 709, SPT-X stage 3 3925 / 788 and 60 s
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.def], [2589, 433, 3265, 651]);
// E2 Lv1 2589 / 461 / 522, E2 Lv60 3265 / 520 / 651 (full potential: ATK +28); SPT-X +180 / +50 / +25 → +250 / +65 / +37,
// SPT-Y +180 / +52 → +325 / +83; the shield 3041 / 619, 3545 / 709, SPT-X stage 3 3925 / 788 and 60 s
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.def], [2589, 461, 3265, 651]);
assert.deepEqual([modOf(5, SPTX).attr, modOf(6, SPTX).attr, modOf(5, SPTY).attr, modOf(6, SPTY).attr], [{ maxHp: 180, atk: 50, def: 25 }, { maxHp: 250, atk: 65, def: 37 }, { maxHp: 180, atk: 52 }, { maxHp: 325, atk: 83 }]);
assert.deepEqual([tokenStats(5, false).maxHp, tokenStats(5, true).maxHp, tokenStats(6, true, SPTX).maxHp, tokenStats(6, true, SPTX).def, tokenTalent(6, true, SPTX).duration, tokenTalent(5, true, SPTX).duration], [3041, 3545, 3925, 788, 60, 50]);
});
@@ -100,7 +100,7 @@ test('a 自选 pick: 涤火杰西卡 is offered at tiers 5 and 6 (she has a kit)
{ ok: true, picks: { [SLOT[6]]: { charId: JESCA, skillIndex: 2, uniEquipId: SPTY } } });
});
test('T1 灵活应变: she turns to face her shield (her range turns with her) and back once it leaves; while it stands next to her she and the unit on the tile behind her DEF +15 % (孤立 too), nobody else', () => {
test('T1 灵活应变: she turns to face her shield (her range turns with her) and back once it leaves; while it stands next to her she and the unit on the tile behind her DEF +18 % (孤立 too), nobody else', () => {
for (const f of [[5, false, null], [6, true, SPTX], [6, true, SPTY]]) {
const [tier, elite, mod] = f;
// shield above (11, 4): she faces UP; behind her is (9, 4); 德克萨斯 at (10, 3) was behind her RIGHT facing only
@@ -108,7 +108,7 @@ test('T1 灵活应变: she turns to face her shield (her range turns with her) a
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE, others });
const yak = h.unit(3), texas = h.unit(4);
const d = talentOf(tier, elite, mod, 0).def;
assert.equal(d, 0.15);
assert.equal(d, 0.18);
assert.deepEqual([u.dir, t.dir], ['UP', 'UP'], `${label(f)}: she faces her shield, it faces away from her`);
const ext = mod === SPTY ? 1 : 0;
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(3 + ext).keys()].map((i) => K(10 + i, 4))), `${label(f)}: her range turned UP`);
@@ -174,12 +174,12 @@ test('T1 灵活应变: the shield lasts 50 s (SPT-X stage 3: 60 s), comes back o
}
});
test('T2 蓄能释放: each damage instance on the shield (next to her) gives her 1 SP with 50 % (SPT-Y stage 3: 60 % and its next redeploy −1 s per success, ≤ 14 s); a 流失 does not count', () => {
test('T2 蓄能释放: each damage instance on the shield (next to her) gives her 1 SP with 55 % (SPT-Y stage 3: 65 % and its next redeploy −1 s per success, ≤ 14 s); a 流失 does not count', () => {
for (const f of [[5, false, null], [6, true, SPTY], [5, true, SPTY]]) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 1);
const y3 = tier === 6 && mod === SPTY;
assert.deepEqual([tb.prob, tb.sp, tb.respawn_time ?? 0, tb.respawn_time_max ?? 0], y3 ? [0.6, 1, 1, 14] : [0.5, 1, 0, 0], label(f));
assert.deepEqual([tb.prob, tb.sp, tb.respawn_time ?? 0, tb.respawn_time_max ?? 0], y3 ? [0.65, 1, 1, 14] : [0.55, 1, 0, 0], label(f));
const { h, u, t } = field({ tier, elite, mod, skill: 0, shield: ABOVE, seed: 17 });
// a shooter beside the shield and out of her range: it picks the shield (taunt 1), she cannot reach it
h.spawn('enemy_shooter', { pos: [12, 5] });
@@ -216,7 +216,7 @@ test('S1 坚守阵线 (AUTO, SP_FULL): at full SP with no enemy, 持续时间无
assert.ok(t.alive);
h.step();
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.15), `${label(f)}: DEF (+15 % T1)`);
approx(u.s.def, u.base.def * (1 + sk.bb.def + 0.18), `${label(f)}: DEF (+18 % T1)`);
approx(t.s.def, t.base.def * (1 + sk.bb.def), `${label(f)}: shield DEF`);
approx(t.mem.lifeEnd - t.deployedAt, life + 20, `${label(f)}: shield life +20 s`);
h.run(200);
@@ -264,7 +264,7 @@ test('S3 饱和迸射 (MANUAL, data ACTIVE_RANGE on 2-2 + 1): 20 bullets, range
assert.ok(h.runUntil(() => u.skill.active, 1), `${label(f)}: cast`);
assert.deepEqual(sortN(u.rangeKeys), sortN([...Array(4 + ext).keys()].map((i) => K(10, 4 - i))), `${label(f)}: +1 tile (${ext ? 'and SPT-Y\'s' : 'no module'})`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `${label(f)}: ATK`);
approx(u.s.def, u.base.def * (1 + sk.bb['jesca2_s_3[def].def'] + 0.15), `${label(f)}: DEF (+15 % T1)`);
approx(u.s.def, u.base.def * (1 + sk.bb['jesca2_s_3[def].def'] + 0.18), `${label(f)}: DEF (+18 % T1)`);
approx(u.s.interval, 1.8, `${label(f)}: 1.2 + 0.6 s`);
approx(t.s.def, t.base.def * (1 + sk.bb['jesca2_s_3_token[def].def']), `${label(f)}: shield DEF`);
assert.deepEqual([sk.bb.atk, sk.bb['jesca2_s_3[def].def'], sk.bb['jesca2_s_3_token[def].def']], elite ? [2.7, 0.65, 1.35] : [2.4, 0.5, 1], label(f));
+6 -6
View File
@@ -83,8 +83,8 @@ test('斥罪 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// zh_CN numbers: E2 Lv1 2960 / 683 / 474, E2 Lv60 3686 / 811 / 568; UNY-X +150/+35/+15 → +300/+50/+33, UNY-Y +60/+48 → +85/+70
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], [2960, 683, 3686, 811]);
// zh_CN numbers (full potential): E2 Lv1 2960 / 711 / 474, E2 Lv60 3686 / 839 / 568; UNY-X +150/+35/+15 → +300/+50/+33, UNY-Y +60/+48 → +85/+70
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], [2960, 711, 3686, 839]);
assert.deepEqual([modOf(5, UNYX).attr, modOf(6, UNYX).attr, modOf(5, UNYY).attr, modOf(6, UNYY).attr],
[{ maxHp: 150, atk: 35, def: 15 }, { maxHp: 300, atk: 50, def: 33 }, { atk: 60, def: 48 }, { atk: 85, def: 70 }]);
});
@@ -97,13 +97,13 @@ test('a 自选 pick: 斥罪 is offered at tiers 5 and 6 (she has a kit) and a ro
{ ok: true, picks: { [SLOT[5]]: { charId: JUDGE, skillIndex: 0, uniEquipId: UNYY } } });
});
test('T1 律法卫士: barrier 50 % max HP at every deployment (70 % UNY-X stage 3), +10 % per enemy she knocks out (12 %), never past 300 % of the current max HP; it absorbs before HP', () => {
test('T1 律法卫士: barrier 55 % max HP at every deployment (75 % UNY-X stage 3), +10 % per enemy she knocks out (12 %), never past 300 % of the current max HP; it absorbs before HP', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
const t0 = t0Of(u);
const x3 = elite && mod === UNYX && tier === 6;
assert.deepEqual([t0.born_hp_ratio, t0.kill_hp_ratio, t0.max_hp_ratio], x3 ? [0.7, 0.12, 3] : [0.5, 0.1, 3], label(f));
assert.deepEqual([t0.born_hp_ratio, t0.kill_hp_ratio, t0.max_hp_ratio], x3 ? [0.75, 0.12, 3] : [0.55, 0.1, 3], label(f));
approx(barrier(u), u.s.maxHp * t0.born_hp_ratio, `${label(f)}: at the deployment`);
killOne(h, u);
approx(barrier(u), u.s.maxHp * (t0.born_hp_ratio + t0.kill_hp_ratio), `${label(f)}: +1 kill`);
@@ -133,12 +133,12 @@ test('T1 律法卫士: barrier 50 % max HP at every deployment (70 % UNY-X stage
}
});
test('T2 荆棘环身: while she holds barrier, every damage from an enemy deals 50 % ATK arts back (58 % UNY-Y stage 3) — the hit that breaks it too; none without barrier, for a 流失, a 无来源 hit or an ally', () => {
test('T2 荆棘环身: while she holds barrier, every damage from an enemy deals 53 % ATK arts back (61 % UNY-Y stage 3) — the hit that breaks it too; none without barrier, for a 流失, a 无来源 hit or an ally', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, others: [{ uid: 2, chessId: 't_mate', row: 12, col: 3 }] });
const t1 = t1Of(u);
assert.equal(t1.atk_scale, elite && mod === UNYY && tier === 6 ? 0.58 : 0.5, label(f));
assert.equal(t1.atk_scale, elite && mod === UNYY && tier === 6 ? 0.61 : 0.53, label(f));
const e = h.spawn('enemy_dummy', { pos: [12, 9] });
h.b.dealDamage(e, u, { amount: 100, type: 'phys' });
const t = thorns(h, u, e);
+4 -4
View File
@@ -79,9 +79,9 @@ test('凯尔希 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// E2 Lv1 1469 / 392 / 172, E2 Lv60 1578 / 457 / 201; PHY-X +35 / +15 → +60 / +25 ATK / DEF, PHY-Y +115 / +5 → +195 / +7
// E2 Lv1 1469 / 417 / 172, E2 Lv60 1578 / 482 / 201 (full potential); PHY-X +35 / +15 → +60 / +25 ATK / DEF, PHY-Y +115 / +5 → +195 / +7
// HP / ASPD, ISW-A +24 / +5 → +50 / +7 ATK / ASPD; Mon3tr 4292 / 1149 / 336 → 5048 / 1317 / 382
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], [1469, 392, 1578, 457]);
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], [1469, 417, 1578, 482]);
assert.deepEqual([modOf(5, PHYX).attr, modOf(6, PHYX).attr, modOf(5, PHYY).attr, modOf(6, PHYY).attr, modOf(5, ISWA).attr, modOf(6, ISWA).attr],
[{ atk: 35, def: 15 }, { atk: 60, def: 25 }, { maxHp: 115, aspd: 5 }, { maxHp: 195, aspd: 7 }, { atk: 24, aspd: 5 }, { atk: 50, aspd: 7 }]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(5, false).stats.atk, variantOf(6, true).stats.maxHp], [4292, 1149, 5048]);
@@ -168,12 +168,12 @@ test('T1 Mon3tr: DEF ×0 off her range (PHY-Y stage 3: inside ASPD +20, DEF +20
}
});
test('T2 不毁重构: Mon3tr knocked out ⇒ every enemy of its 3×3 (air units too) takes 1200 true damage (溅射) and a 3 s stun; PHY-X stage 3: 1500 / 3.5 s, and once per deployment when a hit leaves it at or below half HP', () => {
test('T2 不毁重构: Mon3tr knocked out ⇒ every enemy of its 3×3 (air units too) takes 1400 true damage (溅射) and a 3.5 s stun; PHY-X stage 3: 1700 / 4 s, and once per deployment when a hit leaves it at or below half HP', () => {
for (const f of [[5, false, null], [6, true, PHYX], [5, true, PHYX], [6, true, PHYY]]) {
const [tier, elite, mod] = f;
const rb = rattleOf(tier, elite, mod);
const x3 = tier === 6 && mod === PHYX;
assert.deepEqual([rb.value, rb.stun, rb.hp_ratio ?? 0], x3 ? [1500, 3.5, 0.5] : [1200, 3, 0], label(f));
assert.deepEqual([rb.value, rb.stun, rb.hp_ratio ?? 0], x3 ? [1700, 4, 0.5] : [1400, 3.5, 0], label(f));
const { h, m } = field({ tier, elite, mod, skill: 0 });
const e1 = h.spawn('enemy_dummy', { pos: [11, 5] }), e2 = h.spawn('enemy_fly', { pos: [9, 3] }), far = h.spawn('enemy_dummy', { pos: [10, 6] });
h.step(); // the enemies on the tile index
+11 -11
View File
@@ -72,9 +72,9 @@ test('凯尔希·思衡托 in every 自选 form: her operator kit (all three ski
const { h, u, a } = field({ tier, elite, skill, anchor: { row: 10, col: 9 } });
const form = formOf(tier, elite), t0 = talentOf(tier, elite, 0);
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, !!u.kit.generic, u.kit.skillSource], [KALTS2, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk, form.stats.def, 2.85, 19], `${label(f)}: stats`);
approx(u.s.maxHp, form.stats.maxHp * (1 + t0.max_hp), `${label(f)}: 生命上限 +25 %`);
approx(u.s.def, form.stats.def * (1 + t0.def), `${label(f)}: 防御力 +25 %`);
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk, form.stats.def, 2.85, 17], `${label(f)}: stats`);
approx(u.s.maxHp, form.stats.maxHp * (1 + t0.max_hp), `${label(f)}: 生命上限 +30 %`);
approx(u.s.def, form.stats.def * (1 + t0.def), `${label(f)}: 防御力 +30 %`);
assert.deepEqual([u.s.blockCnt, u.s.blockRadiusScale, t0.block_cnt, t0.block_radius_scale], [2, 0.23, 1, 0.23], `${label(f)}: 阻挡数 +1, 阻挡半径倍率 +0.23`);
assert.deepEqual([u.profile.dmgType, u.profile.heal, u.profile.attack, u.def.subProf], ['heal', { mode: 'single' }, 'ranged', 'watchman'], `${label(f)}: 守望者 heal`);
assert.deepEqual([u.s.flags.liftoff, u.s.flags.blockFly], [true, true], `${label(f)}: 起飞`);
@@ -88,8 +88,8 @@ test('凯尔希·思衡托 in every 自选 form: her operator kit (all three ski
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1393 / 408 / 220, E2 Lv60 1543 / 476 / 257; the anchor 2235 HP, 3 DP, 70 s
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/3'].stats.atk], [1393, 408, 1543, 476]);
// the numbers of the forms (zh_CN): E2 Lv1 1393 / 433 / 220, E2 Lv60 1543 / 501 / 257; the anchor 2235 HP, 3 DP, 70 s
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/3'].stats.atk], [1393, 433, 1543, 501]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(6, true).stats.cost, TOKEN.placeable], [2235, 3, true]);
});
@@ -139,11 +139,11 @@ test('T1 遗尘守望 起飞: no ground enemy targets her or is blocked by her;
done(h);
});
test('T2 医者丰碑: an operator entering her range gets 1 护盾 layer (one damage instance blocked) and 生命回复速度 +50 for 30 s (罗德岛 +100); summons and she get nothing; a new entry refreshes, never a 2nd layer', () => {
test('T2 医者丰碑: an operator entering her range gets 1 护盾 layer (one damage instance blocked) and 生命回复速度 +60 for 30 s (罗德岛 +120); summons and she get nothing; a new entry refreshes, never a 2nd layer', () => {
for (const f of [[5, false], [6, true]]) {
const [tier, elite] = f;
const t1 = talentOf(tier, elite, 1);
assert.deepEqual([t1.buff_duration, t1.hp_recovery_per_sec, t1.rhodes_bonus], [30, 50, 2], label(f));
assert.deepEqual([t1.buff_duration, t1.hp_recovery_per_sec, t1.rhodes_bonus], [30, 60, 2], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }, { uid: 4, chessId: PERFUMER, row: 9, col: 5 },
{ uid: 5, chessId: TEXAS, row: 12, col: 8 }, { uid: 6, diy: { slot: 6, charId: 'char_003_kalts', skillIndex: 0 }, row: 12, col: 2 },
{ uid: 7, kind: 'token', tokenId: 'token_10002_kalts_mon3tr', ownerUid: 6, row: 11, col: 5 }];
@@ -151,7 +151,7 @@ test('T2 医者丰碑: an operator entering her range gets 1 护盾 layer (one d
const yak = h.unit(3), pf = h.unit(4), tx = h.unit(5), m = h.unit(7);
assert.equal(pf.def.raw.nationId, 'rhodes');
assert.ok(yak.findBuff('talent:kalts2:shield') && yak.findBuff('talent:kalts2:shield').shieldHits === 1, `${label(f)}: 角峰 1 护盾 layer`);
assert.deepEqual([yak.findBuff('talent:kalts2:regen')?.mods, pf.findBuff('talent:kalts2:regen')?.mods], [{ hpRegen: 50 }, { hpRegen: 100 }], `${label(f)}: 50 / 罗德岛 100 HP/s (生命回复速度)`);
assert.deepEqual([yak.findBuff('talent:kalts2:regen')?.mods, pf.findBuff('talent:kalts2:regen')?.mods], [{ hpRegen: 60 }, { hpRegen: 120 }], `${label(f)}: 60 / 罗德岛 120 HP/s (生命回复速度)`);
approx(yak.findBuff('talent:kalts2:regen').timeLeft, 30, `${label(f)}: 30 s`, 0.01);
assert.ok(!m.findBuff('talent:kalts2:shield') && !m.findBuff('talent:kalts2:regen'), `${label(f)}: no summon`);
assert.ok(!u.findBuff('talent:kalts2:shield') && !u.findBuff('talent:kalts2:regen'), `${label(f)}: not herself`);
@@ -179,8 +179,8 @@ test('T2 医者丰碑: an operator entering her range gets 1 护盾 layer (one d
const r0 = tx.s.hpRegen; // (调香师's own 熏衣草 regen on everyone)
assert.ok(h.b.relocate(tx, 9, 3));
h.step();
approx(tx.s.hpRegen - r0, 50, `${label(f)}: 德克萨斯 enters`);
assert.deepEqual(tx.findBuff('talent:kalts2:regen')?.mods, { hpRegen: 50 });
approx(tx.s.hpRegen - r0, 60, `${label(f)}: 德克萨斯 enters`);
assert.deepEqual(tx.findBuff('talent:kalts2:regen')?.mods, { hpRegen: 60 });
done(h);
}
});
@@ -328,7 +328,7 @@ test('S3 with her anchor: 0.1 s after the cast she flies to it (hidden, 无敌,
assert.equal(u.mem.summonStock[ANCHOR], 0, `${label(f)}: stock emptied`);
assert.ok(u.skill.active, label(f));
approx(u.skill.timeLeft, tl, `${label(f)}: the remaining time kept`, 0.1);
assert.equal(sil.findBuff('talent:kalts2:regen')?.mods.hpRegen, 50, `${label(f)}: 赫默, 2.0 from her flight, entered T2`);
assert.equal(sil.findBuff('talent:kalts2:regen')?.mods.hpRegen, 60, `${label(f)}: 赫默, 2.0 from her flight, entered T2`);
h.runUntil(() => !u.skill.active, 40);
h.run(5);
assert.ok(!a.alive, `${label(f)}: no anchor back after the skill (stock 0)`);
+21 -18
View File
@@ -84,8 +84,8 @@ test('司霆惊蛰 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// E2 Lv1 3206 / 306 / 417, E2 Lv60 3642 / 342 / 466; LIB-X +225 / +26 / +27 → +360 / +42 / +36 (HP / ATK / 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], [3206, 306, 3642, 342]);
// E2 Lv1 3206 / 335 / 417, E2 Lv60 3642 / 371 / 466 (full potential); LIB-X +225 / +26 / +27 → +360 / +42 / +36 (HP / ATK / 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], [3206, 335, 3642, 371]);
assert.deepEqual([modOf(5, LIBX).attr, modOf(6, LIBX).attr], [{ maxHp: 225, atk: 26, def: 27 }, { maxHp: 360, atk: 42, def: 36 }]);
});
@@ -168,11 +168,11 @@ test('T1 明断 strikes: every 1.0333 s each tile of her range counts — a stru
}
});
test('T1: ×1.07 (LIB-X stage 3: ×1.13) on everything she deals while a skill runs — an atkScaleMul buff for the skill\'s time', () => {
test('T1: ×1.11 (LIB-X stage 3: ×1.17) on everything she deals while a skill runs — an atkScaleMul buff for the skill\'s time', () => {
for (const f of [[5, false, null], [6, true, null], [5, true, LIBX], [6, true, LIBX]]) {
const [tier, elite, mod] = f;
const up = tOf(tier, elite, mod, 0)['atk_scale[skill_up]'];
assert.equal(up, elite && mod === LIBX && tier === 6 ? 1.13 : 1.07, label(f));
assert.equal(up, elite && mod === LIBX && tier === 6 ? 1.17 : 1.11, label(f));
const { h, u, log } = field({ tier, elite, mod, skill: 1 });
approx(u.s.atkScaleMul, 1, `${label(f)}: off`);
u.skill.gainSp(999);
@@ -189,9 +189,9 @@ test('T1: ×1.07 (LIB-X stage 3: ×1.13) on everything she deals while a skill r
}
});
test('T2 追责: every cast strikes each low tile of her range ring by ring from her tile (0.25 s apart; S1 0.07 s), landing 0.15 s later: 100 % ATK × skill bonus arts and 2 s 战栗 on every enemy there, air units too', () => {
test('T2 追责: every cast strikes each low tile of her range ring by ring from her tile (0.25 s apart; S1 0.07 s), landing 0.15 s later: 100 % ATK × skill bonus arts and 3 s 战栗 on every enemy there, air units too', () => {
const t1 = tOf(5, false, null, 1);
assert.deepEqual([t1.atk_scale_t2, t1.not_combat], [1, 2]);
assert.deepEqual([t1.atk_scale_t2, t1.not_combat], [1, 3]);
for (const [skill, gap, pos, d] of [[2, 0.25, [10, 7], 2], [0, 0.07, [10, 6], 1], [2, 0.25, [11, 6], 2]]) {
const { h, u, log } = field({ tier: 5, skill });
const e = h.spawn(pos[0] === 11 ? 'enemy_fly' : 'enemy_dummy', { pos });
@@ -203,11 +203,11 @@ test('T2 追责: every cast strikes each low tile of her range ring by ring from
const hits = log.filter((x) => x.e === e && tagged(x, 'leizi2:accuse'));
assert.equal(hits.length, 1, `S${skill + 1}: one strike on ${pos}`);
approx(hits[0].t - t0, d * gap + 0.15, `S${skill + 1}: ring ${d}`, 0.04);
approx(hits[0].amount, hits[0].atk * 1.07, `S${skill + 1}: 100 % ATK × 1.07`);
approx(hits[0].amount, hits[0].atk * 1.11, `S${skill + 1}: 100 % ATK × 1.11`);
assert.equal(hits[0].type, 'arts');
const tr = h.hooksOf('statusApplied').filter((c) => c.status === 'tremble' && c.target === e && c.source === u);
assert.equal(tr.length, 1, `S${skill + 1}: 战栗`);
approx(tr[0].duration, 2, 'for 2 s');
approx(tr[0].duration, 3, 'for 3 s');
done(h);
}
// S2: one cast per low tile of the flood fill (19 on the flat stage at (10,5)) — each gives a +10 % stack at once
@@ -223,7 +223,7 @@ test('T2 追责: every cast strikes each low tile of her range ring by ring from
done(h);
});
test('S1 浩气长存 (MANUAL, 3 charges, data ACTIVE_RANGE — the SEARCH row on her larger 3-19): a 0.5 s skill on the 3-19; 0.3 s in, a 3-2 to her left, front and right each hits every ground enemy on it for 260 % / 300 % ATK × 1.07 physical (one on her tile three times, none diagonal, no flyer)', () => {
test('S1 浩气长存 (MANUAL, 3 charges, data ACTIVE_RANGE — the SEARCH row on her larger 3-19): a 0.5 s skill on the 3-19; 0.3 s in, a 3-2 to her left, front and right each hits every ground enemy on it for 260 % / 300 % ATK × 1.11 physical (one on her tile three times, none diagonal, no flyer)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
const { h, u, log } = field({ tier, elite, skill: 0, silence: true });
@@ -242,7 +242,7 @@ test('S1 浩气长存 (MANUAL, 3 charges, data ACTIVE_RANGE — the SEARCH row o
assert.deepEqual([n(own), n(front), n(left), n(right), n(diag), n(fly)], [3, 1, 1, 1, 0, 0], `T${tier}: who is hit`);
for (const x of s1) {
approx(x.t - t0, 0.3, `T${tier}: 0.3 s in`, 0.04);
approx(x.amount, x.atk * sk.bb['attack@atk_scale_s1'] * 1.07, `T${tier}: ${sk.bb['attack@atk_scale_s1'] * 100} % × 1.07`);
approx(x.amount, x.atk * sk.bb['attack@atk_scale_s1'] * 1.11, `T${tier}: ${sk.bb['attack@atk_scale_s1'] * 100} % × 1.11`);
assert.equal(x.type, 'phys');
}
assert.equal(log.filter((x) => x.isAttack).length, 0, `T${tier}: no normal attack`);
@@ -270,7 +270,7 @@ test('S1: "充能耗尽前特性不重置" — the ramp is kept while a charge r
done(h);
});
test('S2 正霆摄威 (MANUAL, data SEARCH): range = the low tiles 3 four-way steps from hers (high ground and the field\'s edge stop it), 3 targets with air units for 110 % / 125 % ATK × 1.07, +10 % ATK per lightning cast up to 25, all gone after 36 s', () => {
test('S2 正霆摄威 (MANUAL, data SEARCH): range = the low tiles 3 four-way steps from hers (high ground and the field\'s edge stop it), 3 targets with air units for 110 % / 125 % ATK × 1.11, +10 % ATK per lightning cast up to 25, all gone after 36 s', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.bb['attack@atk_scale_s2'], sk.bb['attack@max_target_s2'], sk.bb.thunder_atk, sk.bb.thunder_max_stack_cnt, sk.duration, sk.rangeId], [elite ? 1.25 : 1.1, 3, 0.1, 25, 36, null]);
@@ -293,7 +293,7 @@ test('S2 正霆摄威 (MANUAL, data SEARCH): range = the low tiles 3 four-way st
const ids = new Set(atk.filter((x) => x.attackId === atk[0].attackId).map((x) => x.e));
assert.equal(ids.size, 3, `T${tier}: 3 targets`);
assert.ok(atk.some((x) => x.e === fly), `T${tier}: a flyer among them`);
for (const x of atk) approx(x.amount, x.atk * sk.bb['attack@atk_scale_s2'] * 1.07, `T${tier}: ${sk.bb['attack@atk_scale_s2'] * 100} % × 1.07`);
for (const x of atk) approx(x.amount, x.atk * sk.bb['attack@atk_scale_s2'] * 1.11, `T${tier}: ${sk.bb['attack@atk_scale_s2'] * 100} % × 1.11`);
h.run(30);
const st = u.findBuff('skill:leizi2:thunderAtk');
assert.equal(st.stacks, 25, `T${tier}: 25 stacks at most`);
@@ -306,7 +306,7 @@ test('S2 正霆摄威 (MANUAL, data SEARCH): range = the low tiles 3 four-way st
}
});
test('S3 天地通明 (MANUAL, data CUSTOM_RANGE on 2-1): range 2-1, attack interval 1.2 + 1.7 s, 210 % / 240 % ATK × 1.07 on the target and everyone within 1.65 (air too); then four currents', () => {
test('S3 天地通明 (MANUAL, data CUSTOM_RANGE on 2-1): range 2-1, attack interval 1.2 + 1.7 s, 210 % / 240 % ATK × 1.11 on the target and everyone within 1.65 (air too); then four currents', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
assert.deepEqual([sk.bb['attack@atk_scale_s3'], sk.bb.base_attack_time, sk.bb['attack@range_radius'], sk.bb['attack@atk_scale_current'], sk.bb.prob, sk.bb.not_combat, sk.duration, sk.rangeId],
@@ -324,12 +324,12 @@ test('S3 天地通明 (MANUAL, data CUSTOM_RANGE on 2-1): range 2-1, attack inte
const a0 = log.find((x) => x.isAttack && x.e === e);
const one = log.filter((x) => x.isAttack && x.attackId === a0.attackId);
assert.deepEqual(new Set(one.map((x) => x.e)), new Set([e, side, flyNear]), `T${tier}: the target and everyone within 1.65`);
for (const x of one) approx(x.amount, x.atk * sk.bb['attack@atk_scale_s3'] * 1.07, `T${tier}: ${sk.bb['attack@atk_scale_s3'] * 100} % × 1.07`);
for (const x of one) approx(x.amount, x.atk * sk.bb['attack@atk_scale_s3'] * 1.11, `T${tier}: ${sk.bb['attack@atk_scale_s3'] * 100} % × 1.11`);
done(h);
}
});
test('S3 currents: four from the target\'s spot (up, right, down, left; 0.28 s apart) at 1 tile/s for 3.1 s, bouncing off high ground and her own tile; 45 % / 56 % ATK × 1.07 arts on ground enemies of their tile, at most every 0.6 s per current; 战栗 7 % / 10 % for 3 s', () => {
test('S3 currents: four from the target\'s spot (up, right, down, left; 0.28 s apart) at 1 tile/s for 3.1 s, bouncing off high ground and her own tile; 45 % / 56 % ATK × 1.11 arts on ground enemies of their tile, at most every 0.6 s per current; 战栗 7 % / 10 % for 3 s', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S3);
const { h, u, log } = field({ tier, elite, skill: 2 });
@@ -354,17 +354,20 @@ test('S3 currents: four from the target\'s spot (up, right, down, left; 0.28 s a
const zap = log.filter((x) => tagged(x, 'leizi2:current'));
assert.ok(zap.length > 0 && zap.every((x) => x.e !== fly), `T${tier}: ground only`);
for (const x of zap) { approx(x.amount, x.atk * sk.bb['attack@atk_scale_current'] * x.mul, `T${tier}: ${sk.bb['attack@atk_scale_current'] * 100} %`); assert.equal(x.type, 'arts'); }
approx(zap[0].mul, 1.07, `T${tier}: × 1.07 while S3 runs`);
approx(zap[0].mul, 1.11, `T${tier}: × 1.11 while S3 runs`);
done(h);
}
// 战栗: about prob of the current hits while S3 runs, 3 s each
// 战栗: about prob of the current hits while S3 runs, 3 s each (T2's strikes at the cast give a 3 s 战栗 too at full
// potential: those are not counted)
const { h, u, log } = field({ tier: 6, elite: true, skill: 2, seed: 9 });
u.skill.gainSp(999);
for (const pos of [[10, 6], [10, 7], [9, 7], [11, 7], [10, 8], [12, 7], [9, 6]]) h.spawn('enemy_dummy', { pos });
assert.ok(h.runUntil(() => u.skill.active, 2));
h.run(23);
const zaps = log.filter((x) => tagged(x, 'leizi2:current')).length;
const tr = h.hooksOf('statusApplied').filter((c) => c.status === 'tremble' && c.source === u && c.duration === 3);
const accused = log.filter((x) => tagged(x, 'leizi2:accuse'));
const tr = h.hooksOf('statusApplied').filter((c) => c.status === 'tremble' && c.source === u && c.duration === 3
&& !accused.some((x) => x.e === c.target && x.t === c.t));
assert.ok(zaps > 60, `${zaps} current hits`);
assert.ok(tr.length / zaps > 0.04 && tr.length / zaps < 0.18, `${tr.length} / ${zaps} ≈ 10 %`);
done(h);
+2 -2
View File
@@ -116,13 +116,13 @@ test('T1 苦痛专注: while he blocks, physical / arts damage from anyone but a
}
});
test('T2 痛楚砺刃: any damage aimed at him (a dodged one too) ⇒ ATK +12 % (DRE-X stage 3: +20 %) for 15 s, refreshed, never stacked', () => {
test('T2 痛楚砺刃: any damage aimed at him (a dodged one too) ⇒ ATK +16 % (DRE-X stage 3: +24 %) for 15 s, refreshed, never stacked', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod });
const t = talentOf(u, 1);
const x3 = elite && mod === DREX && tier === 6;
assert.deepEqual([t.atk, t.add_atk_duration], [x3 ? 0.2 : 0.12, 15], `${label(f)}: the talent of the form`);
assert.deepEqual([t.atk, t.add_atk_duration], [x3 ? 0.24 : 0.16, 15], `${label(f)}: the talent of the form`);
const e = h.spawn('enemy_dummy', { pos: [11, 8] });
assert.equal(u.findBuff(PAIN_KEY), null);
h.b.addBuff(u, { key: 'test:dodge', mods: { dodgePhys: 1 } });
+6 -6
View File
@@ -69,8 +69,8 @@ test('林 in every 自选 form: her operator kit (all three skills authored), th
done(h);
}
}
// E2 Lv1 1679 / 730 / 212, E2 Lv60 1924 / 809 / 232; PLX-X +62 / +45 → +77 / +53 ATK / DEF, PLX-Y +50 / +20 → +73 / +40
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], [1679, 730, 1924, 809]);
// E2 Lv1 1679 / 764 / 212, E2 Lv60 1924 / 843 / 232; PLX-X +62 / +45 → +77 / +53 ATK / DEF, PLX-Y +50 / +20 → +73 / +40
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], [1679, 764, 1924, 843]);
assert.deepEqual([modOf(5, PLXX).attr, modOf(6, PLXX).attr, modOf(5, PLXY).attr, modOf(6, PLXY).attr], [{ atk: 62, def: 45 }, { atk: 77, def: 53 }, { atk: 50, def: 20 }, { atk: 73, def: 40 }]);
});
@@ -200,7 +200,7 @@ test('S3 流光乍裂 (MANUAL, data CUSTOM_RANGE x-2, 26 / 28 s): cast with an e
}
});
test('T1 计出万全 (every form): a 琉璃璧 from deployment — a hit ≤ 200 absorbed whole, a hit over 200 goes through less 200 and breaks it: stun 1 s first, then 100 % / 110 % ATK arts on her range, regrowing after 8 s / 6 s (PLX-Y stage 3); a lethal hit still breaks it', () => {
test('T1 计出万全 (every form): a 琉璃璧 from deployment — a hit ≤ 200 absorbed whole, a hit over 200 goes through less 200 and breaks it: stun 1 s first, then 110 % / 120 % ATK arts on her range, regrowing after 8 s / 6 s (PLX-Y stage 3); a lethal hit still breaks it', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const order = [];
@@ -210,7 +210,7 @@ test('T1 计出万全 (every form): a 琉璃璧 from deployment — a hit ≤ 20
} });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
const y3 = elite && tier === 6 && mod === PLXY;
assert.deepEqual([t0.value, t0.atk_scale, t0.stun, t0.interval], [200, y3 ? 1.1 : 1, 1, y3 ? 6 : 8], label(f));
assert.deepEqual([t0.value, t0.atk_scale, t0.stun, t0.interval], [200, y3 ? 1.2 : 1.1, 1, y3 ? 6 : 8], label(f));
assert.equal(glassOf(u)?.shield, 200, `${label(f)}: up from deployment`);
const e = h.spawn('enemy_dummy', { pos: [10, 7] }), out = h.spawn('enemy_dummy', { pos: [10, 9] });
h.step();
@@ -240,13 +240,13 @@ test('T1 计出万全 (every form): a 琉璃璧 from deployment — a hit ≤ 20
}
});
test('T2 韬光: each damage instance she takes rolls 50 % for +1 SP (PLX-X stage 3: 75 %, +2) — one the 琉璃璧 absorbs whole too; a 流失 never', () => {
test('T2 韬光: each damage instance she takes rolls 55 % for +1 SP (PLX-X stage 3: 80 %, +2) — one the 琉璃璧 absorbs whole too; a 流失 never', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2, seed: 33 });
const x3 = elite && tier === 6 && mod === PLXX;
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1.prob, t1.sp], x3 ? [0.75, 2] : [0.5, 1], label(f));
assert.deepEqual([t1.prob, t1.sp], x3 ? [0.8, 2] : [0.55, 1], label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 9] });
h.step();
const gains = () => h.hooksOf('spGain').filter((c) => c.unit === u && c.reason === 'talent');
+7 -7
View File
@@ -74,8 +74,8 @@ test('令 in every 自选 form: her kit (all three skills authored), the form\'s
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 852 / 406 / 114, E2 Lv60 1002 / 450 / 130; SUM-Y +100 / +30 → +150 / +50 HP / ATK
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], [852, 406, 1002, 450]);
// the numbers of the forms (zh_CN): E2 Lv1 852 / 430 / 114, E2 Lv60 1002 / 474 / 130; SUM-Y +100 / +30 → +150 / +50 HP / ATK
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], [852, 430, 1002, 474]);
assert.deepEqual([modOf(5, SUMY).attr, modOf(6, SUMY).attr], [{ maxHp: 100, atk: 30 }, { maxHp: 150, atk: 50 }]);
});
@@ -127,14 +127,14 @@ test('T1 挑灯问梦: her skill\'s summon is her piece; the deck — 5 held, ca
assert.deepEqual(SOUL_OF.map((t, i) => [tokOf(t, 6, true, i, SUMY).stats.maxHp - tokOf(t, 6, true, i, null).stats.maxHp, tokOf(t, 6, true, i, SUMY).stats.atk - tokOf(t, 6, true, i, null).stats.atk]), [[150, 45], [100, 35], [250, 60]]);
});
test('T2 随付笺咏醉屠苏: a summon knocked out, recalled or absorbed ⇒ +3 SP and a stack of ATK +3 % (at most 5); not when her summons leave with her; no SP while a timed skill of hers runs', () => {
test('T2 随付笺咏醉屠苏: a summon knocked out, recalled or absorbed ⇒ +4 SP and a stack of ATK +3 % (at most 5); not when her summons leave with her; no SP while a timed skill of hers runs', () => {
const { h, u, ss } = field({ tier: 6, elite: true, skill: 0, souls: [[Q, 10, 6], [Q, 11, 6], [Q, 9, 6]] });
const t1 = formOf(6, true).talents.find((t) => t.index === 1).bb;
assert.deepEqual(t1, { max_stack_cnt: 5, sp: 3, atk: 0.03 });
assert.deepEqual(t1, { max_stack_cnt: 5, sp: 4, atk: 0.03 });
const atk0 = u.s.atk;
const sp0 = u.skill.sp;
h.b.kill(ss[0], null);
approx(u.skill.sp - sp0, 3, '+3 SP', 1e-3);
approx(u.skill.sp - sp0, 4, '+4 SP', 1e-3);
assert.equal(u.findBuff('ling:t2').stacks, 1, 'one stack');
approx(u.s.atk, atk0 * 1.03, 'ATK +3 %');
for (let i = 0; i < 6; i++) { h.b.kill(ss[1 + (i % 2)], null); h.runUntil(() => ss.every((s) => s.alive), 15); }
@@ -222,14 +222,14 @@ test('S2 笑鸣瑟 (2 charges): her cast-attack strikes 2 enemies for 310 % / 37
const struck = new Set(h.hooksOf('damaged').filter((x) => x.dmg?.isSkill).map((x) => x.target));
assert.ok(struck.size >= 2 && [...struck].every((e) => e.s.flags.bind), 'every struck enemy bound');
void a; void b; void c;
// 1.2 s after the cast: the summon below half HP is recalled (T2: +3 SP, a stack)
// 1.2 s after the cast: the summon below half HP is recalled (T2: +4 SP, a stack)
h.runUntil(() => h.b.time >= castAt + 1.1, 2);
assert.ok(hurt.alive, 'not before 1.2 s');
const spBefore = u.skill.sp;
assert.ok(h.runUntil(() => !hurt.alive, 0.2), 'recalled at 1.2 s');
assert.deepEqual([hurt.alive, muted.alive, fine.alive], [false, true, true], 'only the one below half');
assert.equal(deck.held, 2, '+1 held');
assert.ok(Math.abs(u.skill.sp - spBefore - 3) < 0.2, `T2: +3 SP (${spBefore} → ${u.skill.sp})`);
assert.ok(Math.abs(u.skill.sp - spBefore - 4) < 0.2, `T2: +4 SP (${spBefore} → ${u.skill.sp})`);
assert.equal(u.findBuff('ling:t2').stacks, 1, 'T2 stack');
void sp0;
done(h);
+2 -2
View File
@@ -108,13 +108,13 @@ test('trait 行商: −3 DP every 3 s while he stands (MER-X: −2), he retreats
}
});
test('T1 和气生财: while he blocks one enemy alone in the 3 × 3 — his ASPD +28, its −28; a second enemy around ⇒ ±14; nothing while he blocks nobody (MER-Y stage 3: ±40 / ±20)', () => {
test('T1 和气生财: while he blocks one enemy alone in the 3 × 3 — his ASPD +30, its −30; a second enemy around ⇒ ±15; nothing while he blocks nobody (MER-Y stage 3: ±42 / ±21)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 9, col: 5, dp: 999 });
const tb = talentOf(tier, elite, mod, 0).bb;
const self = tb['lmlee_t_1[self].attack_speed'], foe = tb['lmlee_t_1[enemy].attack_speed'];
assert.deepEqual([self, foe, tb.cnt], elite && tier === 6 && mod === Y ? [20, -20, 1] : [14, -14, 1], label(f));
assert.deepEqual([self, foe, tb.cnt], elite && tier === 6 && mod === Y ? [21, -21, 1] : [15, -15, 1], label(f));
h.run(0.5);
assert.equal(u.findBuff('talent:lmlee:self'), null, `${label(f)}: blocking nobody`);
const w = h.spawn('enemy_walk', { routeIndex: 0 });
+16 -16
View File
@@ -73,9 +73,9 @@ test('逻各斯 in every 自选 form: his operator kit (all three skills authore
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1297 / 556, E2 Lv60 1540 / 632; CCR-Δ +100 / +36 → +170 / +67 HP / ATK,
// the numbers of the forms (zh_CN, full potential): E2 Lv1 1297 / 583, E2 Lv60 1540 / 659; CCR-Δ +100 / +36 → +170 / +67 HP / ATK,
// CCR-Y +35 / +5 → +60 / +5 ATK / RES
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], [1297, 556, 1540, 632]);
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], [1297, 583, 1540, 659]);
assert.deepEqual([modOf(5, CCRD).attr, modOf(6, CCRD).attr, modOf(5, CCRY).attr, modOf(6, CCRY).attr], [{ maxHp: 100, atk: 36 }, { maxHp: 170, atk: 67 }, { atk: 35, res: 5 }, { atk: 60, res: 5 }]);
});
@@ -166,10 +166,10 @@ test('S2 提喻 (MANUAL, data DEFAULT): 20 s, RES +40 / +50; locks one target wi
for (let i = 1; i < hits.length; i++) approx(hits[i].t - hits[i - 1].t, 0.5, `T${tier}: cadence`, 0.07);
hits.forEach((c, i) => {
assert.equal(c.target, e, `T${tier}: the locked target`);
// + 剜魂具辞's 150 (the first hit marks it already); the target's RES 0
approx(c.amount, u.s.atk * (b['attack@atk_scale_base'] + b['attack@atk_scale_delta'] * Math.min(10, i)) + 150, `T${tier}: hit ${i}`, 1e-4);
// + 剜魂具辞's 165 (the first hit marks it already); the target's RES 0
approx(c.amount, u.s.atk * (b['attack@atk_scale_base'] + b['attack@atk_scale_delta'] * Math.min(10, i)) + 165, `T${tier}: hit ${i}`, 1e-4);
});
assert.ok(Math.abs(hits[10].amount - (u.s.atk * b['attack@atk_scale_base'] * 3 + 150)) < 1e-3 * hits[10].amount, `T${tier}: 3× after 5 s`);
assert.ok(Math.abs(hits[10].amount - (u.s.atk * b['attack@atk_scale_base'] * 3 + 165)) < 1e-3 * hits[10].amount, `T${tier}: 3× after 5 s`);
approx(e.findBuff(`logos:lockSlow:${u.id}`).mods.moveMul, 0.4, `T${tier}: 40 % speed`);
// a stun breaks it: the slow goes, the next hits start over
h.b.applyStatus(u, 'stun', { duration: 1, source: e });
@@ -178,7 +178,7 @@ test('S2 提喻 (MANUAL, data DEFAULT): 20 s, RES +40 / +50; locks one target wi
const n1 = atkHits(h, u).length;
h.run(1.6);
const after = atkHits(h, u).slice(n1);
approx(after[0].amount, u.s.atk * b['attack@atk_scale_base'] + 150, `T${tier}: reset`, 1e-4);
approx(after[0].amount, u.s.atk * b['attack@atk_scale_base'] + 165, `T${tier}: reset`, 1e-4);
// beam: the hit lands as the attack is made (no bolt in flight)
const atks = h.hooksOf('attack').filter((c) => c.attacker === u);
assert.ok(Math.abs(atks[atks.length - 1].t - after[after.length - 1].t) < 1e-9, `T${tier}: instant`);
@@ -203,7 +203,7 @@ test('S2 提喻 keeps its lock while another enemy comes first in the order, and
h.run(1.1);
const next = atkHits(h, u).slice(n1);
assert.ok(next.length >= 2 && next.every((c) => c.target !== a));
approx(next[0].amount, u.s.atk * 0.5 + 150, 'a new lock starts at the base scale', 1e-4);
approx(next[0].amount, u.s.atk * 0.5 + 165, 'a new lock starts at the base scale', 1e-4);
done(h);
});
@@ -263,7 +263,7 @@ test('S3 延异视阈: enemy shots on his range fly at 5 % of their speed (back
}
});
test('T1 语汇演化: 40 % per attacked target for an extra 60 % ATK arts hit on a random enemy of his range + 停顿 0.8 s; CCR-Δ stage 3: 60 % and 60 % ATK 元素伤害 on a target in its 凋亡 burst; CCR-Y stage 3: two executions at 65 %', () => {
test('T1 语汇演化: 40 % per attacked target for an extra 65 % ATK arts hit on a random enemy of his range + 停顿 0.8 s; CCR-Δ stage 3: 60 % and 60 % ATK 元素伤害 on a target in its 凋亡 burst; CCR-Y stage 3: two executions at 70 %', () => {
for (const f of [[5, false, null], [5, true, CCRD], [6, true, CCRD], [6, true, CCRY]]) {
const [tier, elite, mod] = f;
const t = talentOf(tier, elite, mod, 0);
@@ -279,7 +279,7 @@ test('T1 语汇演化: 40 % per attacked target for an extra 60 % ATK arts hit o
for (const c of extra.slice(0, 6)) {
assert.ok(es.includes(c.target), `${label(f)}: an enemy of his range (air too)`);
const marked = h.hooksOf('damaged').some((d) => d.source === u && d.target === c.target && d.dmg?.isAttack && d.t <= c.t && c.t - d.t <= 5);
approx(c.amount, u.s.atk * t.atk_scale + (marked ? 150 : 0), `${label(f)}: ${t.atk_scale * 100} % ATK (+ 剜魂具辞 on a marked one)`, 1e-4);
approx(c.amount, u.s.atk * t.atk_scale + (marked ? 165 : 0), `${label(f)}: ${t.atk_scale * 100} % ATK (+ 剜魂具辞 on a marked one)`, 1e-4);
}
const slug = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'sluggish');
assert.ok(slug.length >= extra.length * 0.95 && slug.every((c) => Math.abs(c.duration - t.sluggish) < 1e-9), `${label(f)}: 停顿 ${t.sluggish} s`);
@@ -295,7 +295,7 @@ test('T1 语汇演化: 40 % per attacked target for an extra 60 % ATK arts hit o
}
// the module texts
assert.match(modOf(6, CCRD).talentChanges.find((t) => t.talentIndex === 0).desc, /60%几率.*凋亡损伤爆发期间则同时造成相当于攻击力60%的元素伤害/);
assert.match(modOf(6, CCRY).talentChanges.find((t) => t.talentIndex === 0).desc, /两个随机目标造成相当于攻击力65%/);
assert.match(modOf(6, CCRY).talentChanges.find((t) => t.talentIndex === 0).desc, /两个随机目标造成相当于攻击力70%/);
assert.equal(modOf(5, CCRD).talentChanges.find((t) => t.talentIndex === 0), undefined, 'stage 1: the talent unchanged');
});
@@ -321,22 +321,22 @@ test('T1 语汇演化 also follows 殁亡\'s follow-up hit (备注 "可触发第
done(h);
});
test('T2 剜魂具辞: each of his attack hits gives the target RES −10 and +150 on every arts damage it takes for 5 s — the hit itself included, other casters\' arts too; one instance per enemy', () => {
test('T2 剜魂具辞: each of his attack hits gives the target RES −10 and +165 on every arts damage it takes for 5 s — the hit itself included, other casters\' arts too; one instance per enemy', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, CCRY]]) {
const t = talentOf(tier, elite, mod, 1);
assert.deepEqual(t, { duration: 5, magic_resistance: -10, atk_addition: 150 });
assert.deepEqual(t, { duration: 5, magic_resistance: -10, atk_addition: 165 });
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 9 });
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
e.base.res = 30; e.markDirty();
const first = () => atkHits(h, u)[0];
assert.ok(h.runUntil(() => !!first(), 4));
const c = first();
approx(c.amount, (u.s.atk + 150) * (1 - 0.2), `T${tier}: (ATK + 150) × (1 − (30 − 10) %) on the very hit`, 1e-4);
approx(c.amount, (u.s.atk + 165) * (1 - 0.2), `T${tier}: (ATK + 165) × (1 − (30 − 10) %) on the very hit`, 1e-4);
const mark = e.findBuff('logos:soulRend');
assert.ok(mark && Math.abs(mark.mods.resFlat + 10) < 1e-9 && mark.data.value === 150);
assert.ok(mark && Math.abs(mark.mods.resFlat + 10) < 1e-9 && mark.data.value === 165);
approx(e.s.res, 20, `T${tier}: RES −10`);
// another source's arts damage gets the 150 too; physical does not
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'arts', canDodge: false }), (1000 + 150) * 0.8, 'arts +150');
// another source's arts damage gets the 165 too; physical does not
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'arts', canDodge: false }), (1000 + 165) * 0.8, 'arts +165');
approx(h.b.dealDamage(null, e, { amount: 1000, type: 'phys', canDodge: false }), 1000, 'phys unchanged');
// it lapses 5 s after his last hit
h.b.applyStatus(u, 'stun', { duration: 20, source: null });
+3 -3
View File
@@ -72,7 +72,7 @@ test('真言 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
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], [1071, 583, 1308, 657]);
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], [1071, 614, 1308, 688]);
assert.deepEqual([modOf(5, PRIX).attr, modOf(6, PRIX).attr], [{ maxHp: 90, atk: 43 }, { maxHp: 145, atk: 87 }]);
});
@@ -284,11 +284,11 @@ test('S3 无言为真: an enemy holding 3 麻痹 entering her range keeps 2 and
done(h2);
});
test('T1 噤声限域: an enemy whose attack a 麻痹 stack interrupts takes 135 % (PRI-X stage 3: 160 %) of her ATK as 元素伤害 (tagged 持续伤害); 10 % / 15 % of the interrupts keep the stack; not for an enemy she cannot select (隐匿)', () => {
test('T1 噤声限域: an enemy whose attack a 麻痹 stack interrupts takes 145 % (PRI-X stage 3: 170 %) of her ATK as 元素伤害 (tagged 持续伤害); 13 % / 18 % of the interrupts keep the stack; not for an enemy she cannot select (隐匿)', () => {
for (const f of [[5, false, null], [6, true, null], [5, true, PRIX], [6, true, PRIX]]) {
const [tier, elite, mod] = f;
const t = talentOf(tier, elite, mod, 0);
assert.deepEqual(t, elite && mod === PRIX && tier === 6 ? { atk_scale: 1.6, prob: 0.15 } : { atk_scale: 1.35, prob: 0.1 }, label(f));
assert.deepEqual(t, elite && mod === PRIX && tier === 6 ? { atk_scale: 1.7, prob: 0.18 } : { atk_scale: 1.45, prob: 0.13 }, label(f));
const { h, u } = field({ tier, elite, mod, skill: 0, seed: 13 });
const e = h.spawn('enemy_shooter', { pos: [10, 8] });
h.b.applyStatus(u, 'disarm', { duration: 999, source: null }); // her attacks would kill nothing, but keep it clean
+6 -5
View File
@@ -78,8 +78,9 @@ test('机械师 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// E2 Lv1 2614 / 425 / 514, E2 Lv60 3288 / 483 / 641; SO-A +60 / +40 → +100 / +80 ATK / DEF; the token 2800 / 492 / 451 → 3264 / 564 / 517
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.def], [2614, 425, 3288, 641]);
// E2 Lv1 2614 / 425 / 542, E2 Lv60 3288 / 483 / 669 (full potential: DEF +28); SO-A +60 / +40 → +100 / +80 ATK / DEF; the token
// 2800 / 492 / 451 → 3264 / 564 / 517
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.def], [2614, 425, 3288, 669]);
assert.deepEqual([modOf(5, SOA).attr, modOf(6, SOA).attr], [{ atk: 60, def: 40 }, { atk: 100, def: 80 }]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(6, true).stats.maxHp], [2800, 3264]);
});
@@ -119,12 +120,12 @@ test('T1 结构性原理: it lasts 30 s from each deployment (SO-A stage 3 too
}
});
test('T2 生命方程: she and her 结构性原理 get a 屏障 of 30 % max HP at each deployment; it takes physical and arts damage, not true; 超额防护: the hit that breaks it is cancelled whole, the next one hurts', () => {
test('T2 生命方程: she and her 结构性原理 get a 屏障 of 40 % max HP at each deployment; it takes physical and arts damage, not true; 超额防护: the hit that breaks it is cancelled whole, the next one hurts', () => {
for (const f of [[5, false, null], [6, true, SOA]]) {
const [tier, elite, mod] = f;
const { h, u, t } = field({ tier, elite, mod, skill: 0 });
const hr = formOf(tier, elite).talents.find((x) => x.index === 1).bb.hp_ratio;
assert.equal(hr, 0.3);
assert.equal(hr, 0.4);
for (const x of [u, t]) {
const b = barrier(x, 'mcnist:t2:barrier');
approx(b.shield, x.s.maxHp * hr, `${label(f)}: ${x.kind} 屏障`);
@@ -295,7 +296,7 @@ test('S3 charge: the 结构性原理 runs along its facing to the farthest free
approx(h.b.time - cast, 0.2 + 4 / 20, `${label(f)}: flight time`, 0.1);
assert.deepEqual([t.tileR, t.tileC], [11, 7], `${label(f)}: the farthest tile before the enemy`);
approx(t.hp, t.s.maxHp, `${label(f)}: a new deployment (full HP)`);
approx(barrier(t, 'mcnist:t2:barrier').shield, t.s.maxHp * 0.3, `${label(f)}: 生命方程 again`);
approx(barrier(t, 'mcnist:t2:barrier').shield, t.s.maxHp * 0.4, `${label(f)}: 生命方程 again`);
const ch = tagged(h, u, 'mcnist:charge');
assert.deepEqual(sortN(ch.map((c) => c.target.id)), sortN([wall.id, hit2.id]), `${label(f)}: within 1.5 of (11, 7), the flyer too`);
for (const c of ch) { approx(c.amount, atk * 3, `${label(f)}: 300 % of her ATK`); assert.equal(c.type, 'phys'); }
+4 -4
View File
@@ -80,9 +80,9 @@ test('麦哲伦 in every 自选 form: her kit (all three skills authored), the f
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 871 / 403 / 116, E2 Lv60 1025 / 447 / 132; SUM-X +100 / +30 → +150 / +50 HP /
// the numbers of the forms (zh_CN): E2 Lv1 871 / 426 / 116, E2 Lv60 1025 / 470 / 132; SUM-X +100 / +30 → +150 / +50 HP /
// ATK, SUM-Y +25 / +25 → +40 / +40 ATK / 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], [871, 403, 1025, 447]);
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], [871, 426, 1025, 470]);
assert.deepEqual([modOf(5, SUMX).attr, modOf(6, SUMX).attr, modOf(5, SUMY).attr, modOf(6, SUMY).attr], [{ maxHp: 100, atk: 30 }, { maxHp: 150, atk: 50 }, { atk: 25, def: 25 }, { atk: 40, def: 40 }]);
});
@@ -171,10 +171,10 @@ test('the deck: a knocked-out drone comes back on its tile its redeploy time (10
done(h);
});
test('T2 光学折射配件: a drone is 隐匿 for 20 s (26 s with SUM-X stage 3) from each deployment — no ranged enemy targets it meanwhile; SUM-X stage 3: 麦哲伦 beside a drone inside that window is 隐匿 too (not two tiles away, not at stage 1)', () => {
test('T2 光学折射配件: a drone is 隐匿 for 22 s (28 s with SUM-X stage 3) from each deployment — no ranged enemy targets it meanwhile; SUM-X stage 3: 麦哲伦 beside a drone inside that window is 隐匿 too (not two tiles away, not at stage 1)', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, SUMX], [6, true, SUMX], [6, true, SUMY]]) {
const x3 = elite && mod === SUMX && tier === 6;
const want = x3 ? 26 : 20;
const want = x3 ? 28 : 22;
const { h, u, ds } = field({ tier, elite, mod, skill: 1, drones: [[L, 10, 5], [L, 11, 7]] });
const [near, far] = ds;
assert.equal(tokOf(L, tier, elite, 1, mod).talents.find((t) => t.bb.hidden_duration != null).bb.hidden_duration, want, `${label([tier, elite, mod])}: data`);
+11 -11
View File
@@ -78,7 +78,7 @@ test('Mon3tr in every 自选 form: her operator kit (all three skills authored),
const { h, u, p } = field({ tier, elite, mod, skill, prosts: { row: 10, col: 6 } });
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], [MONSTR, SLOT[tier], form.skills[skill].skillId, false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 2.85, 18], `${label(f)}: stats`);
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.bat, u.base.cost], [form.stats.maxHp, form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 2.85, 16], `${label(f)}: stats`);
assert.deepEqual([u.profile.dmgType, u.profile.heal, u.profile.attack, u.def.subProf, u.s.blockCnt], ['heal', { mode: 'single' }, 'ranged', 'chainhealer', 1], `${label(f)}: 链愈师`);
assert.deepEqual(u.def.traitBb, traitOf(tier, elite, mod), `${label(f)}: the trait (XAH-X 0.85)`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-2`);
@@ -90,11 +90,11 @@ test('Mon3tr in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// E2 Lv1 1492 / 422 / 176, E2 Lv60 1653 / 492 / 206; XAH-X +40 / +30 → +65 / +40 ATK / DEF; the 重构体 4292 / 181 → 5048 / 208
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], [1492, 422, 1653, 492]);
// E2 Lv1 1492 / 447 / 176, E2 Lv60 1653 / 517 / 206; XAH-X +40 / +30 → +65 / +40 ATK / DEF; the 重构体 4292 / 181 → 5048 / 208
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], [1492, 447, 1653, 517]);
assert.deepEqual([modOf(5, XAHX).attr, modOf(6, XAHX).attr], [{ atk: 40, def: 30 }, { atk: 65, def: 40 }]);
assert.deepEqual([variantOf(5, false).stats.maxHp, variantOf(5, false).stats.def, variantOf(6, true).stats.maxHp], [4292, 181, 5048]);
assert.deepEqual([t0Of(5, true, XAHX).atk, t0Of(6, true, XAHX).atk, t0Of(6, true, XAHX)['attack@chain.extra_cnt'], t0Of(6, false, null).atk], [0.15, 0.25, 1, 0.15]);
assert.deepEqual([t0Of(5, true, XAHX).atk, t0Of(6, true, XAHX).atk, t0Of(6, true, XAHX)['attack@chain.extra_cnt'], t0Of(6, false, null).atk], [0.2, 0.3, 1, 0.2]);
});
test('a 自选 pick: Mon3tr is offered at tiers 5 and 6 (she has a kit) and a roster with her and XAH-X passes validateDiyPicks', () => {
@@ -151,7 +151,7 @@ test('T1 自我修复 free link: her 重构体 in a chain uses no target of the
}
});
test('T1 自我修复 — the 重构体: only her heals reach it (禁疗), it loses 8 HP every 0.1 s from 1 s after it lands, every ally of its 3×3 ATK +15 % (XAH-X stage 3 +25 %; not 2 tiles away); gone ⇒ back 15 s later for 3 DP while she stands; withdrawn when she leaves, her redeployment readies it', () => {
test('T1 自我修复 — the 重构体: only her heals reach it (禁疗), it loses 8 HP every 0.1 s from 1 s after it lands, every ally of its 3×3 ATK +20 % (XAH-X stage 3 +30 %; not 2 tiles away); gone ⇒ back 15 s later for 3 DP while she stands; withdrawn when she leaves, her redeployment readies it', () => {
for (const f of [[5, false, null], [6, true, XAHX], [5, true, XAHX]]) {
const [tier, elite, mod] = f;
const atkUp = t0Of(tier, elite, mod).atk;
@@ -196,21 +196,21 @@ test('T1 自我修复 — the 重构体: only her heals reach it (禁疗), it lo
}
});
test('T2 战术协同: each heal of hers gives its target and her ASPD +20 for 10 s (no stacking: refreshed); under S2 ×1.8 / ×2.3 — the strongest effect stays', () => {
test('T2 战术协同: each heal of hers gives its target and her ASPD +22 for 10 s (no stacking: refreshed); under S2 ×1.8 / ×2.3 — the strongest effect stays', () => {
for (const f of [[5, false, null], [6, true, XAHX]]) {
const [tier, elite, mod] = f;
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
const s2 = skillOf(tier, elite, S2);
assert.deepEqual([t1.attack_speed, t1.buff_duration, s2.bb.talent_scale], [20, 10, elite ? 2.3 : 1.8], label(f));
assert.deepEqual([t1.attack_speed, t1.buff_duration, s2.bb.talent_scale], [22, 10, elite ? 2.3 : 1.8], label(f));
const others = [{ uid: 3, chessId: YAK, row: 10, col: 5 }];
const { h, u, log } = field({ tier, elite, mod, skill: 1, others });
const yak = h.unit(3);
setHp(yak, 0.3);
const n0 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n0).length > 0, 4), label(f));
assert.deepEqual([yak.findBuff('talent:monstr:aspd')?.mods, u.findBuff('talent:monstr:aspd')?.mods], [{ aspd: 20 }, { aspd: 20 }], `${label(f)}: target and herself`);
assert.deepEqual([yak.findBuff('talent:monstr:aspd')?.mods, u.findBuff('talent:monstr:aspd')?.mods], [{ aspd: 22 }, { aspd: 22 }], `${label(f)}: target and herself`);
approx(yak.findBuff('talent:monstr:aspd').timeLeft, 10, `${label(f)}: 10 s`, 0.05);
assert.equal(yak.s.aspd, yak.base.aspd + 20, label(f));
assert.equal(yak.s.aspd, yak.base.aspd + 22, label(f));
// S2 (an enemy in her y-2): ×talent_scale
h.spawn('enemy_dummy', { pos: [11, 6] });
u.skill.gainSp(999);
@@ -218,8 +218,8 @@ test('T2 战术协同: each heal of hers gives its target and her ASPD +20 for 1
setHp(yak, 0.3);
const n1 = log.length;
assert.ok(h.runUntil(() => healsOf(log, u, n1).some((c) => c.target === yak), 6), label(f));
approx(yak.findBuff('talent:monstr:aspd').mods.aspd, 20 * s2.bb.talent_scale, `${label(f)}: ×${s2.bb.talent_scale}`);
approx(u.findBuff('talent:monstr:aspd').mods.aspd, 20 * s2.bb.talent_scale, `${label(f)}: herself too`);
approx(yak.findBuff('talent:monstr:aspd').mods.aspd, 22 * s2.bb.talent_scale, `${label(f)}: ×${s2.bb.talent_scale}`);
approx(u.findBuff('talent:monstr:aspd').mods.aspd, 22 * s2.bb.talent_scale, `${label(f)}: herself too`);
done(h);
}
});
+14 -14
View File
@@ -57,7 +57,7 @@ function done(h) {
const label = ([tier, elite, mod]) => `T${tier} ${elite ? 'elite' : 'normal'} ${mod ?? 'none'}`;
const TICK = 1 / 30;
test('娜仁图亚 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, y-7 range, ranged physical boomerangs that hit air, block 1, 35 % dodge, no 特质', () => {
test('娜仁图亚 in every 自选 form: her operator kit (all three skills authored), the form\'s stats + module attributes, y-7 range, ranged physical boomerangs that hit air, block 1, 38 % dodge, no 特质', () => {
assert.equal(OPERATOR_KITS[NARANT], KITS[NARANT]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -69,14 +69,14 @@ test('娜仁图亚 in every 自选 form: her operator kit (all three skills auth
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.projectile, u.profile.canHitFly, u.base.bat, typeof u.profile.canAttack],
[1, 'ranged', 'phys', 'boomerang', true, 1, 'function'], `${label(f)}: 回环射手`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: y-7`);
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts], [0.35, 0.35], `${label(f)}: 婀娜虚影 dodge`);
assert.deepEqual([u.s.dodgePhys, u.s.dodgeArts], [0.38, 0.38], `${label(f)}: 婀娜虚影 dodge`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [BACKUPS.diy.operators[NARANT].bonds, []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
done(h);
}
}
// E2 Lv1 2000 / 546 / 144, E2 Lv60 2331 / 632 / 161; LPS-X +25 / +25 → +45 / +45 ATK / DEF, LPS-Y +210 / +20 → +300 / +40 HP / ATK
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], [2000, 546, 2331, 632]);
// full potential: E2 Lv1 2000 / 573 / 144, E2 Lv60 2331 / 659 / 161; LPS-X +25 / +25 → +45 / +45 ATK / DEF, LPS-Y +210 / +20 → +300 / +40 HP / ATK
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], [2000, 573, 2331, 659]);
assert.deepEqual([modOf(5, LPSX).attr, modOf(6, LPSX).attr, modOf(5, LPSY).attr, modOf(6, LPSY).attr], [{ atk: 25, def: 25 }, { atk: 45, def: 45 }, { maxHp: 210, atk: 20 }, { maxHp: 300, atk: 40 }]);
});
@@ -119,7 +119,7 @@ test('S1 旋刃 (切换: on until she leaves; data DEFAULT): range −1 (the far
assert.ok(one.length >= 1 && one.every((c) => c.target === lone), `T${tier}: one hit per attack on a lone enemy`);
assert.equal(new Set(one.map((c) => c.dmg.attackId)).size, one.length, `T${tier}: no bounce`);
const before = atkHits(h, u).filter((c) => c.t < one[0].t - 1e-9).length; // her normal attacks' steals so far
approx(one[0].amount, (u.base.atk + Math.min(250, 25 * before)) * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % (+ the ATK stolen before)`);
approx(one[0].amount, (u.base.atk + Math.min(270, 27 * before)) * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} % (+ the ATK stolen before)`);
// A (lone) at 10,5; B at 11,5 (1 from A); C at 9,6 (1.41 from A, 2.24 from B)
const b = h.spawn('enemy_dummy', { pos: [11, 5] }), c = h.spawn('enemy_dummy', { pos: [9, 6] });
const far = h.spawn('enemy_dummy', { pos: [12, 5] }); // 2 from A, 1 from B: reachable from B only
@@ -180,7 +180,7 @@ test('S2 恶魇 (30 s, attack SP 18 from 5 / 8): 210 % / 225 % ATK + 停顿 1 s;
// the hit's ATK scale: her ATK at that hit (base + the steals before it) × 225 %
const before = atkHits(h, u).filter((c) => c.t < main.t - 1e-9).length;
const sa = formOf(tier, elite).talents.find((t) => t.index === 0).bb['attack@steal_atk'];
approx(main.amount, (u.base.atk + Math.min(250, sa * before)) * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} %`);
approx(main.amount, (u.base.atk + Math.min(270, sa * before)) * sk.bb['attack@atk_scale'], `T${tier}: ${sk.bb['attack@atk_scale'] * 100} %`);
done(h);
}
});
@@ -215,13 +215,13 @@ test('S3 吞日 (20 s): 3 boomerangs per attack (3 hits of 145 % / 160 %, flying
}
});
test('T1 “我见,我得”: every landing attack hit steals 25 ATK / 20 DEF — the enemy loses, she gains, each capped at 250 / 200 (LPS-X stage 3: 300 / 240, twice within one tile); two of her: the larger loss, not the sum; a dodged hit steals nothing; all given back when she leaves', () => {
test('T1 “我见,我得”: every landing attack hit steals 27 ATK / 21 DEF — the enemy loses, she gains, each capped at 270 / 210 (LPS-X stage 3: 324 / 252, twice within one tile); two of her: the larger loss, not the sum; a dodged hit steals nothing; all given back when she leaves', () => {
for (const f of [[5, false, null], [6, true, null], [5, true, LPSX], [6, true, LPSX], [6, true, LPSY]]) {
const [tier, elite, mod] = f;
const t0 = formOf(tier, elite).talents.find((t) => t.index === 0).bb;
const tc = mod === LPSX && tier === 6 ? modOf(6, LPSX).talentChanges.find((t) => t.talentIndex === 0) : null;
const capA = tc?.bb['attack@steal_atk_max'] ?? t0['attack@steal_atk_max'], capD = tc?.bb['attack@steal_def_max'] ?? t0['attack@steal_def_max'];
assert.deepEqual([t0['attack@steal_atk'], t0['attack@steal_def'], capA, capD], [25, 20, ...(tc ? [300, 240] : [250, 200])], label(f));
assert.deepEqual([t0['attack@steal_atk'], t0['attack@steal_def'], capA, capD], [27, 21, ...(tc ? [324, 252] : [270, 210])], label(f));
const twice = !!tc;
if (twice) assert.match(tc.desc, /攻击到周围8格的敌人时偷取触发2次/);
// one hit at two tiles, then one at one tile
@@ -229,15 +229,15 @@ test('T1 “我见,我得”: every landing attack hit steals 25 ATK / 20 DEF
const { h, u } = field({ tier, elite, mod, skill: 2 });
const e = h.spawn('enemy_brute', { pos: [10, 5] });
assert.ok(h.runUntil(() => atkHits(h, u).length === 1, 3));
assert.deepEqual(u.findBuff('talent:narant:loot')?.mods, { atkFlat: 25, defFlat: 20 }, `${label(f)}: she gains`);
assert.deepEqual(e.findBuff('narant:stolen')?.mods, { atkFlat: -25, defFlat: -20 }, `${label(f)}: the enemy loses`);
assert.deepEqual([e.s.atk, e.s.def], [375, 280], `${label(f)}: its stats`);
assert.deepEqual(u.findBuff('talent:narant:loot')?.mods, { atkFlat: 27, defFlat: 21 }, `${label(f)}: she gains`);
assert.deepEqual(e.findBuff('narant:stolen')?.mods, { atkFlat: -27, defFlat: -21 }, `${label(f)}: the enemy loses`);
assert.deepEqual([e.s.atk, e.s.def], [373, 279], `${label(f)}: its stats`);
h.b.kill(e, null);
const near = h.spawn('enemy_brute', { pos: [10, 4] });
assert.ok(h.runUntil(() => atkHits(h, u).length === 2, 4));
const k = twice ? 2 : 1;
assert.deepEqual(near.findBuff('narant:stolen')?.mods, { atkFlat: -25 * k, defFlat: -20 * k }, `${label(f)}: within one tile ×${k}`);
assert.deepEqual(u.findBuff('talent:narant:loot')?.mods, { atkFlat: 25 * (1 + k), defFlat: 20 * (1 + k) }, `${label(f)}: hers`);
assert.deepEqual(near.findBuff('narant:stolen')?.mods, { atkFlat: -27 * k, defFlat: -21 * k }, `${label(f)}: within one tile ×${k}`);
assert.deepEqual(u.findBuff('talent:narant:loot')?.mods, { atkFlat: 27 * (1 + k), defFlat: 21 * (1 + k) }, `${label(f)}: hers`);
// the caps
h.run(40);
assert.deepEqual(u.findBuff('talent:narant:loot')?.mods, { atkFlat: capA, defFlat: capD }, `${label(f)}: her cap`);
@@ -259,7 +259,7 @@ test('T1 “我见,我得”: every landing attack hit steals 25 ATK / 20 DEF
h.run(8);
const n1 = atkHits(h, u).filter((c) => c.target === e).length, n2 = atkHits(h, v).filter((c) => c.target === e).length;
assert.ok(n1 >= 3 && n2 >= 3, `both hit it (${n1}, ${n2})`);
assert.deepEqual(e.findBuff('narant:stolen')?.mods.atkFlat, -Math.min(250, 25 * Math.max(n1, n2)), 'the larger loss');
assert.deepEqual(e.findBuff('narant:stolen')?.mods.atkFlat, -Math.min(270, 27 * Math.max(n1, n2)), 'the larger loss');
const ghost = h.spawn('enemy_brute', { pos: [11, 4] });
h.b.kill(e, null);
h.b.addBuff(ghost, { key: 'test:dodge', mods: { dodgePhys: 1 } });
+6 -6
View File
@@ -120,20 +120,20 @@ test('<支援装置> hand pieces: the picked skill\'s device (variant sources),
}
});
test('注意安全: 10 % 庇护 on her; once a 远程 enemy is on the field (until she leaves) 15 % on her and on our 远程 operators (not the 近战 ones), and +1 SP every 6 s for them', () => {
test('注意安全: 12 % 庇护 on her; once a 远程 enemy is on the field (until she leaves) 18 % on her and on our 远程 operators (not the 近战 ones), and +1 SP every 6 s for them', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1['nasti_t2[res].damage_resistance'], t1['nasti_t2[res_plus].damage_resistance'], t1['nasti_t2[add_sp].interval']], [0.1, 0.15, 6]);
assert.deepEqual([t1['nasti_t2[res].damage_resistance'], t1['nasti_t2[res_plus].damage_resistance'], t1['nasti_t2[add_sp].interval']], [0.12, 0.18, 6]);
const { h, u } = field({ tier, elite, skill: 0, others: [{ uid: 3, chessId: 'chess_char_1_14_a', row: 9, col: 3 }, { uid: 4, chessId: 'chess_char_1_08_a', row: 12, col: 3 }] });
const prov = h.unit(3), tx = h.unit(4);
h.run(0.5);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.9, artsTakenMul: 0.9 }, null, null], `T${tier}: 10 % on her only`);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 1 - 0.12, artsTakenMul: 1 - 0.12 }, null, null], `T${tier}: 12 % on her only`);
h.spawn('enemy_dummy', { pos: [11, 9] });
h.run(0.5);
assert.deepEqual(protectOf(u), { physTakenMul: 0.9, artsTakenMul: 0.9 }, `T${tier}: a 近战 enemy changes nothing`);
assert.deepEqual(protectOf(u), { physTakenMul: 1 - 0.12, artsTakenMul: 1 - 0.12 }, `T${tier}: a 近战 enemy changes nothing`);
const rng = h.spawn('enemy_rng', { pos: [12, 9] });
h.run(0.5);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 0.85, artsTakenMul: 0.85 }, { physTakenMul: 0.85, artsTakenMul: 0.85 }, null], `T${tier}: 15 % on her and 格雷伊 (远程), not 德克萨斯`);
assert.deepEqual([protectOf(u), protectOf(prov), protectOf(tx)], [{ physTakenMul: 1 - 0.18, artsTakenMul: 1 - 0.18 }, { physTakenMul: 1 - 0.18, artsTakenMul: 1 - 0.18 }, null], `T${tier}: 18 % on her and 格雷伊 (远程), not 德克萨斯`);
const gifts = (a) => h.hooksOf('spGain').filter((c) => c.unit === a && c.reason === 'talent').map((c) => c.t);
prov.skill.sp = 0;
u.skill.sp = 0;
@@ -144,7 +144,7 @@ test('注意安全: 10 % 庇护 on her; once a 远程 enemy is on the field (unt
assert.equal(gifts(tx).length, 0, `T${tier}: not 德克萨斯`);
h.b.kill(rng, null);
h.run(1);
assert.deepEqual(protectOf(prov), { physTakenMul: 0.85, artsTakenMul: 0.85 }, `T${tier}: permanent until she leaves`);
assert.deepEqual(protectOf(prov), { physTakenMul: 1 - 0.18, artsTakenMul: 1 - 0.18 }, `T${tier}: permanent until she leaves`);
h.b.kill(u, null);
h.run(1);
assert.equal(protectOf(prov), null, `T${tier}: gone with her`);
+3 -3
View File
@@ -155,7 +155,7 @@ test('复燃: no deployable kill tile ⇒ a random free low melee tile of her ra
done(h);
});
test('回光黯淡: her and her summons\' damage on an enemy below 50 % HP ×1.4 (the final damage); SOC-X "攻击力提升至115%" on an enemy her summon blocks (stage 1 and 3), which she can attack anywhere', () => {
test('回光黯淡: her and her summons\' damage on an enemy below 60 % HP ×1.45 (the final damage); SOC-X "攻击力提升至115%" on an enemy her summon blocks (stage 1 and 3), which she can attack anywhere', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, X]]) {
const f = label([tier, elite, mod]);
const { h, u } = field({ tier, elite, mod, skill: 1, row: 9, col: 3 });
@@ -164,10 +164,10 @@ test('回光黯淡: her and her summons\' damage on an enemy below 50 % HP ×1.4
const hits = () => h.hooksOf('damaged').filter((c) => c.source === u && c.target === e && c.dmg.isAttack);
const full = hits().at(-1).amount;
approx(full, u.s.atk, `${f}: ×1 at full HP`);
e.hp = e.s.maxHp * 0.49;
e.hp = e.s.maxHp * 0.59;
const n0 = hits().length;
h.run(2);
approx(hits().slice(n0).at(-1).amount, u.s.atk * 1.4, `${f}: ×1.4 below 50 %`);
approx(hits().slice(n0).at(-1).amount, u.s.atk * 1.45, `${f}: ×1.45 below 60 %`);
done(h);
}
// the trait: she attacks the enemy her summon blocks out of her range (×1.15 with SOC-X)
+5 -5
View File
@@ -68,9 +68,9 @@ test('年 in every 自选 form: her operator kit (all three skills authored), th
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2737 / 513 / 529, E2 Lv60 3375 / 583 / 660; PRO-X +200 HP / +60 DEF →
// the numbers of the forms (zh_CN, full potential): E2 Lv1 2737 / 537 / 559, E2 Lv60 3375 / 607 / 690; PRO-X +200 HP / +60 DEF →
// +375 / +50 / +80, PRO-Y +320 / +35 / +21 / block +1 → +550 / +70 / +30 / block +1
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/1/4/0'].stats.def, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.def], [2737, 513, 529, 3375, 660]);
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/1/4/0'].stats.def, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.def], [2737, 537, 559, 3375, 690]);
assert.deepEqual([modOf(5, PROX).attr, modOf(6, PROX).attr, modOf(5, PROY).attr, modOf(6, PROY).attr],
[{ maxHp: 200, def: 60 }, { maxHp: 375, atk: 50, def: 80 }, { maxHp: 320, atk: 35, def: 21, blockCnt: 1 }, { maxHp: 550, atk: 70, def: 30, blockCnt: 1 }]);
});
@@ -196,12 +196,12 @@ test('S3 铁御 (40 s): ATK +65 % / +80 % (her block +0); every other allied ope
}
});
test('T1 积甲成山: every 重装 operator of her team (her included) max HP +16 % for the whole battle — +21 % with PRO-Y stage 3; a 先锋 nothing', () => {
test('T1 积甲成山: every 重装 operator of her team (her included) max HP +20 % for the whole battle — +25 % with PRO-Y stage 3; a 先锋 nothing', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, others: [{ uid: 2, chessId: YAK, row: 11, col: 3 }, { uid: 3, chessId: TEXAS, row: 11, col: 7 }] });
const yak = h.unit(2), tex = h.unit(3);
const want = elite && mod === PROY && tier === 6 ? 0.21 : 0.16;
const want = elite && mod === PROY && tier === 6 ? 0.25 : 0.2;
assert.equal(u.def.raw.talents.find((t) => t.index === 0).bb.max_hp, want, `${label(f)}: data`);
for (const a of [u, yak]) assert.deepEqual(a.findBuff('talent:nian:armor')?.mods, { hpPct: want }, `${label(f)}: ${a.defId}`);
assert.equal(tex.findBuff('talent:nian:armor'), null, `${label(f)}: 德克萨斯 (先锋)`);
@@ -280,7 +280,7 @@ test('PRO-Y “请勿叩门”: blocks 4 at both stages; stage 3 only: per 重
const fort = u.findBuff('trait:nian:fortify');
approx(fort.mods.hpPct, 0.04 * k, `${n} 重装: max HP`);
approx(fort.mods.healingTakenMul, 1 + 0.04 * k, `${n} 重装: healing received`);
approx(u.s.maxHp, u.base.maxHp * (1 + 0.21 + 0.04 * k), `${n} 重装: with 积甲成山's 21 %`);
approx(u.s.maxHp, u.base.maxHp * (1 + 0.25 + 0.04 * k), `${n} 重装: with 积甲成山's 25 %`);
// a heal of 1000 lands as 1000 × (1 + 0.04 k)
u.hp = 1;
const got = h.b.heal(h.unit(9), u, 1000);
+5 -5
View File
@@ -74,9 +74,9 @@ test('帕拉斯 in every 自选 form: her operator kit (all three skills authore
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1413 / 577 / 382, E2 Lv60 1778 / 650 / 430; INS-X +150 HP +55 ATK → +210 / +83,
// trait 130 %; INS-Y +40 ATK +3 ASPD → +60 / +5
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/1/4/0'].stats.def, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1413, 577, 382, 1778, 650]);
// the numbers of the forms (zh_CN, full potential: ATK +25): E2 Lv1 1413 / 602 / 382, E2 Lv60 1778 / 675 / 430; INS-X +150 HP
// +55 ATK → +210 / +83, trait 130 %; INS-Y +40 ATK +3 ASPD → +60 / +5
assert.deepEqual([FORMS['2/1/4/0'].stats.maxHp, FORMS['2/1/4/0'].stats.atk, FORMS['2/1/4/0'].stats.def, FORMS['2/60/7/1'].stats.maxHp, FORMS['2/60/7/1'].stats.atk], [1413, 602, 382, 1778, 675]);
assert.deepEqual([modOf(5, INSX).attr, modOf(6, INSX).attr, modOf(5, INSY).attr, modOf(6, INSY).attr], [{ maxHp: 150, atk: 55 }, { maxHp: 210, atk: 83 }, { atk: 40, aspd: 3 }, { atk: 60, aspd: 5 }]);
assert.deepEqual([formOf(5, false).trait.bb.atk_scale, modOf(5, INSX).traitOverride.bb.atk_scale, modOf(6, INSY).traitOverride.bb.atk_scale], [1.2, 1.3, 1.2]);
});
@@ -158,7 +158,7 @@ test('T1 英雄的诞生: while she is deployed, every 米诺斯 operator (her t
}
});
test('T2 女神的振奋: every damage instance she deals heals her and the ally on the tile in front 40 HP (INS-Y stage 3: 60, and every 米诺斯 operator — her too — 40 more), through 禁疗, not a 不成为治疗目标 ally, not the one behind', () => {
test('T2 女神的振奋: every damage instance she deals heals her and the ally on the tile in front 45 HP (INS-Y stage 3: 65, and every 米诺斯 operator — her too — 40 more), through 禁疗, not a 不成为治疗目标 ally, not the one behind', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 'chess_char_1_02_a', row: 10, col: 6 }, { uid: 3, chessId: 'test_minos_a', row: 10, col: 4 }];
@@ -166,7 +166,7 @@ test('T2 女神的振奋: every damage instance she deals heals her and the ally
const front = h.unit(2), behind = h.unit(3);
const t = talentOf(u, 1);
const y3 = elite && mod === INSY && tier === 6;
assert.deepEqual([t.value, t['pallas_e_t_2.value']], y3 ? [60, 40] : [40, undefined], `${label(f)}: the talent of the form`);
assert.deepEqual([t.value, t['pallas_e_t_2.value']], y3 ? [65, 40] : [45, undefined], `${label(f)}: the talent of the form`);
for (const a of [u, front, behind]) a.hp = a.s.maxHp * 0.5;
h.b.addBuff(front, { key: 'test:healFree', flags: { healFree: true } }); // 禁疗: the heal ignores it
h.spawn('enemy_dummy', { pos: [10, 7] });
+13 -10
View File
@@ -76,7 +76,7 @@ test('酒神 in every 自选 form: her kit (all three skills authored), the form
done(h);
}
}
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], [936, 411, 1111, 463]);
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], [936, 434, 1111, 486]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr], [{ maxHp: 125, atk: 30 }, { maxHp: 200, atk: 42 }]);
});
@@ -88,11 +88,11 @@ test('a 自选 pick: 酒神 is offered at tiers 5 and 6 and a roster with her pa
{ ok: true, picks: { [SLOT[6]]: { charId: ID, skillIndex: 2, uniEquipId: X } } });
});
test('T1 形为心役: every hit of her attacks first gives its target 30 % ATK 神经损伤 and every other enemy within 1.3 of it (中点, air too) 20 % ATK — then the arts damage; one 1.5 away nothing', () => {
test('T1 形为心役: every hit of her attacks first gives its target 33 % ATK 神经损伤 and every other enemy within 1.3 of it (中点, air too) 23 % ATK — then the arts damage; one 1.5 away nothing', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const { h, u } = field({ tier, elite, skill: 2 });
const t1 = formOf(tier, elite).talents[0].bb;
assert.deepEqual(t1, { 'attack@ep_damage_ratio': 0.3, ep_damage_ratio: 0.2, range_radius: 1.3 });
assert.deepEqual(t1, { 'attack@ep_damage_ratio': 0.33, ep_damage_ratio: 0.23, range_radius: 1.3 });
const main = h.spawn('enemy_dummy', { pos: [10, 6] });
const nearFly = h.spawn('enemy_fly', { pos: [11, 6] });
const far = h.spawn('enemy_dummy', { pos: [11.5, 7] }); // ≈ 1.8 from (10, 6)
@@ -105,8 +105,8 @@ test('T1 形为心役: every hit of her attacks first gives its target 30 % ATK
const flyEl = before.find((c) => c.type === 'element' && c.target === nearFly);
assert.ok(mainEl && flyEl, '神经损伤 before the damage');
assert.equal(mainEl.dmg.element, 'neural');
approx(mainEl.amount, u.s.atk * 0.3, '30 % ATK');
approx(flyEl.amount, u.s.atk * 0.2, '20 % ATK to the flyer around');
approx(mainEl.amount, u.s.atk * 0.33, '33 % ATK');
approx(flyEl.amount, u.s.atk * 0.23, '23 % ATK to the flyer around');
assert.ok(!before.some((c) => c.target === far), 'outside the 1.3');
approx(seq[iHit].amount, u.s.atk, 'arts damage = ATK');
assert.equal(seq[iHit].type, 'arts');
@@ -122,17 +122,17 @@ test('RIT-X “酒神之心”: her 神经损伤 on an ELITE / leader enemy ×1.
const el = h.spawn('enemy_elite', { pos: [10, 6] });
const n0 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n0).some((c) => c.target === el), 3));
approx(elemOf(h, u, n0).find((c) => c.target === el).amount, u.s.atk * 0.3 * want, `${label([tier, elite, mod])}: elite`);
approx(elemOf(h, u, n0).find((c) => c.target === el).amount, u.s.atk * 0.33 * want, `${label([tier, elite, mod])}: elite`);
h.b.kill(el);
const nm = h.spawn('enemy_dummy', { pos: [10, 6] });
const n1 = h.hooksOf('damaged').length;
assert.ok(h.runUntil(() => elemOf(h, u, n1).some((c) => c.target === nm), 3));
approx(elemOf(h, u, n1).find((c) => c.target === nm).amount, u.s.atk * 0.3, 'normal enemy');
approx(elemOf(h, u, n1).find((c) => c.target === nm).amount, u.s.atk * 0.33, 'normal enemy');
done(h);
}
});
test('T2 堕梦: while she is on the field every enemy in a 神经损伤 burst has ASPD −12 (RIT-X stage 3 −20), on the whole field; none outside the burst or once she left', () => {
test('T2 堕梦: while she is on the field every enemy in a 神经损伤 burst has ASPD −16 (RIT-X stage 3 −24), on the whole field; none outside the burst or once she left', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, X], [5, true, X]]) {
const { h, u } = field({ tier, elite, mod, skill: 2 });
quiet(h, u);
@@ -198,6 +198,9 @@ test('S1 暗夜回声 (AUTO, 3 attack SP): every 4th attack hits twice for 105 %
const bind = h.hooksOf('statusApplied').find((c) => c.status === 'bind' && c.target === fresh);
assert.ok(bind, 'rooted');
approx(bind.duration, sk.bb.unmove, 'root time');
// (her hits so far plus the ones during the root — T1 33 % / 23 % ATK at full potential — would burst it before the
// ×1 check below, and a bursting enemy takes no 神经损伤: its gauge starts over here)
fresh.elem.neural = 0;
const n0 = h.hooksOf('damaged').length;
const other = h.spawn('enemy_dummy', { pos: [12, 12] });
h.b.dealDamage(other, fresh, { type: 'element', element: 'neural', amount: 100 });
@@ -254,7 +257,7 @@ test('S2 + 本能的召唤: the placed piece deploys at the battle start (untarg
const ticks = h.hooksOf('damaged').slice(n0).filter((c) => c.target === inside && c.source === u);
const arts = ticks.filter((c) => c.type === 'arts'), neural = ticks.filter((c) => c.type === 'element');
assert.equal(arts.length, 12, '12 arts ticks');
// 12 神经损伤 ticks — unless the gauge bursts first (then the 爆发冷却 takes no more: elite 12 × 92.6 > 1000)
// 12 神经损伤 ticks — unless the gauge bursts first (then the 爆发冷却 takes no more: elite 12 × 97.2 > 1000)
const per = atkAtDeploy * sk.bb.ep_damage_ratio_token;
assert.equal(neural.length, Math.min(12, Math.ceil(1000 / per)), '神经损伤 ticks');
for (const c of arts) approx(c.amount, atkAtDeploy * sk.bb.atk_scale, `arts ${sk.bb.atk_scale * 100} % of the cached ATK`);
@@ -402,7 +405,7 @@ test('S3 迷狂牢笼: no 牢笼 on a tile with a unit or on high ground; at the
assert.ok(h.runUntil(() => u.skill.active, 3));
const splash = elemOf(h, u, n0).filter((c) => c.target === side);
assert.equal(splash.length, 1, 'T1\'s splash once around it');
approx(splash[0].amount, u.s.atk * 0.2, '20 % of her (S3) ATK');
approx(splash[0].amount, u.s.atk * 0.23, '23 % of her (S3) ATK');
const c1 = cagesOf(h).find((c) => c.tileR === 10 && c.tileC === 7);
assert.ok(c1 && pre.blockedBy === c1, 'a 牢笼 for the bursting one');
// a unit on the tile: none
+4 -4
View File
@@ -68,7 +68,7 @@ function redeployNow(h, u) {
assert.ok(h.b.redeploy(u, { free: true }), `${u.name} redeployed`);
}
test('傀影 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 18 s redeploy, 维多利亚, no 特质; his placed 镜中虚影 deploys after him with the variant\'s stats, 禁疗 and its copy of his skill', () => {
test('傀影 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 1-1, blocks 1, melee ground-only, 16 s redeploy, 维多利亚, no 特质; his placed 镜中虚影 deploys after him with the variant\'s stats, 禁疗 and its copy of his skill', () => {
assert.equal(OPERATOR_KITS[PHATOM], KITS[PHATOM]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -77,7 +77,7 @@ test('傀影 in every 自选 form: his operator kit (all three skills authored),
const form = formOf(tier, elite), m = elite ? modOf(tier, mod) : null, tw = twinOf(tier, elite, mod);
assert.deepEqual([u.def.charId, u.def.diyFor, u.skill.id, u.skill.kind, !!u.kit.generic, u.kit.skillSource], [PHATOM, SLOT[tier], form.skills[skill].skillId, 'passive', false, 'skills'], label(f));
assert.deepEqual([u.base.maxHp, u.base.atk, u.base.def, u.base.aspd, u.base.respawnTime, u.base.cost],
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 100 + (m?.attr.aspd ?? 0), 18, 10], `${label(f)}: stats`);
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), 100 + (m?.attr.aspd ?? 0), 16, 8], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly], [1, 'melee', 'phys', false], `${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 / 特质`);
@@ -91,9 +91,9 @@ test('傀影 in every 自选 form: his operator kit (all three skills authored),
done(h);
}
}
// the numbers (zh_CN): 傀影 E2 Lv1 1283 / 435 / 270, E2 Lv60 1523 / 517 / 304; the twin 1126 / 429 / 226 → 1318 / 508 / 254,
// the numbers (zh_CN): 傀影 E2 Lv1 1413 / 457 / 270, E2 Lv60 1653 / 539 / 304; the twin 1126 / 429 / 226 → 1318 / 508 / 254,
// EXE-Y stage 3 "属性进一步增强" 1418 / 568 / 294; modules
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], [1283, 435, 1523, 517]);
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], [1413, 457, 1653, 539]);
assert.deepEqual([twinOf(5, false).stats.maxHp, twinOf(5, true).stats.atk, twinOf(6, true, EY).stats.maxHp, twinOf(6, true, EY).stats.atk, twinOf(5, true, EY).stats.atk], [1126, 508, 1418, 568, 508]);
assert.deepEqual([modOf(5, EX).attr, modOf(6, EX).attr, modOf(5, EY).attr, modOf(6, EY).attr, modOf(5, IS).attr, modOf(6, IS).attr],
[{ maxHp: 100, atk: 50 }, { maxHp: 140, atk: 73 }, { atk: 40, def: 24 }, { atk: 75, def: 30 }, { atk: 37, aspd: 5 }, { atk: 71, aspd: 7 }]);
+3 -3
View File
@@ -78,7 +78,7 @@ test('菲亚梅塔 in every 自选 form: her kit (all three skills authored), th
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => [s.skillType, s.trigger.rule]), [['MANUAL', 'DEFAULT'], ['MANUAL', 'DEFAULT'], ['MANUAL', 'DEFAULT']]);
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], [1540, 671, 1796, 797]);
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], [1540, 706, 1796, 832]);
assert.deepEqual([Y, X, ISW].flatMap((id) => [modOf(5, id).attr, modOf(6, id).attr]),
[{ atk: 48, def: 26 }, { atk: 70, def: 43 }, { atk: 60, def: 17 }, { atk: 85, def: 32 }, { maxHp: 85, atk: 65 }, { maxHp: 115, atk: 90 }]);
const data = { chess: CHESS, backups: BACKUPS };
@@ -139,12 +139,12 @@ test('T1 精力充沛: ATK +25 % above 50 % HP, +50 % above 80 % (ART-X stage 3:
}
});
test('T2 宣告终局: ASPD +27 outside a running skill, none while S1 / S3 run (S2 is instant); ART-Y stage 3: +30 outside, +10 inside', () => {
test('T2 宣告终局: ASPD +30 outside a running skill, none while S1 / S3 run (S2 is instant); ART-Y stage 3: +33 outside, +10 inside', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t1 = talentsOf(tier, elite, mod)[1].bb;
const y3 = elite && mod === Y && tier === 6;
assert.deepEqual([t1.attack_speed, t1['phenxi_e_t_2[in_skill].attack_speed'] ?? 0], y3 ? [30, 10] : [27, 0], label(f));
assert.deepEqual([t1.attack_speed, t1['phenxi_e_t_2[in_skill].attack_speed'] ?? 0], y3 ? [33, 10] : [30, 0], label(f));
for (const skill of [0, 1, 2]) {
const { h, u } = field({ tier, elite, mod, skill });
const own = formOf(tier, elite).stats.aspd + (elite ? modOf(tier, mod)?.attr.aspd ?? 0 : 0);
+6 -6
View File
@@ -33,7 +33,7 @@ const ENEMIES = {
/** Every 自选 form: [tier, elite, module]. */
const FORMS_ALL = [[5, false, null], [6, false, null], ...[5, 6].flatMap((t) => [null, SIEX, SIEY].map((m) => [t, true, m]))];
/** 学生楷模: she is a 【乌萨斯学生自治团】 operator herself — her ATK always carries the talent's +atk. */
const studentAtk = (tier, elite, mod) => (elite && mod === SIEY && tier === 6 ? 0.12 : 0.08);
const studentAtk = (tier, elite, mod) => (elite && mod === SIEY && tier === 6 ? 0.14 : 0.1);
/** 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 = 3, others = [], seed = 5 } = {}) {
@@ -71,9 +71,9 @@ test('早露 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1351 / 856 / 100, E2 Lv60 1619 / 973 / 115; SIE-X +60 / +30 → +90 / +45,
// the numbers of the forms (zh_CN): E2 Lv1 1351 / 890 / 100, E2 Lv60 1619 / 1007 / 115; SIE-X +60 / +30 → +90 / +45,
// SIE-Y +70 / +20 → +100 / +30 ATK / 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], [1351, 856, 1619, 973]);
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], [1351, 890, 1619, 1007]);
assert.deepEqual([modOf(5, SIEX).attr, modOf(6, SIEX).attr, modOf(5, SIEY).attr, modOf(6, SIEY).attr], [{ atk: 60, def: 30 }, { atk: 90, def: 45 }, { atk: 70, def: 20 }, { atk: 100, def: 30 }]);
});
@@ -239,7 +239,7 @@ test('T1 深入骨髓: her damage ignores 60 % of the DEF of an enemy of 重量
}
});
test('T2 学生楷模: every 【乌萨斯学生自治团】 operator of her team — her and 古米, deployed or not — ATK +8 %, nobody else; SIE-Y stage 3: +12 % and +15 % per student in skill (≤ +45 %), re-read every 0.3 s', () => {
test('T2 学生楷模: every 【乌萨斯学生自治团】 operator of her team — her and 古米, deployed or not — ATK +10 %, nobody else; SIE-Y stage 3: +14 % and +15 % per student in skill (≤ +45 %), re-read every 0.3 s', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 'chess_char_1_10_a', row: 12, col: 3 }, { uid: 3, chessId: 'chess_char_1_02_a', row: 12, col: 6 }];
@@ -257,7 +257,7 @@ test('T2 学生楷模: every 【乌萨斯学生自治团】 operator of her team
}
// SIE-Y stage 3: the extra +15 % per student operator on the field whose skill runs, at most +45 %
const t6 = formOf(6, true).modules.find((m) => m.uniEquipId === SIEY).talentChanges;
assert.deepEqual([t6.find((t) => t.talentIndex === 1).bb.atk, t6.find((t) => t.talentIndex === -1).bb], [0.12, { init_atk: 0.15, max_atk: 0.45 }]);
assert.deepEqual([t6.find((t) => t.talentIndex === 1).bb.atk, t6.find((t) => t.talentIndex === -1).bb], [0.14, { init_atk: 0.15, max_atk: 0.45 }]);
const others = [
{ uid: 2, chessId: 'chess_char_1_10_a', row: 12, col: 3, skillIndex: 1 }, { uid: 3, chessId: 'chess_char_1_10_b', row: 12, col: 5, skillIndex: 1 },
{ uid: 4, diy: { slot: 'chess_char_6_diy2_a', charId: 'char_1051_headb2', skillIndex: 0 }, row: 11, col: 8 },
@@ -272,7 +272,7 @@ test('T2 学生楷模: every 【乌萨斯学生自治团】 operator of her team
h.step(); // at once: 每次开启技能时
for (const a of team) approx(drive(a), 0.15, `one in skill: ${a.def.charId}`);
assert.equal(drive(h.unit(5)), 0, '角峰 gets nothing');
approx(u.s.atk, u.base.atk * (1 + 0.12 + 0.15 + skillOf(6, true, S1).bb.atk), 'her ATK: +12 % +15 % (and her S1)');
approx(u.s.atk, u.base.atk * (1 + 0.14 + 0.15 + skillOf(6, true, S1).bb.atk), 'her ATK: +14 % +15 % (and her S1)');
cast(h.unit(2));
h.step();
approx(drive(h.unit(4)), 0.3, 'two in skill');
+10 -10
View File
@@ -75,9 +75,9 @@ test('电弧 in every 自选 form: her kit (all three skills authored), the form
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 844 / 400 / 118, E2 Lv60 993 / 444 / 135; SO-A +125 / +27 → +165 / +43 HP / ATK,
// the numbers of the forms (zh_CN): E2 Lv1 844 / 424 / 118, E2 Lv60 993 / 468 / 135; SO-A +125 / +27 → +165 / +43 HP / ATK,
// SO-B +41 → +65 ATK
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], [844, 400, 993, 444]);
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], [844, 424, 993, 468]);
assert.deepEqual([modOf(5, SOA).attr, modOf(6, SOA).attr, modOf(5, SOB).attr, modOf(6, SOB).attr], [{ maxHp: 125, atk: 27 }, { maxHp: 165, atk: 43 }, { atk: 41 }, { atk: 65 }]);
});
@@ -127,23 +127,23 @@ test('T1 卡带里的灵感: her skill\'s summon is her piece; the deck — 5 he
}
});
test('T2 加油~: her summons on the field get 鼓舞 = 12 % of HER current ATK / DEF / max HP (PRTS 修正 电弧自身), added after their own multipliers, refreshed every second; the strongest 鼓舞 of a kind is kept', () => {
test('T2 加油~: her summons on the field get 鼓舞 = 15 % of HER current ATK / DEF / max HP (PRTS 修正 电弧自身), added after their own multipliers, refreshed every second; the strongest 鼓舞 of a kind is kept', () => {
for (const f of [[5, false, null], [6, true, SOA]]) {
const [tier, elite, mod] = f;
const { h, u, ts } = field({ tier, elite, mod, skill: 0, towers: [[DIVE, 10, 6]] });
const t = ts[0];
assert.deepEqual(formOf(tier, elite).talents.find((x) => x.index === 1).bb, { max_hp: 0.12, def: 0.12, atk: 0.12 });
approx(t.s.atk, t.base.atk + 0.12 * u.s.atk, `${label(f)}: ATK`);
approx(t.s.def, t.base.def + 0.12 * u.s.def, `${label(f)}: DEF`);
approx(t.s.maxHp, t.base.maxHp + 0.12 * u.s.maxHp, `${label(f)}: max HP`);
assert.deepEqual(formOf(tier, elite).talents.find((x) => x.index === 1).bb, { max_hp: 0.15, def: 0.15, atk: 0.15 });
approx(t.s.atk, t.base.atk + 0.15 * u.s.atk, `${label(f)}: ATK`);
approx(t.s.def, t.base.def + 0.15 * u.s.def, `${label(f)}: DEF`);
approx(t.s.maxHp, t.base.maxHp + 0.15 * u.s.maxHp, `${label(f)}: max HP`);
// her ATK rises: the 鼓舞 follows within a second
h.b.addBuff(u, { key: 'test:atk', mods: { atkPct: 1 } });
h.run(1.05);
approx(t.s.atk, t.base.atk + 0.12 * u.s.atk, 'updated every second');
approx(t.s.atk, t.base.atk + 0.15 * u.s.atk, 'updated every second');
// a summon's own ATK +% does not scale it
h.b.addBuff(t, { key: 'test:tatk', mods: { atkPct: 0.5 } });
h.run(1.05);
approx(t.s.atk, t.base.atk * 1.5 + 0.12 * u.s.atk, 'after its multipliers');
approx(t.s.atk, t.base.atk * 1.5 + 0.15 * u.s.atk, 'after its multipliers');
// a stronger 鼓舞 of another source stays
h.b.addBuff(t, { key: 'inspire', mods: { atkFlat: 5000 }, duration: 30, data: { src: -1, val: 5000 } });
h.run(1.05);
@@ -173,7 +173,7 @@ test('S1 律动线: +1 held; she and her summons DEF +40 % / +60 % and a 屏障
assert.deepEqual(bt.shieldType, ['phys', 'arts', 'true'], 'not 元素伤害');
// the knocked-out one comes back during the skill: its 屏障 counts its 鼓舞 HP
assert.ok(h.runUntil(() => late.alive, 21), 'back during the skill');
approx(late.findBuff('radian:s1:barrier').shield, (late.base.maxHp + 0.12 * u.s.maxHp) * sk.bb.hp_ratio, 'with 鼓舞 HP');
approx(late.findBuff('radian:s1:barrier').shield, (late.base.maxHp + 0.15 * u.s.maxHp) * sk.bb.hp_ratio, 'with 鼓舞 HP');
h.runUntil(() => !u.skill.active, 30);
assert.ok([u, t, late].every((x) => !x.findBuff('radian:s1:barrier')), 'gone at its end');
assert.equal(t.findBuff('radian:s1:tower'), null, 'DEF back');
+6 -6
View File
@@ -106,8 +106,8 @@ test('trait 猎手: 8 bullets, one per attack at ×1.2 (HUN-Y ×1.33); empty ⇒
const hits = atkHits(h, u);
assert.equal(hits.length, 8);
const sc = mod === HY ? 1.33 : 1.2;
// (入神 adds +8 % / +9 % per attack on the same target; the first shot has one layer)
const t1 = (mod === HY && tier === 6) ? 0.09 : 0.08;
// (入神 adds +9 % / +10 % per attack on the same target; the first shot has one layer)
const t1 = (mod === HY && tier === 6) ? 0.1 : 0.09;
approx(hits[0].amount, u.base.atk * (1 + t1) * sc, `${mod}: ×${sc}`);
const iv = u.s.interval;
approx(iv, 1.6 * 100 / (u.base.aspd + 100), 'ASPD +100 shortens her attacks');
@@ -149,12 +149,12 @@ test('莱伊 shoots air units (猎手 "可对空"): a flyer in her range is her
}
});
test('T2 入神: +8 % ATK per attack on the same target, ≤ 3 layers (HUN-Y stage 3: +9 %, ≤ 4), lost on a new target (one layer again)', () => {
test('T2 入神: +9 % ATK per attack on the same target, ≤ 3 layers (HUN-Y stage 3: +10 %, ≤ 4), lost on a new target (one layer again)', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, HY], [6, true, HY], [6, true, HX]]) {
const y3 = tier === 6 && mod === HY;
const [per, max] = y3 ? [0.09, 4] : [0.08, 3];
const [per, max] = y3 ? [0.1, 4] : [0.09, 3];
const t1 = (mod ? formOf(tier, true) : formOf(tier, elite)).talents.find((t) => t.index === 1).bb;
if (!y3) assert.deepEqual(t1, { atk: 0.08, max_stack_cnt: 3 });
if (!y3) assert.deepEqual(t1, { atk: 0.09, max_stack_cnt: 3 });
const { h, u } = field({ tier, elite, mod, skill: 1 });
u.skill.sp = 0;
h.b.addBuff(u, { key: 'test:noSp', flags: { noSp: true } });
@@ -237,7 +237,7 @@ test('S2 广域警觉 (AUTO, attack SP 16): on at full SP — no enemy needed be
assert.ok(h.runUntil(() => u.skill.active, 120), `T${tier}: on`);
assert.equal(h.hooksOf('attack').filter((c) => c.attacker === u).length, 16, `T${tier}: after 16 attacks (reloads give no SP)`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 4-10`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + u.mem.rayStacks * 0.08), `T${tier}: ATK (with 入神's layers)`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk + u.mem.rayStacks * 0.09), `T${tier}: ATK (with 入神's layers)`);
h.b.kill(e, null);
h.run(200);
assert.ok(u.skill.active, `T${tier}: 持续时间无限`);
+7 -7
View File
@@ -67,8 +67,8 @@ test('嵯峨 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// E2 Lv1 1609 / 434 / 297, E2 Lv60 2004 / 498 / 347; SOL-Y +75 / +45 / +15 → +125 / +55 / +20, SOL-X +50 / +28 → +70 / +44
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], [1609, 434, 2004, 498]);
// E2 Lv1 1609 / 459 / 297, E2 Lv60 2004 / 523 / 347 (full potential); SOL-Y +75 / +45 / +15 → +125 / +55 / +20, SOL-X +50 / +28 → +70 / +44
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], [1609, 459, 2004, 523]);
assert.deepEqual([modOf(5, SOLY).attr, modOf(6, SOLY).attr, modOf(5, SOLX).attr, modOf(6, SOLX).attr],
[{ maxHp: 75, atk: 45, def: 15 }, { maxHp: 125, atk: 55, def: 20 }, { atk: 50, def: 28 }, { atk: 70, def: 44 }]);
});
@@ -326,24 +326,24 @@ test('T1 劝善 SOL-X stage 3: her damage on a target below half HP ×1.15 (stag
}
});
test('T2 清明: once per deployment, the first time she drops below 40 %: 70 % physical dodge and 5 % max HP/s for 15 s; SOL-Y stage 3: 6.5 % (its blackboard) for 18 s', () => {
test('T2 清明: once per deployment, the first time she drops below 40 %: 70 % physical dodge and 6 % max HP/s for 17 s; SOL-Y stage 3: 7 % (its blackboard) for 20 s', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 12, col: 9 });
const y3 = elite && tier === 6 && mod === SOLY;
const tb = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual(tb, { hp_ratio: 0.4, hp_recovery_per_sec_by_max_hp_ratio: y3 ? 0.065 : 0.05, prob: 0.7, duration: y3 ? 18 : 15 }, label(f));
assert.deepEqual(tb, { hp_ratio: 0.4, hp_recovery_per_sec_by_max_hp_ratio: y3 ? 0.07 : 0.06, prob: 0.7, duration: y3 ? 20 : 17 }, label(f));
h.b.dealDamage(null, u, { amount: u.s.maxHp * 0.55, type: 'true' });
assert.equal(u.findBuff('talent:saga:qingming'), null, `${label(f)}: at 45 % nothing`);
h.b.dealDamage(null, u, { amount: u.s.maxHp * 0.1, type: 'true' });
const b = u.findBuff('talent:saga:qingming');
assert.ok(b, `${label(f)}: below 40 %`);
assert.deepEqual(b.mods, { dodgePhys: 0.7, hpRegenRatio: y3 ? 0.065 : 0.05 }, `${label(f)}: mods`);
approx(b.duration, y3 ? 18 : 15, `${label(f)}: duration`);
assert.deepEqual(b.mods, { dodgePhys: 0.7, hpRegenRatio: y3 ? 0.07 : 0.06 }, `${label(f)}: mods`);
approx(b.duration, y3 ? 20 : 17, `${label(f)}: duration`);
approx(u.s.dodgePhys, 0.7, `${label(f)}: dodge`);
const hp0 = u.hp;
h.run(2.01);
approx(u.hp - hp0, 2 * (y3 ? 0.065 : 0.05) * u.s.maxHp, `${label(f)}: regeneration`, 0.02);
approx(u.hp - hp0, 2 * (y3 ? 0.07 : 0.06) * u.s.maxHp, `${label(f)}: regeneration`, 0.02);
h.run(b.duration);
assert.equal(u.findBuff('talent:saga:qingming'), null, `${label(f)}: over`);
u.hp = u.s.maxHp;
+6 -6
View File
@@ -76,7 +76,7 @@ test('闪灵 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1451 / 424 / 110, E2 Lv60 1558 / 494 / 129; PHY-Y +35 ATK +5 ASPD → +60 / +7,
// the numbers of the forms (zh_CN): E2 Lv1 1451 / 424 / 135, E2 Lv60 1558 / 494 / 154; PHY-Y +35 ATK +5 ASPD → +60 / +7,
// PHY-X +45 ATK +13 DEF → +63 / +20
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], [1451, 424, 1558, 494]);
assert.deepEqual([modOf(5, PHYY).attr, modOf(6, PHYY).attr, modOf(5, PHYX).attr, modOf(6, PHYX).attr], [{ atk: 35, aspd: 5 }, { atk: 60, aspd: 7 }, { atk: 45, def: 13 }, { atk: 63, def: 20 }]);
@@ -185,19 +185,19 @@ test('S3 教条力场 (MANUAL, data DEFAULT): 60 s, ATK +30 % / +40 %, every all
}
});
test('T1 黑恶魔的庇护: allies in her range DEF +60 (herself too, not outside); PHY-Y stage 3: +100 and 地面 units +40 more (stage 1: +60 only)', () => {
test('T1 黑恶魔的庇护: allies in her range DEF +65 (herself too, not outside); PHY-Y stage 3: +105 and 地面 units +40 more (stage 1: +65 only)', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u, ground, high, out } = field({ tier, elite, mod, skill: 0 });
h.run(0.3);
const y3 = elite && tier === 6 && mod === PHYY;
const d = y3 ? 100 : 60, low = y3 ? 40 : 0;
const d = y3 ? 105 : 65, low = y3 ? 40 : 0;
assert.deepEqual(ground.findBuff('talent:shining:def')?.mods, { defFlat: d + low }, `${label(f)}: 地面 ally`);
assert.deepEqual(high.findBuff('talent:shining:def')?.mods, { defFlat: d }, `${label(f)}: 高台 ally`);
assert.deepEqual(u.findBuff('talent:shining:def')?.mods, { defFlat: d }, `${label(f)}: herself (高台)`);
assert.equal(out.findBuff('talent:shining:def'), null, `${label(f)}: outside her range`);
approx(ground.s.def, 100 + d + low, `${label(f)}: DEF of the 地面 ally`);
if (y3) assert.match(u.def.raw.talents.find((t) => t.index === 0).desc, /防御力\+100,地面单位防御力额外\+40/);
if (y3) assert.match(u.def.raw.talents.find((t) => t.index === 0).desc, /防御力\+105(\+5),地面单位防御力额外\+40/);
h.b.retreat(u);
h.run(0.3);
assert.equal(ground.findBuff('talent:shining:def'), null, `${label(f)}: gone once she leaves`);
@@ -205,13 +205,13 @@ test('T1 黑恶魔的庇护: allies in her range DEF +60 (herself too, not outsi
}
});
test('T2 法典: ASPD +10; PHY-X stage 3: ASPD +15, ATK +25 % with 自动掩护 picked, SP +0.6/s with 教条力场 picked (stage 1: ASPD +10 only)', () => {
test('T2 法典: ASPD +13; PHY-X stage 3: ASPD +18, ATK +25 % with 自动掩护 picked, SP +0.6/s with 教条力场 picked (stage 1: ASPD +13 only)', () => {
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 x3 = elite && tier === 6 && mod === PHYX;
approx(u.s.aspd, u.base.aspd + (x3 ? 15 : 10), `${label(f)} S${skill + 1}: ASPD`);
approx(u.s.aspd, u.base.aspd + (x3 ? 18 : 13), `${label(f)} S${skill + 1}: ASPD`);
approx(u.s.atk, u.base.atk * (1 + (x3 && skill === 1 ? 0.25 : 0)), `${label(f)} S${skill + 1}: ATK`);
approx(u.s.spRecovery, 1 + (x3 && skill === 2 ? 0.6 : 0), `${label(f)} S${skill + 1}: SP/s`);
if (x3 && skill === 2) {
+4 -4
View File
@@ -86,8 +86,8 @@ test('黍 in every 自选 form: her operator kit (all three skills authored), th
done(h);
}
}
// zh_CN numbers: E2 Lv1 2313 / 392 / 493, E2 Lv60 2910 / 450 / 565; GUA-X +160/+30/+30 → +270/+50/+50
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], [2313, 392, 2910, 450]);
// zh_CN numbers: E2 Lv1 2513 / 417 / 493, E2 Lv60 3110 / 475 / 565; GUA-X +160/+30/+30 → +270/+50/+50
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], [2513, 417, 3110, 475]);
assert.deepEqual([modOf(5, GUAX).attr, modOf(6, GUAX).attr], [{ maxHp: 160, atk: 30, def: 30 }, { maxHp: 270, atk: 50, def: 50 }]);
});
@@ -257,7 +257,7 @@ test('S3 离离枯荣: a ground enemy on a sown tile ⇒ the operators of her ra
}
});
test('T1 百谷长青: her heals sow the healed unit\'s tile and the 4 next to it (walkable / deployable tiles only); units on them regenerate 70 / s (80 GUA-X stage 3) and take 10 % (15 %) 庇护 — a 无法被治疗 unit and summons too; all gone when she leaves; GUA-X stage 3 sows her tile at the deployment', () => {
test('T1 百谷长青: her heals sow the healed unit\'s tile and the 4 next to it (walkable / deployable tiles only); units on them regenerate 75 / s (85 GUA-X stage 3) and take 12 % (17 %) 庇护 — a 无法被治疗 unit and summons too; all gone when she leaves; GUA-X stage 3 sows her tile at the deployment', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const others = [{ uid: 2, chessId: 't_a', row: 9, col: 5 }, { uid: 3, chessId: 't_b', row: 12, col: 9 }, { uid: 4, chessId: 'chess_char_3_19_a', row: 12, col: 3 },
@@ -265,7 +265,7 @@ test('T1 百谷长青: her heals sow the healed unit\'s tile and the 4 next to i
const { h, u } = field({ tier, elite, mod, skill: 2, others });
const t0 = u.def.raw.talents.find((t) => t.index === 0);
const x3 = elite && mod === GUAX && tier === 6;
assert.deepEqual([t0.bb.hp_recovery_per_sec, t0.bb.damage_resistance, t0.bbStr?.born_range_id ?? null], x3 ? [80, 0.15, '0-1'] : [70, 0.1, null], label(f));
assert.deepEqual([t0.bb.hp_recovery_per_sec, t0.bb.damage_resistance, t0.bbStr?.born_range_id ?? null], x3 ? [85, 0.17, '0-1'] : [75, 0.12, null], label(f));
assert.equal(sownOf(u).has(key(10, 5)), x3, `${label(f)}: her own tile sown at the deployment (GUA-X stage 3 only)`);
const [a, b, wolf] = [h.unit(2), h.unit(3), h.unit(5)];
a.hp = a.s.maxHp * 0.5;
+6 -6
View File
@@ -59,7 +59,7 @@ test('黑 in every 自选 form: her operator kit (all three skills authored), th
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], [SHWAZ, SLOT[tier], form.skills[skill].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`);
// ARC-X “定制弩配件套装”: "再部署时间减少" — the attribute respawn_time −25 (70 → 45 s)
// ARC-X “定制弩配件套装”: "再部署时间减少" — the attribute respawn_time −25 (66 → 41 s at full potential)
assert.equal(u.base.respawnTime, form.stats.respawnTime + (m?.attr.respawnTime ?? 0), `${label(f)}: redeploy time`);
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`);
@@ -69,9 +69,9 @@ test('黑 in every 自选 form: her operator kit (all three skills authored), th
}
}
// the data: all three DEFAULT (S3's 「攻击范围改为」 is an attack-range change, not a 技能范围 — tools/build-data.mjs
// ATTACK_RANGE_CHANGE); E2 Lv1 1393 / 646 / 164, Lv60 1685 / 775 / 204
// ATTACK_RANGE_CHANGE); E2 Lv1 1393 / 676 / 164, Lv60 1685 / 805 / 204
assert.deepEqual(formOf(6, true).skills.map((s) => s.trigger.rule), ['DEFAULT', 'DEFAULT', 'DEFAULT']);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [646, 775]);
assert.deepEqual([FORMS['2/1/4/0'].stats.atk, FORMS['2/60/7/1'].stats.atk], [676, 805]);
assert.deepEqual([modOf(5, X).attr, modOf(6, X).attr, modOf(5, Y).attr, modOf(6, Y).attr],
[{ maxHp: 130, atk: 55, respawnTime: -25 }, { maxHp: 170, atk: 75, respawnTime: -25 }, { atk: 80, def: 10 }, { atk: 120, def: 16 }]);
});
@@ -187,11 +187,11 @@ test('T1 破甲箭头: a proc ×160 % (ARC-Y stage 3: 170 %) and, once it hit, D
}
});
test('T2 交叉火力: with another 【狙击】 operator on the field every sniper (her too, a partner\'s too) ATK +8 % — not 角峰, not alone, not once she leaves; ARC-X stage 3 "携带": +13 % on her player\'s snipers for the whole battle', () => {
test('T2 交叉火力: with another 【狙击】 operator on the field every sniper (her too, a partner\'s too) ATK +10 % — not 角峰, not alone, not once she leaves; ARC-X stage 3 "携带": +15 % on her player\'s snipers for the whole battle', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const squad = tier === 6 && mod === X;
const v = squad ? 0.13 : 0.08;
const v = squad ? 0.15 : 0.1;
const t1 = { ...formOf(tier, elite).talents[1].bb, ...(modOf(tier, mod)?.talentChanges ?? []).filter((t) => t.talentIndex === 1).reduce((o, t) => ({ ...o, ...t.bb }), {}) };
assert.equal(t1.atk, v, label(f));
// alone: nothing
@@ -220,7 +220,7 @@ test('T2 交叉火力: with another 【狙击】 operator on the field every sni
{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: tier === 6 ? 'chess_char_6_diy1_b' : 'chess_char_5_diy1_b', diy: { charId: SHWAZ, skillIndex: 0, uniEquipId: mod }, row: 10, col: 5 }], bonds: {}, bandId: null, playerEffects: [] },
{ playerId: 'p2', seat: 1, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: PROVE, row: 12, col: 3 }], bonds: {}, bandId: null, playerEffects: [] },
] });
for (const [mod, tier, want] of [[null, 6, 0.08], [X, 6, null]]) {
for (const [mod, tier, want] of [[null, 6, 0.1], [X, 6, null]]) {
const { h } = pair(mod, tier);
h.run(1);
const mine = h.b.allyUnits.find((a) => a.ownerId === 'p1'), theirs = h.b.allyUnits.find((a) => a.ownerId === 'p2');
+8 -8
View File
@@ -63,9 +63,9 @@ test('推进之王 in every 自选 form: its operator kit (all three skills auth
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 1643 / 422 / 307, E2 Lv60 2046 / 484 / 358; SOL-X +60 / +40 → +75 / +50 ATK / DEF,
// SOL-Y +200 / +65 → +260 / +82 HP / ATK
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], [1643, 422, 2046, 484]);
// the numbers of the forms (zh_CN, full potential: ATK +25): E2 Lv1 1643 / 447 / 307, E2 Lv60 2046 / 509 / 358; SOL-X +60 / +40
// → +75 / +50 ATK / DEF, SOL-Y +200 / +65 → +260 / +82 HP / ATK
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], [1643, 447, 2046, 509]);
assert.deepEqual([modOf(5, SOLX).attr, modOf(6, SOLX).attr, modOf(5, SOLY).attr, modOf(6, SOLY).attr], [{ atk: 60, def: 40 }, { atk: 75, def: 50 }, { maxHp: 200, atk: 65 }, { maxHp: 260, atk: 82 }]);
});
@@ -160,7 +160,7 @@ test('S3 碎颅击 (MANUAL, data DEFAULT): 18 / 21 s, attack interval 1.05 + 1.0
}
});
test('T1 万兽之王: every 【先锋】 operator of the field ATK / DEF +8 % while she is deployed (not other professions); SOL-X stage 3 adds her own +8 % / +8 %', () => {
test('T1 万兽之王: every 【先锋】 operator of the field ATK / DEF +10 % while she is deployed (not other professions); SOL-X stage 3 adds her own +8 % / +8 %', () => {
const others = [{ uid: 2, chessId: 'chess_char_1_08_a', row: 11, col: 3 }, { uid: 3, chessId: 'chess_char_1_02_a', row: 11, col: 7 }];
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -169,11 +169,11 @@ test('T1 万兽之王: every 【先锋】 operator of the field ATK / DEF +8 % w
const texas = h.unit(2), yak = h.unit(3);
const t0 = u.def.raw.talents.find((t) => t.index === 0);
const extra = elite && mod === SOLX && tier === 6 ? 0.08 : 0;
assert.deepEqual(texas.findBuff('talent:siege:pioneers')?.mods, { atkPct: 0.08, defPct: 0.08 }, `${label(f)}: 德克萨斯 (先锋)`);
assert.deepEqual(texas.findBuff('talent:siege:pioneers')?.mods, { atkPct: 0.1, defPct: 0.1 }, `${label(f)}: 德克萨斯 (先锋)`);
assert.equal(yak.findBuff('talent:siege:pioneers'), null, `${label(f)}: 角峰 (重装) gets nothing`);
assert.deepEqual(u.findBuff('talent:siege:pioneers')?.mods, { atkPct: 0.08, defPct: 0.08 }, `${label(f)}: herself`);
approx(u.s.atk, u.base.atk * (1 + 0.08 + extra), `${label(f)}: her ATK`);
approx(u.s.def, u.base.def * (1 + 0.08 + extra), `${label(f)}: her DEF`);
assert.deepEqual(u.findBuff('talent:siege:pioneers')?.mods, { atkPct: 0.1, defPct: 0.1 }, `${label(f)}: herself`);
approx(u.s.atk, u.base.atk * (1 + 0.1 + extra), `${label(f)}: her ATK`);
approx(u.s.def, u.base.def * (1 + 0.1 + extra), `${label(f)}: her DEF`);
assert.equal(!!u.findBuff('talent:siege:self'), extra > 0, `${label(f)}: the SOL-X self extra`);
if (extra) assert.match(t0.desc, /自身攻击力和防御力额外\+8%/, 'the module text');
h.b.retreat(u);
+4 -4
View File
@@ -69,9 +69,9 @@ test('琴柳 in every 自选 form: her kit (all three skills authored), the form
done(h);
}
}
// the form numbers (zh_CN): E2 Lv1 1339 / 458 / 311, E2 Lv60 1668 / 516 / 348; BEA-X +150 / +48 → +200 / +68 HP / ATK,
// 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, 458, 1668, 516]);
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 }]);
});
@@ -223,7 +223,7 @@ test('S3 光辉旗帜: +10 DP at once; the flag on the ground tile of the enemy
}
});
test('T1 不退之旗 in every form: operators of the 3×3 around her ASPD +10 (BEA-X stage 3 +13) — 孤立 ones too, summons not —, enemies there (ground and air) ASPD −10 (−13); nothing outside, nothing once she left', () => {
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;
@@ -235,7 +235,7 @@ test('T1 不退之旗 in every form: operators of the 3×3 around her ASPD +10 (
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 ? [13, -13] : [10, -10], label(f));
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`);
+3 -3
View File
@@ -230,14 +230,14 @@ test('T1 无声砥柱: every ally of her range (herself too) holds 庇护 base
}
});
test('T2 丰润羽翼: an ally of her range below 50 % HP gets 生命回复速度 5 % of her ATK (hpRegen, from 1 s after; 禁疗 does not stop it), a 【莱茵生命】 operator twice; none at ≥ 50 % or outside; BLS-X stage 3: 7 % and max HP +10 % (×2 莱茵生命) in her range; stage 1: neither', () => {
test('T2 丰润羽翼: an ally of her range below 50 % HP gets 生命回复速度 6 % of her ATK (hpRegen, from 1 s after; 禁疗 does not stop it), a 【莱茵生命】 operator twice; none at ≥ 50 % or outside; BLS-X stage 3: 8 % and max HP +10 % (×2 莱茵生命) in her range; stage 1: neither', () => {
for (const [tier, elite, mod] of [[5, false, null], [5, true, X], [6, true, X]]) {
const x3 = mod === X && tier === 6;
const { h, u } = field({ tier, elite, mod, skill: 0, others: [ally(2, 10, 6), ally(3, 11, 6, 'test_rhine_a'), ally(4, 9, 6)] });
const a = h.unit(2), r = h.unit(3), full = h.unit(4);
assert.ok(RHINE.has(r.def.charId) && !RHINE.has(a.def.charId));
const ratio = x3 ? 0.07 : 0.05;
assert.equal(formOf(tier, elite).talents[1].bb.atk_to_hp_recovery_ratio, 0.05);
const ratio = x3 ? 0.08 : 0.06;
assert.equal(formOf(tier, elite).talents[1].bb.atk_to_hp_recovery_ratio, 0.06);
approx(a.s.maxHp, 2000 * (x3 ? 1.1 : 1), 'max HP');
approx(r.s.maxHp, 2000 * (x3 ? 1.2 : 1), '莱茵生命 max HP');
h.b.addBuff(a, { key: 'test:healFree', flags: { noHeal: true, healFree: true } });
+17 -17
View File
@@ -76,9 +76,9 @@ test('棘刺 in every 自选 form: his operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2063 / 575 / 333, E2 Lv60 2427 / 665 / 379; LOR-X +160 / +39 / +5 → +260 / +55 / +7,
// the numbers of the forms (zh_CN): E2 Lv1 2063 / 601 / 333, E2 Lv60 2427 / 691 / 379; LOR-X +160 / +39 / +5 → +260 / +55 / +7,
// LOR-Δ +80 / +60 / +5 → +150 / +75 / +7 (HP / ATK / ASPD)
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], [2063, 575, 2427, 665]);
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], [2063, 601, 2427, 691]);
assert.deepEqual([modOf(5, LORX).attr, modOf(6, LORX).attr, modOf(5, LORD).attr, modOf(6, LORD).attr],
[{ maxHp: 160, atk: 39, aspd: 5 }, { maxHp: 260, atk: 55, aspd: 7 }, { maxHp: 80, atk: 60, aspd: 5 }, { maxHp: 150, atk: 75, aspd: 7 }]);
});
@@ -229,11 +229,11 @@ test('S3 至高之术 (MANUAL, attack SP, data ACTIVE_RANGE on 3-3): 30 s of ran
}
});
test('T1 神经腐蚀: an attack poisons its target — 3 ticks of 125 arts (250 on an enemy that attacks at range: RANGED / ALL), the first a second in, 持续伤害; a new hit refreshes it without a second instance', () => {
test('T1 神经腐蚀: an attack poisons its target — 3 ticks of 140 arts (280 on an enemy that attacks at range: RANGED / ALL), the first a second in, 持续伤害; a new hit refreshes it without a second instance', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const t0 = t0Of(tier, elite, null);
assert.deepEqual([t0['damage[normal]'], t0['damage[ranged]'], t0.duration], [125, 250, 3]);
for (const [key, dmg] of [['enemy_dummy', 125], ['enemy_ranged', 250], ['enemy_both', 250]]) {
assert.deepEqual([t0['damage[normal]'], t0['damage[ranged]'], t0.duration], [140, 280, 3]);
for (const [key, dmg] of [['enemy_dummy', 140], ['enemy_ranged', 280], ['enemy_both', 280]]) {
const { h, u } = field({ tier, elite, skill: 0 });
u.skill.sp = 0;
const e = h.spawn(key, { pos: [10, 6] });
@@ -258,15 +258,15 @@ test('T1 神经腐蚀: an attack poisons its target — 3 ticks of 125 arts (250
assert.equal(e.buffs.filter((b) => b.key === `thorns:poison:${u.id}`).length, 1, `T${tier}: one instance`);
const ticks = from(h, u, (c) => tagged(c, 'thorns:poison'));
assert.ok(ticks.length >= 8 && ticks.length <= 10, `T${tier}: ${ticks.length} ticks in 10 s`);
assert.ok(ticks.every((c) => Math.abs(c.amount - 125) < 1e-6), `T${tier}: the value never changes`);
assert.ok(ticks.every((c) => Math.abs(c.amount - 140) < 1e-6), `T${tier}: the value never changes`);
done(h);
}
});
test('T1 + LOR-X stage 3 "可叠加4次": the tick is 135 × the hits so far (≤ 4), 270 on a ranged enemy; stage 1 leaves the talent (125, one layer); a dodged hit poisons nothing', () => {
test('T1 + LOR-X stage 3 "可叠加4次": the tick is 150 × the hits so far (≤ 4), 300 on a ranged enemy; stage 1 leaves the talent (140, one layer); a dodged hit poisons nothing', () => {
const t03 = t0Of(6, true, LORX), t01 = t0Of(5, true, LORX);
assert.deepEqual([t03['damage[normal]'], t03['damage[ranged]'], t03.max_cnt, t01['damage[normal]'], t01.max_cnt], [135, 270, 4, 125, undefined]);
for (const [tier, per, cap] of [[6, 135, 4], [5, 125, 1]]) {
assert.deepEqual([t03['damage[normal]'], t03['damage[ranged]'], t03.max_cnt, t01['damage[normal]'], t01.max_cnt], [150, 300, 4, 140, undefined]);
for (const [tier, per, cap] of [[6, 150, 4], [5, 140, 1]]) {
const { h, u } = field({ tier, elite: true, mod: LORX, skill: 0 });
u.skill.sp = 0;
h.spawn('enemy_dummy', { pos: [10, 6] });
@@ -323,7 +323,7 @@ test('LOR-Δ “沙蚀” (stages 1 / 3): every damage instance he deals carries
}
// stage 3: the poison on an enemy in its 神经 burst
const t03 = t0Of(6, true, LORD);
assert.deepEqual([t03['damage[normal]'], t03['damage[ranged]'], t03.ep_break_multi], [300, 600, 2.5]);
assert.deepEqual([t03['damage[normal]'], t03['damage[ranged]'], t03.ep_break_multi], [315, 630, 2.5]);
{
const { h, u } = field({ tier: 6, elite: true, mod: LORD, skill: 0 });
u.skill.sp = 0;
@@ -332,7 +332,7 @@ test('LOR-Δ “沙蚀” (stages 1 / 3): every damage instance he deals carries
h.run(4);
const ticks = from(h, u, (c) => tagged(c, 'thorns:poison'));
assert.ok(ticks.length >= 2, 'ticks');
for (const c of ticks) { approx(c.amount, 300 * 2.5, '×2.5'); assert.deepEqual([c.type, c.dmg.element], ['elemental', 'neural']); }
for (const c of ticks) { approx(c.amount, 315 * 2.5, '×2.5'); assert.deepEqual([c.type, c.dmg.element], ['elemental', 'neural']); }
// thorns_e_003_t has no independentCharacterSource: one poison per enemy, not one per 棘刺
assert.deepEqual([!!e.findBuff('thorns:poison'), !!e.findBuff(`thorns:poison:${u.id}`)], [true, false], 'the shared poison');
done(h);
@@ -344,7 +344,7 @@ test('LOR-Δ “沙蚀” (stages 1 / 3): every damage instance he deals carries
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.b.addBuff(e, { key: 'neuralBurst', duration: 30, flags: { burstLock: true } });
h.run(4);
for (const c of from(h, u, (x) => tagged(x, 'thorns:poison'))) { approx(c.amount, 125, 'stage 1: 125'); assert.equal(c.type, 'arts'); }
for (const c of from(h, u, (x) => tagged(x, 'thorns:poison'))) { approx(c.amount, 140, 'stage 1: 140'); assert.equal(c.type, 'arts'); }
done(h);
}
// S2 spikes: they poison at stage 1 and without a module, never at LOR-Δ stage 3
@@ -359,14 +359,14 @@ test('LOR-Δ “沙蚀” (stages 1 / 3): every damage instance he deals carries
}
});
test('T2 故土潮声: 生命回复速度 +3.5 % max HP/s (an hpRegenRatio buff, no 治疗: 禁疗 does not stop it) once his last attack is 2 s old — off while he attacks; S2\'s spikes are no 主动攻击', () => {
test('T2 故土潮声: 生命回复速度 +4 % max HP/s (an hpRegenRatio buff, no 治疗: 禁疗 does not stop it) once his last attack is 2 s old — off while he attacks; S2\'s spikes are no 主动攻击', () => {
for (const f of [[5, false, null], [6, true, LORX]]) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0 });
const r = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
assert.deepEqual([r.delay, r.hp_recovery_per_sec_by_max_hp_ratio], [2, 0.035]);
assert.deepEqual(u.findBuff('talent:thorns:tide')?.mods, { hpRegenRatio: 0.035 }, `${label(f)}: on from the deployment`);
approx(u.s.hpRegen, u.s.maxHp * 0.035, `${label(f)}: 3.5 % max HP / s`);
assert.deepEqual([r.delay, r.hp_recovery_per_sec_by_max_hp_ratio], [2, 0.04]);
assert.deepEqual(u.findBuff('talent:thorns:tide')?.mods, { hpRegenRatio: 0.04 }, `${label(f)}: on from the deployment`);
approx(u.s.hpRegen, u.s.maxHp * 0.04, `${label(f)}: 4 % max HP / s`);
u.skill.sp = 0;
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.runUntil(() => h.hooksOf('attack').some((c) => c.attacker === u), 3);
@@ -381,7 +381,7 @@ test('T2 故土潮声: 生命回复速度 +3.5 % max HP/s (an hpRegenRatio buff,
h.b.addBuff(u, { key: 'test:healFree', flags: { healFree: true, noHeal: true } });
u.hp = u.s.maxHp * 0.5;
h.run(1);
approx(u.hp, u.s.maxHp * (0.5 + 0.035), `${label(f)}: +3.5 % in a second under 禁疗`, 0.01);
approx(u.hp, u.s.maxHp * (0.5 + 0.04), `${label(f)}: +4 % in a second under 禁疗`, 0.01);
done(h);
}
// S2: the spikes keep the regeneration on
+13 -12
View File
@@ -71,8 +71,9 @@ test('珊比 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2565 / 536 / 430, E2 Lv60 3194 / 625 / 516; PRP-X +250 / +50 → +450 / +80
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], [2565, 536, 3194, 625]);
// the numbers of the forms (zh_CN, full potential: HP +270): E2 Lv1 2835 / 536 / 430, E2 Lv60 3464 / 625 / 516; PRP-X +250 / +50
// → +450 / +80
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], [2835, 536, 3464, 625]);
assert.deepEqual([modOf(5, PRPX).attr, modOf(6, PRPX).attr], [{ maxHp: 250, atk: 50 }, { maxHp: 450, atk: 80 }]);
});
@@ -273,18 +274,18 @@ test('S3 belt damage: every second from the cast, each targetable ground enemy o
}
});
test('T1 探险理论: 元素损伤 she takes ×0.85; her physical damage adds 10 % ATK 侵蚀损伤 (not her arts), or — while the target\'s 侵蚀 burst runs — shortens that burst by 1 s instead', () => {
test('T1 探险理论: 元素损伤 she takes ×0.8; her physical damage adds 12 % ATK 侵蚀损伤 (not her arts), or — while the target\'s 侵蚀 burst runs — shortens that burst by 1.2 s instead', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 9 });
noSp(h, u);
assert.deepEqual(u.def.raw.talents.find((t) => t.index === 0).bb, { ep_damage_scale: 0.85, duration_dec: 1, 'ep_damage_ratio[trigger]': 0.1 }, label(f));
assert.deepEqual(u.def.raw.talents.find((t) => t.index === 0).bb, { ep_damage_scale: 0.8, duration_dec: 1.2, 'ep_damage_ratio[trigger]': 0.12 }, label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 8] });
h.step();
h.b.dealDamage(e, u, { type: 'element', element: 'burn', amount: 100 });
approx(u.elem.burn, 85, `${label(f)}: 元素损伤 taken ×0.85`);
approx(u.elem.burn, 80, `${label(f)}: 元素损伤 taken ×0.8`);
h.b.dealDamage(u, e, { amount: 100, type: 'phys' });
approx(e.elem.erosion, 0.1 * u.s.atk, `${label(f)}: 10 % ATK 侵蚀损伤 (not blocking: no module bonus)`);
approx(e.elem.erosion, 0.12 * u.s.atk, `${label(f)}: 12 % ATK 侵蚀损伤 (not blocking: no module bonus)`);
const e0 = e.elem.erosion;
h.b.dealDamage(u, e, { amount: 100, type: 'arts' });
assert.equal(e.elem.erosion, e0, `${label(f)}: not on arts damage`);
@@ -293,26 +294,26 @@ test('T1 探险理论: 元素损伤 she takes ×0.85; her physical damage adds 1
assert.ok(lock, `${label(f)}: bursting`);
const left = lock.timeLeft;
h.b.dealDamage(u, e, { amount: 100, type: 'phys' });
approx(lock.timeLeft, left - 1, `${label(f)}: the burst 1 s shorter`);
approx(lock.timeLeft, left - 1.2, `${label(f)}: the burst 1.2 s shorter`);
done(h);
}
});
test('T2 坚硬脚板: an enemy she has damaged whose 侵蚀 bursts gives her DEF +8 (PRP-X stage 3: DEF +12, ATK +5; ≤ 30 stacks) and 250 (400) 屏障 added to one barrier, ≤ 300 % max HP; unmarked enemies nothing; the marks go when she leaves', () => {
test('T2 坚硬脚板: an enemy she has damaged whose 侵蚀 bursts gives her DEF +11 (PRP-X stage 3: DEF +15, ATK +6; ≤ 30 stacks) and 330 (480) 屏障 added to one barrier, ≤ 300 % max HP; unmarked enemies nothing; the marks go when she leaves', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 0, row: 9 });
noSp(h, u);
const x3 = elite && mod === PRPX && tier === 6;
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual(t1, x3 ? { def: 12, atk: 5, shield_value: 400, max_stack_cnt: 30, scale: 3 } : { def: 8, atk: 0, shield_value: 250, max_stack_cnt: 30, scale: 3 }, label(f));
assert.deepEqual(t1, x3 ? { def: 15, atk: 6, shield_value: 480, max_stack_cnt: 30, scale: 3 } : { def: 11, atk: 0, shield_value: 330, max_stack_cnt: 30, scale: 3 }, label(f));
const marked = h.spawn('enemy_dummy', { pos: [10, 8] }), other = h.spawn('enemy_dummy', { pos: [11, 8] });
h.step();
h.b.dealDamage(null, other, { type: 'element', element: 'erosion', amount: 1000 });
assert.equal(u.findBuff('talent:thumpy:sole'), null, `${label(f)}: an unmarked enemy's burst`);
h.b.dealDamage(u, marked, { amount: 10, type: 'arts' });
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
assert.deepEqual(u.findBuff('talent:thumpy:sole')?.mods, x3 ? { defFlat: 12, atkFlat: 5 } : { defFlat: 8 }, `${label(f)}: the stack`);
assert.deepEqual(u.findBuff('talent:thumpy:sole')?.mods, x3 ? { defFlat: 15, atkFlat: 6 } : { defFlat: 11 }, `${label(f)}: the stack`);
assert.equal(u.findBuff('talent:thumpy:barrier')?.shield, t1.shield_value, `${label(f)}: 屏障`);
h.run(8.1);
h.b.dealDamage(null, marked, { type: 'element', element: 'erosion', amount: 1000 });
@@ -344,12 +345,12 @@ test('PRP-X 出发的勇气 trait: every 元素损伤 she deals ×1.15 while she
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
h.step();
h.b.dealDamage(u, far, { amount: 100, type: 'phys' });
approx(far.elem.erosion, 0.1 * u.s.atk, `${label(f)}: blocking nobody`);
approx(far.elem.erosion, 0.12 * u.s.atk, `${label(f)}: blocking nobody`);
const near = h.spawn('enemy_dummy', { pos: [10, 5] });
assert.ok(h.runUntil(() => near.blockedBy === u, 1), `${label(f)}: blocking`);
const g0 = far.elem.erosion;
h.b.dealDamage(u, far, { amount: 100, type: 'phys' });
approx(far.elem.erosion - g0, 0.1 * u.s.atk * (x ? 1.15 : 1), `${label(f)}: while blocking`);
approx(far.elem.erosion - g0, 0.12 * u.s.atk * (x ? 1.15 : 1), `${label(f)}: while blocking`);
done(h);
}
});
+9 -9
View File
@@ -69,9 +69,9 @@ test('提丰 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// E2 Lv1 1310 / 867 / 92, E2 Lv60 1570 / 985 / 106; SIE-X +120 HP / +60 ATK → +210 / +90, SIE-Y +70 ATK / +5 ASPD → +94 / +7,
// E2 Lv1 1310 / 901 / 92, E2 Lv60 1570 / 1019 / 106; SIE-X +120 HP / +60 ATK → +210 / +90, SIE-Y +70 ATK / +5 ASPD → +94 / +7,
// ISW-A +100 / +70 → +150 / +90
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], [1310, 867, 1570, 985]);
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], [1310, 901, 1570, 1019]);
assert.deepEqual([modOf(5, SIEX).attr, modOf(6, SIEX).attr, modOf(5, SIEY).attr, modOf(6, SIEY).attr, modOf(5, ISWA).attr, modOf(6, ISWA).attr],
[{ maxHp: 120, atk: 60 }, { maxHp: 210, atk: 90 }, { atk: 70, aspd: 5 }, { atk: 94, aspd: 7 }, { maxHp: 100, atk: 70 }, { maxHp: 150, atk: 90 }]);
});
@@ -192,9 +192,9 @@ test('S3 “永恒狩猎”: 7 / 8 rounds, attack interval 2.4 + 3.1 s; the mark
const rounds = Map.groupBy(rain, (c) => Math.round((c.t - t0) / 5.5));
assert.equal(rounds.size, sk.bb['attack@s3_trigger_time'], `T${tier}: one round per bullet`);
for (const list of rounds.values()) list.forEach((c, i) => i && approx(c.t - list[i - 1].t, 0.1, `T${tier}: 0.1 s apart`, 0.05));
// amounts: ATK × s3_atk_scale (DEF 0), the first hit on each enemy ×1.6 (重如沼泥)
// amounts: ATK × s3_atk_scale (DEF 0), the first hit on each enemy ×1.7 (重如沼泥)
const firsts = new Set([marked, near].map((e) => rain.find((c) => c.target === e)));
for (const c of rain) approx(c.amount, u.s.atk * sk.bb['attack@s3_atk_scale'] * (firsts.has(c) ? 1.6 : 1), `T${tier}: s3_atk_scale`);
for (const c of rain) approx(c.amount, u.s.atk * sk.bb['attack@s3_atk_scale'] * (firsts.has(c) ? 1.7 : 1), `T${tier}: s3_atk_scale`);
const stuns = h.hooksOf('statusApplied').filter((c) => c.source === u && c.status === 'stun');
assert.equal(stuns.length, rain.length, `T${tier}: a stun per landed arrow`);
for (const c of stuns) approx(c.duration, 0.4, `T${tier}: 0.4 s`);
@@ -247,12 +247,12 @@ test('T1 锐如兽牙: each attack adds 10 % (SIE-X stage 3: 12 %) DEF ignore up
}
});
test('T2 重如沼泥: while her skill runs, the first damage on each enemy deals ×1.6 and 停顿 3 s (SIE-Y stage 3: the first two, ×2.2); not outside a skill; a new cast clears the marks', () => {
test('T2 重如沼泥: while her skill runs, the first damage on each enemy deals ×1.7 and 停顿 3 s (SIE-Y stage 3: the first two, ×2.3); not outside a skill; a new cast clears the marks', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, null], [5, true, SIEY], [6, true, SIEY], [6, true, ISWA]]) {
const t1 = formOf(tier, elite).talents.find((t) => t.index === 1).bb;
const tc = mod === SIEY && tier === 6 ? modOf(tier, SIEY).talentChanges.find((t) => t.talentIndex === 1).bb : null;
const n = tc?.max_stack_cnt ?? 1, scale = tc?.atk_scale ?? t1.atk_scale;
assert.deepEqual([scale, tc?.sluggish ?? t1.sluggish], [tier === 6 && mod === SIEY ? 2.2 : 1.6, 3], label([tier, elite, mod]));
assert.deepEqual([scale, tc?.sluggish ?? t1.sluggish], [tier === 6 && mod === SIEY ? 2.3 : 1.7, 3], label([tier, elite, mod]));
const { h, u } = field({ tier, elite, mod, skill: 0 });
const a = h.spawn('enemy_light', { pos: [10, 5] });
const far = (d) => (mod === SIEY ? 1 + 0.12 * (d - 1) / 3.5 : 1);
@@ -296,14 +296,14 @@ test('SIE-X 自然的包容: ×1.15 on her attacks against 重量等级 ≥ 3, n
assert.ok(atkHits(h, u).some((c) => c.target === mid) && atkHits(h, u).some((c) => c.target.weight >= 3));
done(h);
}
// S3: rain hits on a weight-3 enemy are s3_atk_scale × ATK (× 1.6 the first)
// S3: rain hits on a weight-3 enemy are s3_atk_scale × ATK (× 1.7 the first)
const { h, u } = field({ tier: 6, elite: true, mod: SIEX, skill: 2 });
h.spawn('enemy_heavy', { pos: [10, 6] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.active, 4));
h.run(7);
const rain = rainHits(h, u);
rain.forEach((c, i) => approx(c.amount, u.s.atk * 1.6 * (i === 0 ? 1.6 : 1), `rain ${i + 1}: no ×1.15`));
rain.forEach((c, i) => approx(c.amount, u.s.atk * 1.6 * (i === 0 ? 1.7 : 1), `rain ${i + 1}: no ×1.15`));
done(h);
});
@@ -325,7 +325,7 @@ test('SIE-Y 冰原的影子: her attack damage rises with the distance, linearly
assert.ok(h.runUntil(() => u.skill.active, 4));
h.run(7);
const n = modOf(6, SIEY).talentChanges.find((t) => t.talentIndex === 1).bb.max_stack_cnt;
rainHits(h, u).forEach((c, i) => approx(c.amount, u.s.atk * 1.6 * (i < n ? 2.2 : 1), `rain ${i + 1}: no distance bonus`));
rainHits(h, u).forEach((c, i) => approx(c.amount, u.s.atk * 1.6 * (i < n ? 2.3 : 1), `rain ${i + 1}: no distance bonus`));
done(h);
});
+10 -10
View File
@@ -63,7 +63,7 @@ test('维伊 in every 自选 form: her operator kit (all three skills authored),
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], [VEEN, SLOT[tier], form.skills[skill].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], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat, u.base.cost], [1, 'ranged', 'arts', true, 3, 27], `${label(f)}: 秘术师`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat, u.base.cost], [1, 'ranged', 'arts', true, 3, 24], `${label(f)}: 秘术师`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-14`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target her`);
@@ -82,11 +82,11 @@ test('a 自选 pick: 维伊 is offered at tiers 5 and 6 (she has a kit) and a ro
{ ok: true, picks: { [SLOT[6]]: { charId: VEEN, skillIndex: 1, uniEquipId: MSCY } } });
});
test('trait + T1: with no target her attack check stores one energy (an attack action: the interval restarts — PRTS 分支特性信息), 3 merge into a 转置能量, at most 3 of those (then she stops); ATK +10 % while she holds one, ASPD +15 while she holds none; MSC-Y: ASPD +30 while she holds any energy', () => {
test('trait + T1: with no target her attack check stores one energy (an attack action: the interval restarts — PRTS 分支特性信息), 3 merge into a 转置能量, at most 3 of those (then she stops); ATK +12 % while she holds one, ASPD +15 while she holds none; MSC-Y: ASPD +30 while she holds any energy', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const t0 = talentOf(tier, elite, mod, 0);
assert.deepEqual(t0, { atk: 0.1, attack_speed: 15 }, label(f));
assert.deepEqual(t0, { atk: 0.12, attack_speed: 15 }, label(f));
const msc = elite && mod === MSCY ? 30 : 0;
const { h, u } = field({ tier, elite, mod, skill: 0 });
u.skill.sp = 0;
@@ -101,7 +101,7 @@ test('trait + T1: with no target her attack check stores one energy (an attack a
approx(h.b.time - t2, 3 * 100 / (115 + msc), `${label(f)}: then at the interval of ASPD ${115 + msc}`, 0.03);
storeUp(h, u, 1, 0);
approx(u.s.aspd, 100 + msc, `${label(f)}: a 转置能量: the ASPD +15 goes`);
approx(u.s.atk, u.base.atk * 1.1, `${label(f)}: ATK +10 %`);
approx(u.s.atk, u.base.atk * 1.12, `${label(f)}: ATK +12 %`);
storeUp(h, u, 3, 0);
h.run(30);
assert.deepEqual(stored(u), { s: 0, t: 3 }, `${label(f)}: no more storing at 3 转置能量`);
@@ -109,13 +109,13 @@ test('trait + T1: with no target her attack check stores one energy (an attack a
}
});
test('the volley: 转置能量 first (300 % ATK each), the main attack (100 %), then the stored energies (100 %) — one target, all landing together, each with the ATK it left with (+10 % while a 转置能量 was held)', () => {
test('the volley: 转置能量 first (300 % ATK each), the main attack (100 %), then the stored energies (100 %) — one target, all landing together, each with the ATK it left with (+12 % while a 转置能量 was held)', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const { h, u } = field({ tier, elite, skill: 1 });
u.skill.sp = 0;
storeUp(h, u, 1, 2);
const atk = u.s.atk;
approx(atk, u.base.atk * 1.1, `T${tier}: holding a 转置能量`);
approx(atk, u.base.atk * 1.12, `T${tier}: holding a 转置能量`);
const e = h.spawn('enemy_fly', { pos: [10, 7] });
const a0 = atkHits(h, u).length;
h.runUntil(() => atkHits(h, u).length > a0, 4);
@@ -211,10 +211,10 @@ test('S2 “以鲜血洗去” (MANUAL, data DEFAULT): 28 s, attack interval 3.0
const hits = atkHits(h, u);
h.runUntil(() => atkHits(h, u).length >= 3, 4);
const v = atkHits(h, u).slice(0, 3);
// 转置能量 (0 stacks, +10 %), main (3 stacks), stored (4 stacks) — every one +10 % (a 转置能量 was held)
approx(v[0].amount, base * (1 + 0.1) * 3, `T${tier}: 转置能量 before any stack`);
approx(v[1].amount, base * (1 + 0.1 + 3 * per), `T${tier}: the main shot after 3`);
approx(v[2].amount, base * (1 + 0.1 + 4 * per), `T${tier}: the stored one after 4`);
// 转置能量 (0 stacks, +12 %), main (3 stacks), stored (4 stacks) — every one +12 % (a 转置能量 was held)
approx(v[0].amount, base * (1 + 0.12) * 3, `T${tier}: 转置能量 before any stack`);
approx(v[1].amount, base * (1 + 0.12 + 3 * per), `T${tier}: the main shot after 3`);
approx(v[2].amount, base * (1 + 0.12 + 4 * per), `T${tier}: the stored one after 4`);
assert.ok(hits.length <= 3);
approx(u.s.aspd, 100 + 15 + 5 * perAs, `T${tier}: 5 stacks of ASPD (and T1's +15 again)`);
h.run(25);
+19 -19
View File
@@ -75,8 +75,8 @@ test('薇薇安娜 in every 自选 form: her operator kit (all three skills auth
done(h);
}
}
// zh_CN: E2 Lv1 2219 / 523 / 371, E2 Lv60 2684 / 605 / 421; AFT-D +240 / +30 → +420 / +60 HP / ATK, AFT-Y +240 / +30 / +30 → +300 / +45 / +45
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], [2219, 523, 2684, 605]);
// zh_CN, full potential: E2 Lv1 2219 / 551 / 371, E2 Lv60 2684 / 633 / 421; AFT-D +240 / +30 → +420 / +60 HP / ATK, AFT-Y +240 / +30 / +30 → +300 / +45 / +45
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], [2219, 551, 2684, 633]);
assert.deepEqual([modOf(5, AFTD).attr, modOf(6, AFTD).attr, modOf(5, AFTY).attr, modOf(6, AFTY).attr], [{ maxHp: 240, atk: 30 }, { maxHp: 420, atk: 60 }, { maxHp: 240, atk: 30, def: 30 }, { maxHp: 300, atk: 45, def: 45 }]);
});
@@ -103,7 +103,7 @@ test('S1 光影迅捷剑 (AUTO, DEFAULT, 2 charges): with one charge the next at
assert.equal(skillAtk[0].length, 2, `T${tier}: 连续攻击两次`);
for (const c of skillAtk[0]) {
assert.deepEqual([c.target, c.type], [e, 'arts'], `T${tier}: the target, arts`);
approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.08, `T${tier}: ${sk.bb.atk_scale * 100} % ATK (× 燃烛施明 1.08)`);
approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.09, `T${tier}: ${sk.bb.atk_scale * 100} % ATK (× 燃烛施明 1.09)`);
}
assert.ok(u.skill.charges < 1 && !u.mem.vvanaCharged, `T${tier}: one charge spent, no 蓄力`);
assert.deepEqual(u.liveRangeGrid, [[0, 0], [0, 1]], `T${tier}: 1-1`);
@@ -131,7 +131,7 @@ test('S1 蓄力: both charges stored ⇒ the 3-2 range (card and trigger), the c
const skillAtk = byAttack(atkHits(h, u).filter((c) => c.dmg.isSkill));
assert.equal(skillAtk.length, 1);
assert.equal(skillAtk[0].length, 3, `T${tier}: 改为连续攻击三次`);
for (const c of skillAtk[0]) { assert.equal(c.target, e); approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.08, `T${tier}: ${sk.bb.atk_scale * 100} %`); }
for (const c of skillAtk[0]) { assert.equal(c.target, e); approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.09, `T${tier}: ${sk.bb.atk_scale * 100} %`); }
assert.ok(u.skill.charges === 0 && u.skill.sp < 0.2, `T${tier}: 任何时候开启均消耗全部技力 (${u.skill.charges} / ${u.skill.sp})`);
assert.deepEqual(u.liveRangeGrid, [[0, 0], [0, 1]], `T${tier}: 蓄力 over with the skill`);
const n = atkHits(h, u).length;
@@ -186,8 +186,8 @@ test('S2 烛燃影息 (MANUAL, DEFAULT): 28 / 30 s, ATK +20 / 30 %, DEF +80 / 11
assert.equal(twice.length + once.length, attacks.length, `T${tier}: one or two hits per target`);
const rate = twice.length / attacks.length;
assert.ok(rate > 0.13 && rate < 0.27, `T${tier}: double hits ${twice.length} / ${attacks.length} ≈ 20 %`);
for (const c of once[0]) approx(c.amount, u.s.atk * 1.08, `T${tier}: a plain hit 100 %`, 1e-3);
for (const c of twice[0]) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale_twice'] * 1.08, `T${tier}: a double hit ${sk.bb['attack@atk_scale_twice'] * 100} %`, 1e-3);
for (const c of once[0]) approx(c.amount, u.s.atk * 1.09, `T${tier}: a plain hit 100 %`, 1e-3);
for (const c of twice[0]) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale_twice'] * 1.09, `T${tier}: a double hit ${sk.bb['attack@atk_scale_twice'] * 100} %`, 1e-3);
// 偷取: capped on both sides
const steal = sk.bb['attack@steal_atk_speed'];
assert.equal(u.findBuff('skill:vvana:loot')?.mods.aspd, steal, `T${tier}: her +${steal} ASPD (max ${steal})`);
@@ -251,23 +251,23 @@ test('S3 “明灭” (MANUAL, DEFAULT): 15 s, interval 1.25 + 0.5 s, ATK / DEF
}
});
test('T1 燃烛施明: arts dealt ×1.08, physical / arts taken ×0.92 — ×1.16 / ×0.84 while an elite / leader enemy is in her range; AFT-D stage 3: 9 % (18 %) of her attack damage as 灼燃损伤', () => {
test('T1 燃烛施明: arts dealt ×1.09, physical / arts taken ×0.91 — ×1.18 / ×0.82 while an elite / leader enemy is in her range; AFT-D stage 3: 9 % (18 %) of her attack damage as 灼燃损伤', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 2 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
assert.deepEqual([t0.damage_scale_m, t0.damage_resistance_pm, t0.super_scale], [0.08, 0.08, 2], label(f));
assert.deepEqual(u.findBuff(CANDLE)?.mods, { artsDealtMul: 1.08, physTakenMul: 0.92, artsTakenMul: 0.92 }, `${label(f)}: no elite`);
assert.deepEqual([t0.damage_scale_m, t0.damage_resistance_pm, t0.super_scale], [0.09, 0.09, 2], label(f));
assert.deepEqual(u.findBuff(CANDLE)?.mods, { artsDealtMul: 1.09, physTakenMul: 0.91, artsTakenMul: 0.91 }, `${label(f)}: no elite`);
const taken = (type) => { const hp = u.hp; h.b.dealDamage(null, u, { amount: 1000, type }); const d = hp - u.hp; u.hp = u.s.maxHp; return d; };
approx(taken('arts'), 1000 * (1 - u.s.res / 100) * 0.92, `${label(f)}: arts taken −8 %`);
approx(taken('arts'), 1000 * (1 - u.s.res / 100) * 0.91, `${label(f)}: arts taken −9 %`);
approx(taken('true'), 1000, `${label(f)}: true damage untouched`);
const el = h.spawn('enemy_elite', { pos: [10, 6] });
h.step();
approx(taken('arts'), 1000 * (1 - u.s.res / 100) * 0.84, `${label(f)}: −16 % with the elite in her range`);
approx(taken('arts'), 1000 * (1 - u.s.res / 100) * 0.82, `${label(f)}: −18 % with the elite in her range`);
const m = u.findBuff(CANDLE).mods;
approx(m.artsDealtMul, 1.16, `${label(f)}: elite in range — arts dealt`);
approx(m.physTakenMul, 0.84, `${label(f)}: phys taken`);
approx(m.artsTakenMul, 0.84, `${label(f)}: arts taken`);
approx(m.artsDealtMul, 1.18, `${label(f)}: elite in range — arts dealt`);
approx(m.physTakenMul, 0.82, `${label(f)}: phys taken`);
approx(m.artsTakenMul, 0.82, `${label(f)}: arts taken`);
const ep = elite && tier === 6 && mod === AFTD ? 0.09 : 0;
assert.equal(t0.ep_damage_ratio_m ?? 0, ep, `${label(f)}: the AFT-D stage-3 灼燃损伤`);
h.run(1.3);
@@ -276,7 +276,7 @@ test('T1 燃烛施明: arts dealt ×1.08, physical / arts taken ×0.92 — ×1.1
approx(el.elem.burn, ep * 2 * atkHits(h, u).filter((c) => c.target === el).reduce((s, c) => s + c.amount, 0), `${label(f)}: 灼燃损伤 ×2 next to an elite`);
h.b.kill(el, null);
h.step();
approx(u.findBuff(CANDLE).mods.artsDealtMul, 1.08, `${label(f)}: back without the elite`);
approx(u.findBuff(CANDLE).mods.artsDealtMul, 1.09, `${label(f)}: back without the elite`);
if (ep) {
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.run(1.3);
@@ -286,7 +286,7 @@ test('T1 燃烛施明: arts dealt ×1.08, physical / arts taken ×0.92 — ×1.1
}
});
test('T2 散华: hits on elite / leader enemies give a 护盾 layer at 18 % (AFT-Y stage 3: 25 %, 2 layers; × talent_scale while S3 runs) — only on elites, never above the cap', () => {
test('T2 散华: hits on elite / leader enemies give a 护盾 layer at 20 % (AFT-Y stage 3: 27 %, 2 layers; × talent_scale while S3 runs) — only on elites, never above the cap', () => {
const rateOf = (tier, elite, mod, s3) => {
const { h, u } = field({ tier, elite, mod, skill: s3 ? 2 : 1, seed: 21 });
if (!s3) h.b.on('spGain', (c) => { if (c.unit === u) c.amount = 0; });
@@ -305,11 +305,11 @@ test('T2 散华: hits on elite / leader enemies give a 护盾 layer at 18 % (AFT
done(h);
return gains / rolls;
};
for (const [tier, elite, mod, p] of [[5, false, null, 0.18], [6, true, AFTY, 0.25], [5, true, AFTY, 0.18]]) {
for (const [tier, elite, mod, p] of [[5, false, null, 0.2], [6, true, AFTY, 0.27], [5, true, AFTY, 0.2]]) {
const r = rateOf(tier, elite, mod, false);
assert.ok(Math.abs(r - p) < 0.06, `T${tier} ${mod ?? 'none'}: ${r.toFixed(3)} ≈ ${p}`);
}
for (const [tier, elite, mod, p] of [[5, false, null, 0.18 * 1.8], [6, true, AFTY, 0.25 * 2]]) {
for (const [tier, elite, mod, p] of [[5, false, null, 0.2 * 1.8], [6, true, AFTY, 0.27 * 2]]) {
const r = rateOf(tier, elite, mod, true);
assert.ok(Math.abs(r - p) < 0.08, `T${tier} ${mod ?? 'none'} under S3: ${r.toFixed(3)} ≈ ${p}`);
}
@@ -412,7 +412,7 @@ test('AFT-Y “最后一行” trait: the enemies she blocks take 10 % 法术脆
const n0 = atkHits(h, u).length;
h.run(2);
const c = atkHits(h, u).slice(n0).find((x) => x.target === w);
approx(c.amount, u.s.atk * 1.08 * 1.1, `${label(f)}: ×1.1 arts`);
approx(c.amount, u.s.atk * 1.09 * 1.1, `${label(f)}: ×1.1 arts`);
}
done(h);
}
+27 -27
View File
@@ -90,7 +90,7 @@ test('望 in every 自选 form: her kit, stats + module attributes, 3-3, blocks
assert.deepEqual([s.base.cost, s.base.respawnTime, s.def.skill.id, !!s.s.flags.untargetable, s.s.blockCnt, !!s.profile.noAttack],
[v.stats.cost, v.stats.respawnTime, v.skill.skillId, true, 0, true], `${label(f)}: 棋子`);
assert.equal(s.base.cost, elite && mod === X ? 2 : 3, `${label(f)}: TRP-X 部署费用更低(-1)`);
assert.deepEqual([v.stats.deployLimit], [elite && mod === X ? 7 : 6], `${label(f)}: TRP-X 可同时部署的陷阱数量提升(+1)`);
assert.deepEqual([v.stats.deployLimit], [elite && mod === X ? 8 : 7], `${label(f)}: TRP-X 可同时部署的陷阱数量提升(+1)`);
done(h);
}
}
@@ -189,12 +189,12 @@ test('铸子: a spent 棋子 comes back on its tile 2 s later for its cost (TRP-
}
});
test('料敌机先: a line of 2 pieces ⇒ 2 stacks (+20 % damage, 18 RES ignored), a line of 3 or more ⇒ 3 (the cap); TRP-X stage 3: 13 % / 12 per stack', () => {
test('料敌机先: a line of 2 pieces ⇒ 2 stacks (+24 % damage, 20 RES ignored), a line of 3 or more ⇒ 3 (the cap); TRP-X stage 3: 15 % / 13 per stack', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const tb = talentOf(tier, elite, mod, 1).bb;
assert.deepEqual([tb['attack@per_atk_scale'], tb['attack@per_magic_resist_penetrate_fixed'], tb['attack@max_trigger_cnt']],
elite && tier === 6 && mod === X ? [0.13, 12, 3] : [0.1, 9, 3], label(f));
elite && tier === 6 && mod === X ? [0.15, 13, 3] : [0.12, 10, 3], label(f));
const sk = skillOf(tier, elite, S2);
for (const [line, stacks] of [[[[10, 5], [10, 6]], 2], [[[10, 4], [10, 5], [10, 6], [10, 7]], 3]]) {
const { h, u, st } = field({ tier, elite, mod, skill: 1, stones: line });
@@ -211,13 +211,13 @@ test('料敌机先: a line of 2 pieces ⇒ 2 stacks (+20 % damage, 18 RES ignore
}
});
test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes 105 % / 120 % of 望\'s ATK as 法术 every second for 6.5 s (6 ticks, 无来源 credited to her, independent); the active +2 棋子 (≤ 7) and 阻回 at the cap', () => {
test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes 105 % / 120 % of 望\'s ATK as 法术 every second for 6.5 s (6 ticks, 无来源 credited to her, independent); the active +2 棋子 (≤ 8) and 阻回 at the cap', () => {
for (const [tier, elite] of [[5, false], [6, true]]) {
const sk = skillOf(tier, elite, S1);
assert.deepEqual([sk.spCost, sk.bb.cnt, sk.bb['attack@atk_scale'], sk.bb['attack@sluggish']], elite ? [20, 2, 1.2, 6.5] : [21, 2, 1.05, 6.5], `T${tier}`);
const { h, u, st } = field({ tier, elite, skill: 0, stones: [[10, 6], [10, 7]] });
// her stock: 6 less the 2 placed 棋子 (the owner's decision of 2026-10-06 — see the stock test below)
assert.deepEqual([u.skill.kind, u.skill.rule, u.mem.wang.stock], ['instant', 'SP_FULL', 4]);
// her stock: 7 less the 2 placed 棋子 (the owner's decision of 2026-10-06 — see the stock test below)
assert.deepEqual([u.skill.kind, u.skill.rule, u.mem.wang.stock], ['instant', 'SP_FULL', 5]);
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
h.step();
const slug = h.hooksOf('statusApplied').find((c) => c.target === e && c.status === 'sluggish');
@@ -228,7 +228,7 @@ test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes
assert.equal(ticks.length, 6, `T${tier}: one per second`);
for (const c of ticks) {
assert.deepEqual([c.source, c.credit, c.type], [null, u, 'arts'], `T${tier}: 无来源 arts credited to her`);
approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'] * (1 + 0.1 * 2), `T${tier}: precomputed with 2 stacks`, 1e-4);
approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'] * (1 + 0.12 * 2), `T${tier}: precomputed with 2 stacks`, 1e-4);
}
// independent: two triggers ⇒ two DoTs ticking together
h.b.kill(e, null);
@@ -239,10 +239,10 @@ test('S1 取势 (AUTO, SP_FULL): the triggering enemy is 停顿 6.5 s and takes
h.step();
h.step();
assert.equal(e2.buffs.filter((b) => b.key === 'wang:qushi').length, 2, `T${tier}: independent`);
// the active part: +2 stock (6 → 7: the cap — both pieces are off the field, their cards back in her hand), then 阻回
// the active part: +2 stock (7 → 8: the cap — both pieces are off the field, their cards back in her hand), then 阻回
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 1), `T${tier}: cast at full SP`);
assert.equal(u.mem.wang.stock, 7, `T${tier}: 最多拥有7枚`);
assert.equal(u.mem.wang.stock, 8, `T${tier}: 最多拥有8枚`);
h.step();
assert.ok(u.s.flags.noSp, `T${tier}: 阻回 at the cap`);
done(h);
@@ -254,24 +254,24 @@ test('her stock (the owner\'s decision of 2026-10-06): the placed 棋子 occupy
const S = `S${skill + 1}`;
const { h, u, st } = field({ tier: 6, elite: true, skill, stones: [[10, 6], [10, 7]] });
const w = u.mem.wang;
assert.equal(w.stock, 6 - 2, `${S}: 6 less the 2 placed`);
assert.equal(w.stock, 7 - 2, `${S}: 7 less the 2 placed`);
const a = at(st, 10, 6), b = at(st, 10, 7);
let e = h.spawn('enemy_dummy', { pos: [10, 7] });
h.step();
assert.ok(!b.alive, `${S}: set off`);
assert.equal(w.stock, 5, `${S}: its card is back in her stock at once`);
assert.equal(w.stock, 6, `${S}: its card is back in her stock at once`);
h.b.kill(e, null);
assert.ok(h.runUntil(() => b.alive, 5), `${S}: back on its tile`);
assert.equal(w.stock, 4, `${S}: its return takes one`);
// two casts: 4 → 6 → 7 (8, over the cap); 阻回 at the cap only
assert.equal(w.stock, 5, `${S}: its return takes one`);
// two casts: 5 → 7 → 8 (9, over the cap); 阻回 at the cap only
u.skill.gainSp(999);
h.step();
assert.deepEqual([u.skill.activations, w.stock], [1, 6], `${S}: 立即获得两枚棋子`);
assert.deepEqual([u.skill.activations, w.stock], [1, 7], `${S}: 立即获得两枚棋子`);
h.step();
assert.ok(!u.s.flags.noSp, `${S}: below the cap, no 阻回`);
u.skill.gainSp(999);
h.step();
assert.deepEqual([u.skill.activations, w.stock], [2, 7], `${S}: 最多拥有7枚`);
assert.deepEqual([u.skill.activations, w.stock], [2, 8], `${S}: 最多拥有8枚`);
h.step();
assert.ok(u.s.flags.noSp, `${S}: 阻回 at the cap`);
const sp0 = u.skill.sp;
@@ -281,10 +281,10 @@ test('her stock (the owner\'s decision of 2026-10-06): the placed 棋子 occupy
e = h.spawn('enemy_dummy', { pos: [10, 6] });
h.step();
assert.ok(!a.alive, `${S}: set off at the cap`);
assert.equal(w.stock, 7, `${S}: still the cap`);
assert.equal(w.stock, 8, `${S}: still the cap`);
h.b.kill(e, null);
assert.ok(h.runUntil(() => a.alive, 5), `${S}: back`);
assert.equal(w.stock, 6, `${S}: its return took one`);
assert.equal(w.stock, 7, `${S}: its return took one`);
h.step();
assert.ok(!u.s.flags.noSp, `${S}: 阻回 lifted`);
const sp1 = u.skill.sp;
@@ -292,10 +292,10 @@ test('her stock (the owner\'s decision of 2026-10-06): the placed 棋子 occupy
assert.ok(u.skill.sp > sp1, `${S}: her SP recovers again`);
done(h);
}
// TRP-X: 7 placed 棋子 (its deploy limit) on a stock of 6 ⇒ 0, never below
const { h, u, st } = field({ tier: 6, elite: true, mod: X, skill: 0, stones: [[10, 3], [10, 4], [10, 5], [10, 6], [10, 7], [10, 8], [10, 9]] });
assert.ok(st.length === 7 && st.every((s) => s.alive), 'TRP-X: all 7 deploy');
assert.equal(u.mem.wang.stock, 0, 'TRP-X: 7 placed on a stock of 6');
// TRP-X: 8 placed 棋子 (its deploy limit) on a stock of 7 ⇒ 0, never below
const { h, u, st } = field({ tier: 6, elite: true, mod: X, skill: 0, stones: [[10, 3], [10, 4], [10, 5], [10, 6], [10, 7], [10, 8], [10, 9], [11, 5]] });
assert.ok(st.length === 8 && st.every((s) => s.alive), 'TRP-X: all 8 deploy');
assert.equal(u.mem.wang.stock, 0, 'TRP-X: 8 placed on a stock of 7');
done(h);
});
@@ -314,7 +314,7 @@ test('S2 连星 (AUTO, SP_FULL): a triggered piece hits every enemy on its conne
const victims = new Set(hit.map((c) => c.target));
assert.ok(victims.has(trig) && victims.has(onLine), `T${tier}: the line, air included`);
assert.ok(!victims.has(beyond) && !victims.has(off), `T${tier}: not beyond ± 3 / off the line`);
for (const c of hit.filter((x) => x.target === trig)) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'] * 1.2, `T${tier}: 2 stacks`, 1e-4);
for (const c of hit.filter((x) => x.target === trig)) approx(c.amount, u.s.atk * sk.bb['attack@atk_scale'] * 1.24, `T${tier}: 2 stacks`, 1e-4);
const slow = trig.findBuff('wang:slow');
approx(trig.s.moveSpeed, trig.base.moveSpeed * (1 + sk.bb['attack@move_speed']), `T${tier}: slowed`);
// a second slow 3 s later adds up and keeps its own timer
@@ -345,7 +345,7 @@ test('S3 天下劫 (MANUAL, ACTIVE_RANGE 4-12, 20 bullets): passive — the piec
const hit = stoneHits(h);
assert.ok(hit.some((c) => c.target === e) && hit.some((c) => c.target === fl), `T${tier}: x-6, air too`);
assert.ok(!hit.some((c) => c.target === diag && c.source === at(st, 10, 7)), `T${tier}: (10,7)'s area`);
for (const c of hit.filter((x) => x.target === e)) approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.2, `T${tier}: ${sk.bb.atk_scale * 100} %, 2 stacks`, 1e-4);
for (const c of hit.filter((x) => x.target === e)) approx(c.amount, u.s.atk * sk.bb.atk_scale * 1.24, `T${tier}: ${sk.bb.atk_scale * 100} %, 2 stacks`, 1e-4);
done(h);
}
{ // active
@@ -357,9 +357,9 @@ test('S3 天下劫 (MANUAL, ACTIVE_RANGE 4-12, 20 bullets): passive — the piec
assert.ok(h.runUntil(() => u.skill.active, 1), `T${tier}: an enemy inside 4-12 casts it`);
assert.deepEqual(u.liveRangeGrid, sk.rangeGrid, `T${tier}: 4-12`);
assert.equal(u.skill.attackOverride()?.noAttack, true, `T${tier}: 停止攻击`);
assert.equal(u.mem.wang.stock, 7, `T${tier}: +8, capped`);
assert.equal(u.mem.wang.stock, 8, `T${tier}: +8, capped`);
const fl = placed(h).map(([r, c]) => `${r},${c}`);
assert.equal(fl.length, 5, `T${tier}: (6 − 2 placed) + 8 − 7 over the cap`);
assert.equal(fl.length, 5, `T${tier}: (7 − 2 placed) + 8 − 8 over the cap`);
assert.deepEqual(fl.slice(0, 2).sort(), ['10,7', '9,5'], `T${tier}: the enemies' tiles first (${fl})`);
assert.ok(h.hooksOf('damaged').some((c) => c.target === a && c.source === u && c.dmg.tags.includes('wang:stone')), `T${tier}: they set off`);
// inside her range a 棋子 comes back onto an enemy ("可部署至敌人所在位置") — and sets off again
@@ -390,10 +390,10 @@ test('S3 天下劫 (MANUAL, ACTIVE_RANGE 4-12, 20 bullets): passive — the piec
}
});
test('the hand: deploying her in prep brings 6 棋子 (TRP-X "可同时部署的陷阱数量提升": 7) — the match\'s data view reads the module variant\'s deploy limit', () => {
test('the hand: deploying her in prep brings 7 棋子 (TRP-X "可同时部署的陷阱数量提升": 8) — the match\'s data view reads the module variant\'s deploy limit', () => {
const gd = new GameData(getData({ log: { warn() {}, error() {}, info() {} } }), 'mode_multi_hard');
const data = { chess: CHESS, backups: BACKUPS };
for (const [tier, elite, mod, n] of [[5, false, null, 6], [6, false, null, 6], [5, true, null, 6], [6, true, null, 6], [5, true, X, 7], [6, true, X, 7]]) {
for (const [tier, elite, mod, n] of [[5, false, null, 7], [6, false, null, 7], [5, true, null, 7], [6, true, null, 7], [5, true, X, 8], [6, true, X, 8]]) {
const slot = elite ? SLOT[tier].replace(/_a$/, '_b') : SLOT[tier];
const rec = diyRecord(SLOT[tier], { charId: WANG, skillIndex: 1, uniEquipId: mod }, { elite, data });
const view = diyGameData(gd, new Map([[slot, rec]]));
+2 -1
View File
@@ -127,7 +127,8 @@ test('S2 水炮模式 (AUTO, SP_FULL): ATK +140 % / +170 %, interval ×3.2, rang
h.step();
assert.ok(u.skill.active, `T${tier}: on at full SP, no enemy needed`);
approx(u.s.atk, u.base.atk * (1 + sk.bb.atk), `T${tier}: ATK`);
approx(u.s.interval, 1.2 * (1 + sk.bb.base_attack_time), `T${tier}: interval`);
// full potential: her ASPD is 108 (潜能「攻击速度+8」), so the interval ×100 / 108
approx(u.s.interval, 1.2 * (1 + sk.bb.base_attack_time) * 100 / 108, `T${tier}: interval`);
assert.deepEqual(u.liveRangeGrid, [[0, 0], [0, 1], [0, 2]], `T${tier}: +1 tile`);
const main = h.spawn('enemy_dummy', { pos: [10, 7] });
const side = h.spawn('enemy_fly', { pos: [10.6, 7] });
+3 -3
View File
@@ -73,7 +73,7 @@ test('维什戴尔 in every 自选 form: her kit (all three skills authored), th
}
}
assert.deepEqual(formOf(6, true).skills.map((s) => [s.skillType, s.trigger.rule, s.trigger.rawRule]), [['AUTO', 'DEFAULT', 'DEFAULT'], ['MANUAL', 'DEFAULT', 'DEFAULT'], ['MANUAL', 'SP_FULL', 'ALWAYS']]);
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], [1434, 583, 1735, 652]);
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], [1434, 615, 1735, 684]);
assert.deepEqual([modOf(5, BOM).attr, modOf(6, BOM).attr], [{ atk: 45, aspd: 5 }, { atk: 65, aspd: 7 }]);
const data = { chess: CHESS, backups: BACKUPS };
assert.ok(KITTED_CHARS.includes(WD) && [5, 6].every((t) => diyPool(t, { data, kitted: KITTED_CHARS }).includes(WD)));
@@ -85,7 +85,7 @@ test('trait 投掷手 + T1 好礼: the main target ×1.15 (BOM-X stage 3 ×1.25)
const [tier, elite, mod] = f;
const t0 = t0Of(tier, elite, mod);
const shocks = elite && mod ? 2 : 1;
assert.deepEqual([t0['attack@main_atk_scale'], t0['attack@bomb_atk_scale']], elite && mod === BOM && tier === 6 ? [1.25, 1.75] : [1.15, 1.5], label(f));
assert.deepEqual([t0['attack@main_atk_scale'], t0['attack@bomb_atk_scale']], elite && mod === BOM && tier === 6 ? [1.25, 1.85] : [1.15, 1.6], label(f));
assert.equal(formOf(tier, elite).trait.bb['attack@append_atk_scale'], 0.5, label(f));
if (elite && mod) assert.equal(modOf(tier, mod).traitOverride.bb['attack@enable_third_attack'], 1, label(f));
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 21 });
@@ -113,7 +113,7 @@ test('trait 投掷手 + T1 好礼: the main target ×1.15 (BOM-X stage 3 ×1.25)
}
});
test('T1 好礼: each aftershock on a marked enemy goes off 15 % of the time — 150 % (175 %) ATK physical to every enemy within 1.1, air too, stun 1 s, the mark spent; the 残影 leave with her', () => {
test('T1 好礼: each aftershock on a marked enemy goes off 15 % of the time — 160 % (185 %) ATK physical to every enemy within 1.1, air too, stun 1 s, the mark spent; the 残影 leave with her', () => {
for (const f of [[5, false, null], [6, true, BOM]]) {
const [tier, elite, mod] = f;
const t0 = t0Of(tier, elite, mod);
+4 -4
View File
@@ -76,7 +76,7 @@ test('森蚺 in every 自选 form: her operator kit (all three skills authored),
done(h);
}
}
// zh_CN numbers: E2 Lv1 3440 / 835 / 492, E2 Lv60 4121 / 949 / 574; HES-X +80/+70 → +105/+85 ATK/DEF, HES-Y +70/+80 →
// zh_CN numbers (full potential): E2 Lv1 3440 / 835 / 519, E2 Lv60 4121 / 949 / 601; HES-X +80/+70 → +105/+85 ATK/DEF, HES-Y +70/+80 →
// +85/+110, RA-A +280/+80 → +380/+130 HP/ATK
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], [3440, 835, 4121, 949]);
assert.deepEqual([modOf(5, HESX).attr, modOf(6, HESX).attr, modOf(5, HESY).attr, modOf(6, HESY).attr, modOf(5, RAA).attr, modOf(6, RAA).attr],
@@ -184,13 +184,13 @@ test('S3 钢铁意志 (data DEFAULT): 31 / 32 s, ATK +140 % / +170 %, DEF +90 %
}
});
test('T1 勇冠三军: above half HP every hit ×1.15 as an 攻击力倍率 (×1.23 with HES-Y stage 3), at or below half 20 % 庇护 (28 %) on physical / arts damage only — checked every 0.1 s', () => {
test('T1 勇冠三军: above half HP every hit ×1.17 as an 攻击力倍率 (×1.25 with HES-Y stage 3), at or below half 22 % 庇护 (30 %) on physical / arts damage only — checked every 0.1 s', () => {
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
const { h, u } = field({ tier, elite, mod, skill: 1 });
const t0 = u.def.raw.talents.find((t) => t.index === 0).bb;
const y3 = elite && mod === HESY && tier === 6;
assert.deepEqual([t0.hp_ratio, t0.atk_scale, t0.damage_resistance], y3 ? [0.5, 1.23, 0.28] : [0.5, 1.15, 0.2], label(f));
assert.deepEqual([t0.hp_ratio, t0.atk_scale, t0.damage_resistance], y3 ? [0.5, 1.25, 0.3] : [0.5, 1.17, 0.22], label(f));
const e = h.spawn('enemy_dummy', { pos: [u.tileR, u.tileC + 1] }); // in her 1-1, not blocked: no SP, no cast
h.run(0.2);
assert.equal(u.s.atkScaleMul, t0.atk_scale, `${label(f)}: above half`);
@@ -198,7 +198,7 @@ test('T1 勇冠三军: above half HP every hit ×1.15 as an 攻击力倍率 (×1
assert.ok(h.runUntil(() => atkHits(h, u).length > n0, 3));
const hit = atkHits(h, u)[n0];
approx(hit.amount, u.s.atk * t0.atk_scale, `${label(f)}: ×${t0.atk_scale} on a 0-DEF target`);
// half HP: 庇护, no ×1.15
// half HP: 庇护, no ×1.17
u.hp = u.s.maxHp * 0.5;
h.run(0.14);
assert.equal(u.s.atkScaleMul, 1, `${label(f)}: at half: no bonus`);
+5 -5
View File
@@ -69,9 +69,9 @@ test('左乐 in every 自选 form: his operator kit (all three skills authored),
done(h);
}
}
// the numbers of the forms (zh_CN): E2 Lv1 2893 / 647 / 267, E2 Lv60 3533 / 735 / 299; SBL-X +55 / +40 → +90 / +60
// the numbers of the forms (zh_CN): E2 Lv1 2893 / 673 / 267, E2 Lv60 3533 / 761 / 299; SBL-X +55 / +40 → +90 / +60
// ATK / DEF, SBL-Y +300 / +60 → +380 / +84 HP / ATK
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], [2893, 647, 3533, 735]);
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], [2893, 673, 3533, 761]);
assert.deepEqual([modOf(5, SBLX).attr, modOf(6, SBLX).attr, modOf(5, SBLY).attr, modOf(6, SBLY).attr], [{ atk: 55, def: 40 }, { atk: 90, def: 60 }, { maxHp: 300, atk: 60 }, { maxHp: 380, atk: 84 }]);
});
@@ -261,16 +261,16 @@ test('T1 秉烛照影 (坚忍): ASPD up to +50 and SP recovery up to +2/s, linea
}
});
test('T2 守正自明: every damage he outputs rolls 20 % (70 % below half HP) for 1 SP — SBL-Y stage 3: 80 % below half and the instance ×1.2 on a success (stage 1: unchanged)', () => {
test('T2 守正自明: every damage he outputs rolls 23 % (75 % below half HP) for 1 SP — SBL-Y stage 3: 85 % below half and the instance ×1.2 on a success (stage 1: unchanged)', () => {
for (const f of [[5, false, null], [5, true, SBLY], [6, true, SBLY]]) {
const [tier, elite, mod] = f;
const y3 = tier === 6 && mod === SBLY;
const { h, u } = field({ tier, elite, mod, skill: 1, seed: 17 });
const t1 = u.def.raw.talents.find((t) => t.index === 1).bb;
assert.deepEqual([t1.prob_1, t1.prob_2, t1.hp_ratio, t1.sp, t1.atk_scale ?? 1], [0.2, y3 ? 0.8 : 0.7, 0.5, 1, y3 ? 1.2 : 1], label(f));
assert.deepEqual([t1.prob_1, t1.prob_2, t1.hp_ratio, t1.sp, t1.atk_scale ?? 1], [0.23, y3 ? 0.85 : 0.75, 0.5, 1, y3 ? 1.2 : 1], label(f));
const e = h.spawn('enemy_dummy', { pos: [10, 6] });
u.skill.spCostMul = 1e5; // the skill never fills: every success is an spGain of its own
for (const [ratio, prob] of [[0.9, 0.2], [0.3, y3 ? 0.8 : 0.7]]) {
for (const [ratio, prob] of [[0.9, 0.23], [0.3, y3 ? 0.85 : 0.75]]) {
let gains = 0, rolls = 0, boosted = 0;
const offG = h.b.on('spGain', (c) => { if (c.unit === u && c.reason === 'talent') gains++; });
const offD = h.b.on('damaged', (c) => {
@@ -187,7 +187,7 @@ test('凯瑟琳: device shield cap and deploy limit follow the owner\'s module v
});
const cap = (lo) => want(tokens, CATSLD, CATHY, lo).talents[0].bb.max_shield_ratio;
const limit = (lo) => (lo === NONE ? 1 : base.rawToken(CATSLD).variants[CATHY].stats.deployLimit);
assert.deepEqual([cap(DEF), cap(NONE), limit(DEF)], [0.2, 0.5, 2]);
assert.deepEqual([cap(DEF), cap(NONE), limit(DEF)], [0.22, 0.5, 2]); // 0.22: the device talent at 凯瑟琳's full potential
for (const lo of ORDERS(DEF, NONE)) {
// two placed devices each (hand pieces, user playtest #6), facing her and one more operator: the default limit (2)
// keeps both, the patched one (1) withdraws the first deployed
+10 -8
View File
@@ -210,7 +210,8 @@ test('纸偶: appear burst = its ATK × damage_scale arts on the 8 surrounding t
test('狼群: board piece becomes 伺夜\'s 援军; 2→3 狼影 (block & hits), fatal sheds a shadow, DEF ignore vs blocked, 战术点形态 after the last one', REAL, () => {
let wolfHits = 0;
const h = makeBattle({
defs: { enemies: { enemy_walker: walker({ def: 200, atk: 0 }) } },
// DEF 250: above the pack's 200 DEF ignore (its talent at 伺夜's full potential), so a bite shows the partial ignore
defs: { enemies: { enemy_walker: walker({ def: 250, atk: 0 }) } },
kits: { chess_char_3_19_a: genericNoSkill },
// the wolf is listed before its owner (higher row): it must still be linked to 伺夜 (owner-level variant)
units: [{ kind: 'token', tokenId: TOKEN_IDS.wolfPack, row: 9, col: 6, uid: 1, ownerUid: 2 }, { chessId: 'chess_char_3_19_b', row: 10, col: 4, uid: 2 }],
@@ -228,11 +229,12 @@ test('狼群: board piece becomes 伺夜\'s 援军; 2→3 狼影 (block & hits),
assert.equal(wolf.s.blockCnt, 2);
const e = h.enemy('enemy_walker');
assert.ok(h.runUntil(() => e.blockedBy === wolf && wolfHits >= 2, 30), 'blocks and bites');
// DEF ignore: every bite does ATK − (200 − 175)
// DEF ignore: every bite does ATK − (250 − 200)
assert.equal(tokDef(TOKEN_IDS.wolfPack, 'chess_char_3_19_b').talents[1].bb.def_penetrate_fixed, 200, 'the pack ignores 200 DEF (175 + the potential step)');
let dealt = null;
h.b.on('damaged', (c) => { if (c.source === wolf && c.type === 'phys') dealt = c.amount; });
h.runUntil(() => dealt != null, 5);
approx(dealt, wolf.s.atk - (200 - 175), 1e-9, 'def pen');
approx(dealt, wolf.s.atk - (250 - 200), 1e-9, 'def pen');
h.runUntil(() => wolfShadows(wolf) === 3, 30);
assert.equal(wolf.s.blockCnt, 3);
assert.equal(wolf.profile.hitsFn(h.b, wolf), 3);
@@ -927,7 +929,7 @@ test('every token variant: a spawn with that owner takes the owner-level stats (
assert.ok(n >= 38, `variants checked: ${n}`);
});
test('elite numbers: 沙之碑 230 %/1.5 s, 香槟炸弹 170 %, 纸偶 its ATK × 2.7, “耀阳” 100 % + ×1.15 vs blocked, 医疗探机 ATK 114', REAL, () => {
test('elite numbers: 沙之碑 230 %/1.5 s, 香槟炸弹 170 %, 纸偶 its ATK × 2.75 (full potential), “耀阳” 100 % + ×1.15 vs blocked, 医疗探机 ATK 114', REAL, () => {
const burstOf = (ownerId, tokenId, r, c, ePos) => {
const base = ownerId.replace(/_[ab]$/, '_a');
const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, kits: { [base]: genericNoSkill }, units: [{ chessId: ownerId, row: 12, col: 2 }], enemies: [{ key: 'enemy_dummy', pos: ePos }], autoFinish: false, timeLimit: 10 });
@@ -951,7 +953,7 @@ test('elite numbers: 沙之碑 230 %/1.5 s, 香槟炸弹 170 %, 纸偶 its ATK
{
const { t, e } = burstOf('chess_char_2_11_b', TOKEN_IDS.paperDoll, 10, 6, [11, 7]);
assert.equal(t.base.atk, 728);
approx(1e7 - e.hp, 728 * 2.7, 1e-9, '纸偶 elite');
approx(1e7 - e.hp, 728 * 2.75, 1e-9, '纸偶 elite (2.7 + the potential step of 风丸)');
}
{
const { o, e } = burstOf('chess_char_6_17_b', TOKEN_IDS.radiantSword, 10, 6, [10, 7]);
@@ -1228,7 +1230,7 @@ test('tactical point: without a board piece the 援军 (狼群) stands on an ene
checkInvariants(h.b);
});
test('狼群 (generic 伺夜): 伺夜\'s own attacks on pack-blocked enemies ignore 175 DEF and get the S3 bonus; the pack leaves with 伺夜', REAL, () => {
test('狼群 (generic 伺夜): 伺夜\'s own attacks on pack-blocked enemies ignore 200 DEF (full potential) and get the S3 bonus; the pack leaves with 伺夜', REAL, () => {
const kit = () => ({ generic: true, talents: [], skill: { kind: 'duration', duration: 60, trigger: 'SP_FULL', spCost: 1, initSp: 1 } });
const own = [], bonus = [];
// RES 100: the 弱点伤害 garrison keeps the attacks physical (arts would do 5 %)
@@ -1244,8 +1246,8 @@ test('狼群 (generic 伺夜): 伺夜\'s own attacks on pack-blocked enemies ign
assert.ok(h.runUntil(() => own.some((x) => x.blocked) && bonus.some((x) => x.src === 'chess_char_3_19_a'), 30), 'owner hits a pack-blocked enemy');
const hit = own.find((x) => x.blocked);
assert.equal(hit.type, 'phys');
// tactician trait ×1.5 vs enemies its 援军 blocks; DEF 300 − 175
approx(hit.amount, vigil.s.atk * 1.5 - (300 - 175), 1e-6, 'DEF ignore on 伺夜\'s attack');
// tactician trait ×1.5 vs enemies its 援军 blocks; DEF 300 − 200
approx(hit.amount, vigil.s.atk * 1.5 - (300 - 200), 1e-6, 'DEF ignore on 伺夜\'s attack');
approx(bonus.find((x) => x.src === 'chess_char_3_19_a').amount, vigil.s.atk * 0.2 * 0.05, 1e-6, 'S3 bonus: 20 % ATK arts vs RES 100 (5 % floor)');
assert.ok(bonus.some((x) => x.src === TOKEN_IDS.wolfPack), 'the pack\'s bites carry the bonus too');
// the 援军 leaves with its tactician (no respawn afterwards)
+15 -10
View File
@@ -121,9 +121,10 @@ test('chess: every non-DIY chess has stats, range, classification and a resolvab
assert.ok(c.assets && c.assets.avatar && c.assets.spine, `${c.chessId}: assets`);
if (c.isGolden) assert.equal(c.module ? c.module.active || c.module.id === null : true, true);
}
// Spot checks against official numbers (隐现 E1 Lv55: HP 1123, ATK 399).
// Spot checks against official numbers (隐现 E1 Lv55: HP 1123, ATK 399 + 攻击力+23 at full potential = 422; cost 15 − 3).
assert.equal(chess.chess_char_1_01_a.stats.maxHp, 1123);
assert.equal(chess.chess_char_1_01_a.stats.atk, 399);
assert.equal(chess.chess_char_1_01_a.stats.atk, 422);
assert.equal(chess.chess_char_1_01_a.stats.cost, 12);
assert.equal(chess.chess_char_1_01_a.targetPriority, 'fly');
});
@@ -577,7 +578,7 @@ test('chess: golden modules[] (+ statsBase/traitBase/talentsBase) compose back t
const m = chess.chess_char_6_11_b;
assert.deepEqual(m.modules.map((x) => [x.uniEquipId, x.typeName, x.isDefault]), [['uniequip_002_mlyss', 'TAC-X', true], ['uniequip_003_mlyss', 'TAC-Y', false]]);
assert.deepEqual(m.modules[1].attr, { maxHp: 170, atk: 28, def: 28 });
assert.deepEqual([m.statsBase.maxHp, m.statsBase.atk, m.statsBase.def], [1703, 467, 111]);
assert.deepEqual([m.statsBase.maxHp, m.statsBase.atk, m.statsBase.def], [1703, 492, 111]); // ATK 467 + 攻击力+25 (full potential)
// module-less goldens: no choices, base = stats
assert.deepEqual(chess.chess_char_5_14_b.modules, []);
assert.deepEqual(chess.chess_char_5_14_b.statsBase, chess.chess_char_5_14_b.stats);
@@ -705,12 +706,13 @@ test('stages: the escaped levels\' maps — one per helper count, never a match
});
test('official spot checks (hard-coded values from the zh_CN client data)', () => {
// at full potential (the owner's decision of 2026-10-07): the client's numbers plus each operator's potential steps
const st = (id) => { const s = chess[id].stats; return [s.maxHp, s.atk, s.def, s.res, s.blockCnt, s.cost]; };
assert.deepEqual(st('chess_char_6_11_b'), [1893, 492, 141, 0, 1, 15]); // 缪尔赛思 精锐 (E2 60 + module Lv3)
assert.deepEqual(st('chess_char_6_17_b'), [3593, 1108, 279, 0, 1, 19]); // 耀骑士临光 精锐
assert.deepEqual(st('chess_char_3_08_a'), [1594, 629, 170, 15, 1, 25]); // 薄绿 (E2 1)
assert.deepEqual(st('chess_char_2_11_a'), [1816, 547, 254, 0, 2, 15]); // 风丸 (E1 60)
assert.deepEqual(st('chess_char_4_07_b'), [2383, 604, 359, 0, 1, 13]); // 风笛 精锐
assert.deepEqual(st('chess_char_6_11_b'), [1893, 517, 141, 0, 1, 13]); // 缪尔赛思 精锐 (E2 60 + module Lv3): 492 + 25 ATK, 15 − 2 cost
assert.deepEqual(st('chess_char_6_17_b'), [3593, 1143, 279, 0, 1, 17]); // 耀骑士临光 精锐: 1108 + 35, 19 − 2
assert.deepEqual(st('chess_char_3_08_a'), [1594, 629, 170, 15, 1, 22]); // 薄绿 (E2 1): 25 − 3
assert.deepEqual(st('chess_char_2_11_a'), [1816, 574, 254, 0, 2, 13]); // 风丸 (E1 60): 547 + 27, 15 − 2
assert.deepEqual(st('chess_char_4_07_b'), [2383, 629, 359, 0, 1, 11]); // 风笛 精锐: 604 + 25, 13 − 2
assert.equal(chess.chess_char_6_11_b.module.level, 3);
assert.deepEqual([chess.chess_char_4_07_b.skill.skillId, chess.chess_char_4_07_b.skill.level, chess.chess_char_4_07_b.skill.spCost], ['skchr_bpipe_2', 7, 5]);
const es = (k) => { const s = enemies[k].stats; return [s.maxHp, s.atk, s.def, s.res]; };
@@ -741,8 +743,11 @@ test('independent re-derivation of every chess and enemy stat from the raw offic
const k0 = P.attributesKeyFrames[0], k1 = P.attributesKeyFrames[P.attributesKeyFrames.length - 1];
const t = (cd.status.charLevel - k0.level) / (k1.level - k0.level || 1);
const f = (key) => k0.data[key] + (k1.data[key] - k0.data[key]) * t;
const exp = { maxHp: f('maxHp'), atk: f('atk'), def: f('def'), res: f('magicResistance'), aspd: f('attackSpeed'), blockCnt: f('blockCnt'), cost: f('cost') };
const map = { max_hp: 'maxHp', atk: 'atk', def: 'def', magic_resistance: 'res', attack_speed: 'aspd', block_cnt: 'blockCnt', cost: 'cost' };
const exp = { maxHp: f('maxHp'), atk: f('atk'), def: f('def'), res: f('magicResistance'), aspd: f('attackSpeed'), blockCnt: f('blockCnt'), cost: f('cost'), respawnTime: f('respawnTime') };
const map = { max_hp: 'maxHp', atk: 'atk', def: 'def', magic_resistance: 'res', attack_speed: 'aspd', block_cnt: 'blockCnt', cost: 'cost', respawn_time: 'respawnTime' };
// full potential (the owner's decision of 2026-10-07): every potentialRanks attribute step (all ADDITION)
const POT = { MAX_HP: 'maxHp', ATK: 'atk', DEF: 'def', MAGIC_RESISTANCE: 'res', ATTACK_SPEED: 'aspd', COST: 'cost', RESPAWN_TIME: 'respawnTime' };
for (const r of CT[shop.charId].potentialRanks || []) for (const m of r.buff?.attributes?.attributeModifiers || []) exp[POT[m.attributeType]] += m.value;
if (cd.status.equipLevel > 0 && shop.defaultUniEquipId) {
const mp = BE[shop.defaultUniEquipId]?.phases.find((p) => p.equipLevel === cd.status.equipLevel);
for (const b of mp?.attributeBlackboard || []) if (map[b.key]) exp[map[b.key]] += b.value;
+169
View File
@@ -0,0 +1,169 @@
// test/full-potential.test.js — every operator fights at FULL potential (潜能 6), the owner's decision of 2026-10-07
// (GitHub #252, PR #255; tools/build-data.mjs OPERATOR_POTENTIAL, docs/DATA.md §2): every chess (normal and elite), 自选
// pick and 补位 stand-in adds its character_table `potentialRanks` attribute steps and takes its talent / module-talent
// candidates up to `requiredPotentialRank` 5; a summon picks its talent candidates at its owner's potential but takes
// none of the owner's attribute steps. The evidence: a tournament video shows 刺玫 (chess_char_1_06_a) at ATK 435 and
// cost 15 — her E1 Lv55 413 / 17 plus 攻击力+22 and two 部署费用-1. These numbers reach the battle, the 干员调配 局内数值
// and the detail card through the records (shared/loadoutRecord.js, simdata getChess).
// With the official-data cache (.cache/gamedata) the potential steps are re-derived from the raw character_table.
// Run: node --test test/full-potential.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { resolveLoadout, loadoutRecord } from '../server/sim/simdata.js';
import { diyRecordOf } from '../shared/diy.js';
import { standInRecord } from '../shared/standIn.js';
import { makeBattle } from './helpers/battleHarness.js';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const DATA = process.env.DATA_DIR || join(ROOT, 'data');
const CACHE = join(ROOT, '.cache', 'gamedata');
const HAS_CACHE = ['activity_table', 'character_table'].every((f) => existsSync(join(CACHE, 'excel', `${f}.json`)));
const load = (name) => JSON.parse(readFileSync(join(DATA, `${name}.json`), 'utf8'));
const chess = load('chess');
const tokens = load('tokens');
const backups = load('backups');
const raw = (f) => JSON.parse(readFileSync(join(CACHE, 'excel', `${f}.json`), 'utf8'));
const talent = (rec, name) => rec.talents.find((t) => t.name === name);
test('刺玫 at full potential: ATK 435 / cost 15 (the tournament video), 再部署 66 s, 土壤基肥改良 at its 「天赋效果增强」 step', () => {
const v = chess.chess_char_1_06_a;
assert.equal(v.name, '刺玫');
assert.deepEqual([v.stats.atk, v.stats.cost, v.stats.respawnTime, v.stats.maxHp, v.stats.def], [435, 15, 66, 1137, 77]);
assert.equal(talent(v, '土壤基肥改良').bb.heal_scale, 1.11, 'E1: 1.08 + the potential-5 step');
assert.match(talent(v, '土壤基肥改良').desc, /111%|11%/);
const e = chess.chess_char_1_06_b; // the elite: its own E2 Lv50 record plus the same five steps
assert.deepEqual([e.stats.atk, e.stats.cost, e.stats.respawnTime, e.statsBase.atk, e.statsBase.cost], [553, 15, 66, 530, 15]);
assert.equal(talent(e, '土壤基肥改良').bb.heal_scale, 1.18);
});
test('the other potential kinds: 攻击速度 (格雷伊 +6, 崖心 / 伊内丝 +8), 生命 / 防御 / 法抗, and the redeploy times of GitHub #252', () => {
const s = (id) => chess[id].stats;
assert.deepEqual([s('chess_char_1_14_a').aspd, s('chess_char_1_14_b').aspd], [106, 109], '格雷伊 攻击速度+6');
assert.deepEqual([s('chess_char_2_03_a').aspd, s('chess_char_4_04_a').aspd], [108, 108], '崖心 / 伊内丝 攻击速度+8');
assert.equal(s('chess_char_1_02_a').maxHp, 2466, '角峰 生命上限 +250');
assert.equal(s('chess_char_1_20_a').def, 516, '雷蛇 防御力 +27');
assert.equal(s('chess_char_3_09_a').res, 33, '海霓 法术抗性 +8');
// GitHub #252: 砾 18 → 16 s, 至简 80 → 70 s; 野鬃's two 再部署时间-10秒 steps: 80 → 60 s
assert.deepEqual([s('chess_char_2_12_a').respawnTime, s('chess_char_2_12_a').cost], [16, 6]);
assert.deepEqual([s('chess_char_3_13_a').respawnTime, s('chess_char_3_13_a').cost], [70, 20]);
assert.deepEqual([s('chess_char_1_19_a').respawnTime, s('chess_char_1_19_a').cost], [60, 11]);
});
test('every chess, 自选 form and stand-in form: the raw keyframes plus every potential step (re-derived from character_table)', { skip: !HAS_CACHE && 'no .cache/gamedata' }, () => {
const CT = raw('character_table');
const KEY = { MAX_HP: 'maxHp', ATK: 'atk', DEF: 'def', MAGIC_RESISTANCE: 'res', COST: 'cost', RESPAWN_TIME: 'respawnTime', ATTACK_SPEED: 'aspd' };
const steps = (charId) => {
const out = {};
for (const r of CT[charId].potentialRanks || []) {
for (const m of r.buff?.attributes?.attributeModifiers || []) {
assert.equal(m.formulaItem, 'ADDITION', `${charId}: ${m.attributeType}`);
assert.ok(KEY[m.attributeType], `${charId}: unmapped potential attribute ${m.attributeType}`);
out[KEY[m.attributeType]] = (out[KEY[m.attributeType]] || 0) + m.value;
}
}
return out;
};
const frame = (charId, phase, level) => {
const P = CT[charId].phases[phase];
const k0 = P.attributesKeyFrames[0], k1 = P.attributesKeyFrames[P.attributesKeyFrames.length - 1];
const t = (level - k0.level) / (k1.level - k0.level || 1);
const f = (key) => k0.data[key] + (k1.data[key] - k0.data[key]) * t;
return { maxHp: f('maxHp'), atk: f('atk'), def: f('def'), res: f('magicResistance'), cost: f('cost'), respawnTime: f('respawnTime'), aspd: f('attackSpeed') };
};
const check = (label, charId, status, stats) => {
const exp = frame(charId, status.phase, status.level);
for (const [k, v] of Object.entries(steps(charId))) exp[k] += v;
for (const [k, v] of Object.entries(exp)) assert.ok(Math.abs(v - stats[k]) <= 0.5 + 1e-9, `${label}: ${k} ${stats[k]} vs official ${v}`);
};
let n = 0, withSteps = 0;
for (const c of Object.values(chess)) {
if (c.isDiy || !c.charId) continue;
check(c.chessId, c.charId, c.status, c.statsBase ?? c.stats);
n++;
if (Object.keys(steps(c.charId)).length) withSteps++;
}
assert.equal(n, 258);
assert.ok(withSteps >= 220, `chess with potential steps: ${withSteps}`);
let forms = 0;
for (const [charId, u] of Object.entries(backups.units)) {
for (const f of Object.values(u.forms)) { check(`${charId}@${f.status.phase}/${f.status.level}`, charId, f.status, f.stats); forms++; }
}
assert.equal(forms, 256);
// the 原型干员 stand-ins have no potential ranks: their numbers are the plain keyframes
for (const id of ['char_600_cpione', 'char_602_cdfend', 'char_608_acpion', 'char_617_sharp2']) assert.equal((CT[id].potentialRanks || []).length, 0, id);
});
test('talent candidates: every chess talent is the last one unlocked at its status with requiredPotentialRank ≤ 5', { skip: !HAS_CACHE && 'no .cache/gamedata' }, () => {
const CT = raw('character_table');
const PH = { PHASE_0: 0, PHASE_1: 1, PHASE_2: 2 };
const ok = (c, phase, level) => {
const u = c.unlockCondition;
const p = PH[u?.phase ?? 'PHASE_0'];
return (c.requiredPotentialRank || 0) <= 5 && (p < phase || (p === phase && (u?.level || 1) <= level));
};
let potentialTalents = 0;
for (const c of Object.values(chess)) {
if (c.isDiy || !c.charId) continue;
const base = c.talentsBase ?? c.talents;
(CT[c.charId].talents || []).forEach((t, index) => {
const cands = (t.candidates || []).filter((x) => ok(x, c.status.phase, c.status.level));
const best = cands[cands.length - 1];
const rec = base.find((x) => x.index === index);
// a fully empty placeholder (no name / text / blackboard / token) is dropped from the record (mergeTalentChanges)
if (!best || (!best.name && !best.description && !best.blackboard.length && !best.tokenKey)) return;
assert.ok(rec, `${c.chessId} talent ${index}`);
for (const b of best.blackboard) if (!(b.valueStr && b.value === 0)) assert.ok(Math.abs(rec.bb[b.key] - b.value) < 1e-9, `${c.chessId} talent ${index} ${b.key}: ${rec.bb[b.key]} vs ${b.value}`);
if ((best.requiredPotentialRank || 0) > 0) potentialTalents++;
});
}
assert.ok(potentialTalents >= 150, `chess talents at a potential step: ${potentialTalents}`);
});
test('summons: talent candidates at the owner\'s potential, none of the owner\'s attribute steps', () => {
// 夕's “小自在” 化境: 15 → 18 层 (夕 潜能 5 「第一天赋效果增强」)
assert.equal(tokens.token_10015_dusk_drgn.variants.chess_char_5_12_a.talents[0].bb.max_stack_cnt, 18);
assert.equal(talent(chess.chess_char_5_12_a, '化境').bb.max_stack_cnt, 18);
// 浊心斯卡蒂's 海嗣, 伺夜's 狼群, 凯瑟琳's 防护单元, 风丸's 纸偶
assert.deepEqual(tokens.token_10017_skadi2_dedant.variants.chess_char_6_04_a.talents[1].bb, { 'skadi2_t_2[atk][1].atk': 0.09, 'skadi2_t_2[atk][2].atk': 0.18 });
assert.equal(tokens.token_10028_vigil_wolf.variants.chess_char_3_19_a.talents[1].bb.def_penetrate_fixed, 200);
assert.equal(tokens.token_10041_cathy_catsld.variants.chess_char_4_11_a.talents[0].bb.max_shield_ratio, 0.22);
assert.equal(tokens.token_10022_kazema_shadow.variants.chess_char_2_11_a.talents[0].bb.damage_scale, 2.3);
// 自选: 凯尔希's Mon3tr 不毁重构 — PRTS 凯尔希 潜能增强=5 "晕眩3.5(+0.5)秒…1400(+200)点真实伤害", X 模组 3 级 4 s / 1700
const mon = backups.tokens.token_10002_kalts_mon3tr.variants;
assert.deepEqual([mon['char_003_kalts@2/1/4/0'].talents[1].bb.stun, mon['char_003_kalts@2/1/4/0'].talents[1].bb.value], [3.5, 1400]);
const x3 = mon['char_003_kalts@2/60/7/3'].byModule.uniequip_002_kalts.talents[1].bb;
assert.deepEqual([x3.stun, x3.value], [4, 1700]);
// 望's 棋子: its rank-2 candidate (望 潜能 3 「第一天赋效果增强」) holds and deploys one more, and 铸子 sends 7
const stone = backups.tokens.token_10064_wang_stone1;
assert.deepEqual([stone.deployLimit, stone.count, stone.stats.deckStack], [7, 7, 8]);
assert.equal(backups.units.char_2027_wang.forms['2/1/4/0'].talents[0].bb.cnt, 7);
// the token's own numbers: 凯尔希's 攻击力+25 / 部署费用-1 steps stay hers (Mon3tr's frame is its own)
assert.equal(backups.units.char_003_kalts.forms['2/1/4/0'].stats.cost, 18);
for (const v of Object.values(mon)) assert.equal(v.stats.cost, backups.tokens.token_10002_kalts_mon3tr.stats.cost);
});
test('the numbers reach the loadout record (局内数值 / detail card), the 自选 and 补位 compositions and the battle', () => {
// a non-default module on the elite: statsBase (full potential) + the module's attr
const e = chess.chess_char_1_06_b;
const other = (e.modules || []).find((m) => !m.isDefault);
const none = loadoutRecord(e, resolveLoadout(e, { moduleId: 'none' }));
assert.deepEqual([none.stats.atk, none.stats.cost, none.stats.respawnTime], [530, 15, 66]);
if (other) {
const r = loadoutRecord(e, resolveLoadout(e, { moduleId: other.uniEquipId }));
assert.equal(r.stats.atk, 530 + (other.attr.atk || 0));
}
// 自选 凯尔希 (an owned 6★): her form at full potential
const diy = diyRecordOf(chess.chess_char_6_diy1_a, { charId: 'char_003_kalts', skillIndex: 0 }, { chess, backups });
assert.deepEqual([diy.stats.atk, diy.stats.cost, diy.stats.respawnTime], [417, 18, 66]);
// a 补位 stand-in is unchanged by the rule (预备干员 / 原型干员 have no potential ranks)
const st = standInRecord(chess.chess_char_3_21_a, backups);
assert.equal(st.stats.cost, backups.units[st.charId].forms[`${chess.chess_char_3_21_a.status.phase}/${chess.chess_char_3_21_a.status.level}/${chess.chess_char_3_21_a.status.skillLevel}/0`].stats.cost);
// the battle fields 刺玫 with ATK 435 / cost 15 / 66 s
const h = makeBattle({ units: [{ chessId: 'chess_char_1_06_a', row: 10, col: 5 }], timeLimit: 5 });
const u = h.unit('chess_char_1_06_a');
assert.deepEqual([u.base.atk, u.base.cost, u.base.respawnTime], [435, 15, 66]);
});
+647 -663
View File
File diff suppressed because it is too large. Load diff
+2975 -2951
View File
File diff suppressed because it is too large. Load diff
+794 -766
View File
File diff suppressed because it is too large. Load diff
+411 -388
View File
File diff suppressed because it is too large. Load diff
+1464 -1452
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -183,7 +183,7 @@ test('data.js setLocale: getters return English records after the overlay loads,
const swire = d.lookup('chess', 'chess_char_3_04_a');
assert.equal(swire.name, 'Swire the Elegant Wit');
assert.equal(swire.subProfessionName, 'Merchant');
assert.equal(swire.stats.atk, 672, 'numbers untouched');
assert.equal(swire.stats.atk, 702, 'numbers untouched'); // 672 + 攻击力+30 (full potential)
assert.equal(d.list('bonds').find((b) => b.bondId === 'yanShip').name, 'Yan');
assert.equal(d.get('config').modes.mode_multi_hard.name, 'Dire Simulation');
assert.equal(d.getRaw('config').modes.mode_multi_hard.name, '绝境模拟', 'getRaw keeps the Chinese file');
+1 -1
View File
@@ -65,7 +65,7 @@ test('#22 望\'s 棋子: a hand piece goes on a 高台 (ranged tile) as well as
assert.equal(ps.gd.token(STONE).position, 'ALL');
const wang = placeDiy(m, ps, T5A);
const stack = ps.hand.find((p) => p && p.kind === 'token' && p.id === STONE && p.ownerUid === wang.uid);
assert.ok(stack && stack.count === 6, 'her 6 棋子 joined the hand');
assert.ok(stack && stack.count === 7, 'her 7 棋子 joined the hand (6 + 1 at full potential)');
const high = tilesOf(ps, 'ranged');
const ground = tilesOf(ps, 'melee');
assert.ok(high.length && ground.length, 'the stage has both tile classes');
+7 -7
View File
@@ -30,7 +30,7 @@ const lethal = (h, u) => h.b.dealDamage(null, u, { amount: 1e9, type: 'true', ca
const waitOf = (u) => (u.def.raw.talents || []).find((t) => t.index === 1).bb['surtr_t_2[withdraw].interval'];
describe('#52 史尔特尔 余烬: 禁疗 from the lethal blow until she leaves', () => {
test('a medic beside her heals the other injured operator instead — 0 heals reach her, she is never its target, she still leaves at 8 s; the 禁疗 status is shown', () => {
test('a medic beside her heals the other injured operator instead — 0 heals reach her, she is never its target, she still leaves at 9 s (8 + the potential step); the 禁疗 status is shown', () => {
const h = makeBattle({
defs: { enemies: { enemy_dummy: DUMMY } }, autoFinish: false, timeLimit: 200, seed: 2,
units: [
@@ -42,7 +42,7 @@ describe('#52 史尔特尔 余烬: 禁疗 from the lethal blow until she leaves'
});
const s = h.unit(SURTR), medic = h.unit(MEDIC), tank = h.unit(TANK);
const wait = waitOf(s);
assert.equal(wait, 8, 'data: surtr_t_2[withdraw].interval');
assert.equal(wait, 9, 'data: surtr_t_2[withdraw].interval (8 s, 9 at full potential)');
h.run(1);
const healed = new Map();
const picked = new Set();
@@ -57,18 +57,18 @@ describe('#52 史尔特尔 余烬: 禁疗 from the lethal blow until she leaves'
assert.deepEqual(on.map((ev) => ev[3]), [1], 'the status 禁疗 is reported to the clients');
assert.equal(statusIconKey('healFree'), 'healFree');
assert.ok(STATUS_KEYS.includes('healFree'), 'and drawn in the status atlas');
// 不死: a second lethal blow inside the window changes nothing (and does not restart the 8 s)
// 不死: a second lethal blow inside the window changes nothing (and does not restart the 9 s)
h.run(3);
lethal(h, s);
assert.ok(s.alive && s.hp >= 1);
h.run(wait - 3 - 0.2);
assert.ok(s.alive && s.deployed, 'still on the field just before 8 s');
assert.ok(s.alive && s.deployed, 'still on the field just before 9 s');
assert.ok(s.hp < 2, 'nobody healed her');
assert.equal(healed.get(s) ?? 0, 0, '0 heals to her');
assert.ok(!picked.has(s), 'the medic never picks her');
assert.ok((healed.get(tank) ?? 0) > 0 && picked.has(tank), 'the medic heals the other injured operator instead');
h.run(0.4);
assert.equal(s.deployed, false, 'she leaves at 8 s');
assert.equal(s.deployed, false, 'she leaves at 9 s');
assert.equal(s.removeReason, 'retreat', '"强制退出战场视为撤回干员"');
assert.ok(h.b.time - t0 <= wait + 0.25);
const ev = h.eventsOf('status').filter((e) => e[1] === s.id && e[2] === 'healFree').map((e) => e[3]);
@@ -76,7 +76,7 @@ describe('#52 史尔特尔 余烬: 禁疗 from the lethal blow until she leaves'
clean(h);
});
test('her own heals stop too (休眠子裔), an HP-regen attribute does not, and S3 黄昏\'s start heal ignores 禁疗 (PRTS 技能3 备注) — she still leaves at 8 s', () => {
test('her own heals stop too (休眠子裔), an HP-regen attribute does not, and S3 黄昏\'s start heal ignores 禁疗 (PRTS 技能3 备注) — she still leaves at 9 s', () => {
const mk = (o = {}) => makeBattle({
defs: { enemies: { enemy_dummy: DUMMY } }, autoFinish: false, timeLimit: 200, seed: 2,
units: [{ chessId: SURTR, row: 9, col: 5, dir: 'RIGHT', carryState: { sp: 0 }, ...o }],
@@ -115,7 +115,7 @@ describe('#52 史尔特尔 余烬: 禁疗 from the lethal blow until she leaves'
assert.ok(u.hp > u.s.maxHp - 50, 'healed to full in spite of 禁疗');
h3.runUntil(() => !u.deployed, waitOf(u) + 1);
assert.equal(u.removeReason, 'retreat');
assert.ok(Math.abs(h3.b.time - t0 - waitOf(u)) < 0.1, 'and she still leaves 8 s after the lethal blow');
assert.ok(Math.abs(h3.b.time - t0 - waitOf(u)) < 0.1, 'and she still leaves 9 s after the lethal blow');
clean(h); clean(h3);
});
});
+3 -1
View File
@@ -105,7 +105,9 @@ test('getChess(id, loadout): selected skill (bb, SP, trigger) and module (stats,
assert.equal(m3.raw.module.id, 'uniequip_003_mlyss');
assert.equal(m3.raw.module.active, true);
assert.equal(none.raw.module.active, false, 'no module ⇒ moduleOn(chess) false for kits');
assert.ok(m3.talents.some((t) => t.bb.runtime_cost === -1 && t.bb.cost === -2), 'module talent change applied');
// 开源节流 at full potential (runtime_cost −2; the module restates it) and the module's own data-only talents (sp_other)
assert.ok(m3.talents.some((t) => t.bb.runtime_cost === -2 && t.bb.cost === -2), 'module talent change applied');
assert.ok(m3.talents.some((t) => t.bb.sp_other === 10) && !none.talents.some((t) => t.bb.sp_other === 10), 'the module-only talent');
assert.equal(m3.skill.id, dm.skill.id, 'a module choice keeps the skill');
const both = d.getChess(MLYSS, { skillIndex: 0, moduleId: 'none' });
assert.equal(both.skill.id, 'skchr_mlyss_1');
+2 -2
View File
@@ -581,8 +581,8 @@ test('#18 安洁莉娜 兼职工作 is an HP-regen attribute (PRTS 备注 "不
assert.ok(!ag.skill.active, 'skill off');
const w = h.b.allyUnits.find((x) => x.defId === 'token_10028_vigil_wolf' && x.alive);
assert.ok(w.s.flags.noHeal, '狼群 holds 禁疗');
assert.equal(w.findBuff('aglina:parttime')?.mods.hpRegen, 20, '禁疗 does not stop the regen attribute');
assert.ok(w.s.hpRegen >= 20);
assert.equal(w.findBuff('aglina:parttime')?.mods.hpRegen, 25, '禁疗 does not stop the regen attribute (20 + the potential step)');
assert.ok(w.s.hpRegen >= 25);
assert.ok(!y.findBuff('aglina:parttime') && !y.findBuff('aglina:field'), '孤立: no ally talent selects 炎佑');
assert.ok(h.unit('chess_char_3_19_a').findBuff('aglina:field'), 'the operators get 加速力场');
});