feedback5 WL: 生命回复速度 is not 治疗 — the 吟游者 trait, 调香师, 瑕光 S2, 铃兰 S3 and 锡人 give an hpRegen buff, so 无法被友方治疗 / 禁疗 units get it and 治疗加成 does not scale it (GitHub #96 / #137)
PRTS: 分支特性信息 吟游者 「特性为基于自身攻击力来增加受益者的“生命回复速度”属性」 (魔王, 浊心斯卡蒂); the 备注 of 调香师 熏衣草, 瑕光 S2, 铃兰 S3 and 锡人 S2 「生命恢复的提供方式为增加目标的“生命回复速度”属性…不受治疗加成和禁疗影响」 (铃兰: none in the first second, refreshed every second); 异常效果 禁疗 「增减生命回复速度…的效果不会被识别为治疗类能力」. These were heals from the provider, which damage.js refused on 收割者 / 不屈者 / 武者 (noHeal) and under 禁疗 — 隐德来希 gained nothing under 魔王. This is the separation proposed in PR #135 by @SpiritedAwayCN, written anew (the PR's patch is not used): - professions.js bardRegen: the trait is an hpRegen buff per bard refreshed every 0.25 s; 魔王's 微尘 ×1.5 goes through its `bardRegen` hook (both 魔王 kits); 浊心斯卡蒂's pulse and the 海嗣 token use it too. - 调香师 (every ally), 瑕光 S2 (the skill range, removed at its end), 铃兰 S3 (from the first full second, removed at its end), 锡人 (per 炼金单元, its cached ATK): hpRegen buffs instead of battle.heal. - damage.js: a `regen` tick takes no healing multiplier and a heal hook cannot change its amount (no 治疗加成 on any 生命回复速度); 流明's 凡人之愿 and 华法琳's S1 bonus skip a regen tick of their own. Direct heals are unchanged (still refused / scaled); 魔王 S3's redistribution and 纯烬's HoT are untouched. Other per-second recoveries without such a note stay heals (引星棘刺, 塞雷娅 S3, 流明 / 纯烬 HoTs; 余 闲云隐市 and 天马之盔 left open). Golden moved (every one fields 魔王 / 浊心斯卡蒂 / 调香师 / 瑕光 S2 / 铃兰 / 锡人): roster-004, roster-005, roster-006, roster-011, roster-013, roster-015, roster-016, roster-018, roster-023, roster-025, roster-030, roster-032, roster-035, roster-037, roster-039, roster-041, roster-042, roster-045, roster-046; bond-deputShip-high, bond-emptyShip-low, bond-emptyShip-high, bond-indomShip-high, bond-kazimierzShip-low, bond-kazimierzShip-high, bond-sargonShip-low, bond-sargonShip-high, bond-skillfulShip-high; boss-boss_1-pair, boss-boss_1-solo, boss-boss_2-solo, boss-boss_3-pair, boss-boss_3-solo, boss-boss_4-solo, boss-boss_5-pair, boss-boss_5-solo, boss-boss_6-solo, boss-boss_7-pair, boss-boss_7-solo, hidden-boss_8-solo, hidden-boss_9-pair, hidden-boss_9-solo, hidden-boss_10-solo, unite-1; solo-NORMAL-1, solo-HARD-1, solo-ABYSS-1, coop4-ABYSS-7, coop2-NORMAL-8-serverrun, coop2-NORMAL-14-standins. Docs: SIM.md (heal pipeline, bard row), PLAYING.md, kits/README.md (checklist item 11).
This commit is contained in:
1 parent
696085ac8e
commit
5c6524e1ad
27 files changed
+943
-721
No files matched your search
@@ -0,0 +1,92 @@
|
||||
// test/content/feedback5-regen.test.js — 生命回复速度 is not 治疗 (GitHub #96 / #137). Effects PRTS describes as raising the
|
||||
// target's 「生命回复速度」 attribute — the 吟游者 trait (分支特性信息 吟游者; 魔王, 浊心斯卡蒂), 调香师's 熏衣草, 瑕光 S2, 铃兰 S3 —
|
||||
// are hpRegen buffs: 无法被友方治疗 (隐德来希 收割者, 折桠 不屈者) and 禁疗 do not stop them, 治疗加成 does not scale them
|
||||
// (damage.js heal `regen`); a direct heal is still refused / scaled as before.
|
||||
// Run: node --test test/content/feedback5-regen.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const REAPER = 'chess_char_5_06_a', UNYIELD = 'chess_char_2_17_a';
|
||||
const DEFS = { chess: { t_plain: chessRec({ id: 't_plain', profession: 'TANK', skill: null, stats: { maxHp: 10000, atk: 0, def: 0 } }) } };
|
||||
|
||||
/** Provider on (10,4) facing right with 隐德来希 on (10,5) and 折桠 on (11,4) at half HP (both "无法被友方角色治疗"). */
|
||||
function field(provider, extra = []) {
|
||||
const h = makeBattle({
|
||||
defs: DEFS, autoFinish: false, timeLimit: 120,
|
||||
units: [{ row: 10, col: 4, ...provider }, { chessId: REAPER, row: 10, col: 5 }, { chessId: UNYIELD, row: 11, col: 4 }, ...extra],
|
||||
});
|
||||
h.step();
|
||||
const p = h.unit(provider.chessId), r = h.unit(REAPER), z = h.unit(UNYIELD);
|
||||
for (const x of [r, z]) x.hp = x.s.maxHp * 0.5;
|
||||
return { h, p, r, z };
|
||||
}
|
||||
|
||||
/** HP gained over `secs` s against the unit's own 生命回复速度 (at most ~1 HP pending in the regeneration tick). */
|
||||
function regenOver(h, u, secs) {
|
||||
const hp0 = u.hp, rate = u.s.hpRegen;
|
||||
h.run(secs);
|
||||
return { gained: u.hp - hp0, expected: rate * secs, rate };
|
||||
}
|
||||
|
||||
const PROVIDERS = [
|
||||
// [label, unit entry, prepare(h, p) → buff key of the provider's 生命回复速度]
|
||||
['魔王 trait (吟游者)', { chessId: 'chess_char_4_25_a' }, (h, p) => `trait:bard:${p.id}`],
|
||||
['浊心斯卡蒂 trait (吟游者, S2 loadout: on, trait raised)', { chessId: 'chess_char_6_04_a', skillIndex: 1, carryState: { sp: 999 } }, (h, p) => {
|
||||
assert.ok(h.runUntil(() => p.skill.active, 5));
|
||||
return `trait:bard:${p.id}`;
|
||||
}],
|
||||
['调香师 熏衣草', { chessId: 'chess_char_2_14_a' }, (h, p) => `flower:lavender:${p.id}`],
|
||||
['瑕光 S2 慑敌辉光', { chessId: 'chess_char_3_12_a', skillIndex: 1 }, (h, p) => { assert.ok(p.skill.activate('test', { free: true })); return `blemsh:regen:${p.id}`; }],
|
||||
['铃兰 S3 狐火渺然', { chessId: 'chess_char_5_10_a' }, (h, p) => { assert.ok(p.skill.activate('test', { free: true })); h.run(1.05); return `lisa:fox:${p.id}`; }],
|
||||
];
|
||||
|
||||
test('隐德来希 (收割者) and 折桠 (不屈者) regenerate under 魔王 / 浊心斯卡蒂 / 调香师 / 瑕光 S2 / 铃兰 S3 — none of it is a heal; a direct heal from the same provider is still refused', () => {
|
||||
for (const [label, entry, prepare] of PROVIDERS) {
|
||||
const { h, p, r, z } = field(entry);
|
||||
const key = prepare(h, p);
|
||||
h.run(1.2); // (the providers' own talent auras settle their ATK first)
|
||||
for (const x of [r, z]) {
|
||||
const b = x.findBuff(key);
|
||||
assert.ok(b && b.mods.hpRegen > 0, `${label}: ${x.defId} carries the 生命回复速度 buff`);
|
||||
const { gained, expected } = regenOver(h, x, 2);
|
||||
assert.ok(gained > 0 && Math.abs(gained - expected) <= 1.5, `${label}: ${x.defId} +${gained.toFixed(2)} ≈ ${expected.toFixed(2)}`);
|
||||
assert.equal(h.b.heal(p, x, 100), 0, `${label}: a direct heal on ${x.defId} is still refused (无法被友方角色治疗)`);
|
||||
}
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.source === p && (c.target === r || c.target === z)).length, 0, `${label}: no heal of the provider reached them`);
|
||||
checkInvariants(h.b);
|
||||
assert.equal(h.b.errors.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('禁疗 does not stop 生命回复速度 (异常效果 禁疗 「增减生命回复速度…的效果不会被识别为治疗类能力」); it still stops heals, the unit\'s own included', () => {
|
||||
const { h, p } = field({ chessId: 'chess_char_2_14_a' }, [{ chessId: 't_plain', row: 9, col: 4 }]);
|
||||
const t = h.unit('t_plain');
|
||||
h.b.addBuff(t, { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } });
|
||||
t.hp = 4000;
|
||||
h.run(0.6);
|
||||
const { gained, expected } = regenOver(h, t, 2);
|
||||
assert.ok(gained > 0 && Math.abs(gained - expected) <= 1.5, `under 禁疗: +${gained} ≈ ${expected}`);
|
||||
assert.equal(h.b.heal(p, t, 100), 0, 'a heal from her: refused');
|
||||
assert.equal(h.b.heal(t, t, 100, { self: true }), 0, 'its own heal: 0 under 禁疗');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('治疗加成 does not scale 生命回复速度: a healing-received multiplier and 百炼嘉维尔\'s 医学背景 (a heal hook) leave the regeneration as it is; a direct heal is scaled', () => {
|
||||
const { h, p } = field({ chessId: 'chess_char_2_14_a' }, [{ chessId: 't_plain', row: 9, col: 4 }, { chessId: 'chess_char_4_23_a', row: 12, col: 4 }]);
|
||||
const t = h.unit('t_plain'), gv = h.unit('chess_char_4_23_a');
|
||||
h.b.addBuff(t, { key: 'test:healUp', mods: { healingTakenMul: 1.5 } });
|
||||
t.hp = 4000;
|
||||
gv.hp = gv.s.maxHp * 0.3; // 医学背景: healing received ×1.4 below half HP
|
||||
h.run(0.6);
|
||||
const v = p.s.atk * p.def.talents[0].bb.atk_to_hp_recovery_ratio;
|
||||
for (const x of [t, gv]) {
|
||||
assert.ok(Math.abs(x.s.hpRegen - x.base.hpRecoveryPerSec - v) < 1e-6, `${x.defId}: 生命回复速度 +ATK × ratio, unscaled`);
|
||||
const { gained, expected } = regenOver(h, x, 2);
|
||||
assert.ok(Math.abs(gained - expected) <= 1.5, `${x.defId}: +${gained} ≈ ${expected} (no ×1.5 / ×1.4)`);
|
||||
}
|
||||
assert.ok(Math.abs(h.b.heal(p, t, 100) - 150) < 1e-6, 'a heal: ×1.5 healing received');
|
||||
assert.ok(h.b.heal(p, gv, 100) > 100 + 1e-6, 'a heal on 百炼嘉维尔: her 医学背景 applies');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
@@ -565,7 +565,7 @@ test('3_12 瑕光 S1 光芒涌动 (自动触发 ⇒ DEFAULT, charges): next atta
|
||||
}
|
||||
});
|
||||
|
||||
test('3_12 瑕光 S2 慑敌辉光: ATK +, ground enemies on her tile sleep for the skill, allies of the skill range healed each second', () => {
|
||||
test('3_12 瑕光 S2 慑敌辉光: ATK +, ground enemies on her tile sleep for the skill, allies of the skill range get 生命回复速度 (PRTS 备注: no heal)', () => {
|
||||
for (const id of BOTH('chess_char_3_12_a')) {
|
||||
const b = SB(id, 'skchr_blemsh_2'), t1 = TB(id, 1);
|
||||
const dur = LD(id, 'skchr_blemsh_2').skill.duration;
|
||||
@@ -586,14 +586,17 @@ test('3_12 瑕光 S2 慑敌辉光: ATK +, ground enemies on her tile sleep for t
|
||||
assert.ok(e.s.flags.sleep, 'her tile: asleep');
|
||||
approx(e.findBuff('sleep').timeLeft, dur - h.b.dt, 0.05, 'for the skill duration');
|
||||
assert.ok(!n.s.flags.sleep, 'next tile: awake');
|
||||
const n0 = h.hooksOf('damaged').length;
|
||||
const v = u.s.atk * b['attack@atk_to_hp_recovery_ratio'];
|
||||
approx(ally.findBuff(`blemsh:regen:${u.id}`)?.mods.hpRegen ?? 0, v, 1e-6, '生命回复速度 +ATK × ratio');
|
||||
const n0 = h.hooksOf('damaged').length, hp0 = ally.hp, t1s = h.b.time, own = ally.s.hpRegen - v;
|
||||
h.run(2.2);
|
||||
const onSleeper = h.hooksOf('damaged').slice(n0).filter((c) => c.source === u && c.target === e && c.dmg?.isAttack);
|
||||
assert.ok(onSleeper.length >= 1, '仁慈: she hits the sleeper');
|
||||
approx(onSleeper[0].amount, u.s.atk * t1.atk_scale, 1e-6, '×仁慈');
|
||||
const regen = h.hooksOf('heal').filter((c) => c.source === u && c.target === ally);
|
||||
assert.equal(regen.length, 2, 'once per second');
|
||||
approx(regen[0].amount, u.s.atk * b['attack@atk_to_hp_recovery_ratio']);
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.source === u && c.target === ally).length, 0, 'no heal of hers');
|
||||
assert.ok(Math.abs(ally.hp - hp0 - (v + own) * (h.b.time - t1s)) <= 1.5, `regenerated ${ally.hp - hp0}`);
|
||||
assert.ok(h.runUntil(() => !u.skill.active, dur + 1));
|
||||
assert.equal(ally.findBuff(`blemsh:regen:${u.id}`), null, 'gone with the skill');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -313,7 +313,7 @@ test('6_03 余 S2 厚礼上宾: cast with an enemy on its x-1 (SKILL_RANGE — a
|
||||
// =================================================================================================================
|
||||
// 6_04 浊心斯卡蒂
|
||||
|
||||
test('6_04 浊心斯卡蒂 S1 同归殊途之吟: SP_FULL, full HP + max HP +; trait heal raised; 50 % of the damage of allies in range goes to her', () => {
|
||||
test('6_04 浊心斯卡蒂 S1 同归殊途之吟: SP_FULL, full HP + max HP +; trait (生命回复速度) raised; 50 % of the damage of allies in range goes to her', () => {
|
||||
for (const id of both('chess_char_6_04')) {
|
||||
const sid = 'skchr_skadi2_1', bb = bbOf(id, sid);
|
||||
const h = run({
|
||||
@@ -333,14 +333,14 @@ test('6_04 浊心斯卡蒂 S1 同归殊途之吟: SP_FULL, full HP + max HP +; t
|
||||
approx(onAlly[0].amount, 1000 * (1 - bb.damage_resistance), 'the ally takes the rest');
|
||||
approx(onHer[0].amount, 1000 * bb.damage_resistance, 'she takes the transferred part');
|
||||
assert.equal(onHer[0].type, 'true');
|
||||
const hh = heals(h, u, (c) => c.target === a && c.opts?.aura);
|
||||
assert.ok(hh.length > 0);
|
||||
approx(hh[hh.length - 1].amount, u.s.atk * bb['attack@atk_to_hp_recovery_ratio'], 'trait heal raised');
|
||||
// the trait is an hpRegen buff on the ally (PRTS 分支特性信息 吟游者; professions.js bardRegen), no heal of hers
|
||||
approx(a.findBuff(`trait:bard:${u.id}`)?.mods.hpRegen ?? 0, u.s.atk * bb['attack@atk_to_hp_recovery_ratio'], 'trait raised');
|
||||
assert.equal(heals(h, u, (c) => c.target === a).length, 0, 'no heal of hers');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('6_04 浊心斯卡蒂 S2 同葬无光之愿: toggle; 鼓舞 ATK and DEF (atk / def × hers) on the other allies of her range, trait heal 16/17 %', () => {
|
||||
test('6_04 浊心斯卡蒂 S2 同葬无光之愿: toggle; 鼓舞 ATK and DEF (atk / def × hers) on the other allies of her range, trait 16/17 % (生命回复速度)', () => {
|
||||
for (const id of both('chess_char_6_04')) {
|
||||
const sid = 'skchr_skadi2_2', bb = bbOf(id, sid);
|
||||
const h = run({
|
||||
@@ -359,9 +359,11 @@ test('6_04 浊心斯卡蒂 S2 同葬无光之愿: toggle; 鼓舞 ATK and DEF (at
|
||||
approx(a.s.def, a.base.def + u.s.def * bb.def, 'DEF raised by the flat value', 1e-3);
|
||||
assert.ok(!u.findBuff('inspire') && !u.findBuff('inspire:def'), 'never on herself');
|
||||
a.hp = a.s.maxHp * 0.5;
|
||||
const hp0 = a.hp, t1 = h.b.time;
|
||||
h.run(1.1);
|
||||
const hh = heals(h, u, (c) => c.target === a && c.opts?.aura);
|
||||
approx(hh[hh.length - 1].amount, u.s.atk * bb['attack@atk_to_hp_recovery_ratio'], 'trait heal ratio');
|
||||
const v = u.s.atk * bb['attack@atk_to_hp_recovery_ratio'];
|
||||
approx(a.findBuff(`trait:bard:${u.id}`)?.mods.hpRegen ?? 0, v, 'trait ratio');
|
||||
assert.ok(Math.abs(a.hp - hp0 - (a.s.hpRegen) * (h.b.time - t1)) <= 1.5, `regenerated ${a.hp - hp0}`);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -496,7 +496,7 @@ test('1_15 盟约·辅助干员 (hidden): 迭代元素 18 % ATK 神经 + 灼燃
|
||||
});
|
||||
|
||||
for (const [idA, idB] of [['chess_char_1_16_a', 'chess_char_1_16_b'], ['chess_char_2_19_a', 'chess_char_2_19_b']]) {
|
||||
test(`${idA.slice(11, 15)} 锡人: “大拉里” zone — ground enemies take atk_scale × ATK arts/s, allies heal; elite 凋敝魂灵 + module SP`, () => {
|
||||
test(`${idA.slice(11, 15)} 锡人: “大拉里” zone — ground enemies take atk_scale × ATK arts/s, allies inside get 生命回复速度 (PRTS 备注: no heal); elite 凋敝魂灵 + module SP`, () => {
|
||||
const bb = bbOf(idA);
|
||||
const flyer = dummy('e_fly', { motion: 'FLY' });
|
||||
const h = run({
|
||||
@@ -513,10 +513,25 @@ for (const [idA, idB] of [['chess_char_1_16_a', 'chess_char_1_16_b'], ['chess_ch
|
||||
assert.equal(z.length, bb.projectile_delay_time);
|
||||
for (const c of z) approx(c.amount, u.s.atk * bb.atk_scale);
|
||||
assert.equal(dealt(h, u, (c) => c.target === f && (c.dmg.tags || []).includes('zone')).length, 0, 'air units unaffected');
|
||||
const zh = heals(h, u, (c) => c.target === yak);
|
||||
assert.equal(zh.length, bb.projectile_delay_time);
|
||||
for (const c of zh) approx(c.amount, u.s.atk * bb.hp_recovery_per_sec_ratio);
|
||||
assert.equal(heals(h, u, (c) => c.target === yak).length, 0, 'no heal of hers: an hpRegen buff (PRTS 备注 「增加目标的“生命回复速度”属性」)');
|
||||
done(h);
|
||||
// the ally inside: 生命回复速度 + her ATK at the cast × hp_recovery_per_sec_ratio for the unit's life, then gone
|
||||
const h3 = run({
|
||||
defs: { enemies: { e: dummy('e') } },
|
||||
units: [{ chessId: idA, row: 10, col: 4, carryState: READY }, { chessId: 'chess_char_1_02_a', row: 10, col: 6 }],
|
||||
enemies: [{ key: 'e', pos: [10, 5] }],
|
||||
});
|
||||
const u3 = h3.unit(idA), yak3 = h3.unit('chess_char_1_02_a');
|
||||
const zone = () => yak3.buffs.find((b) => String(b.key).startsWith('tinman:zone:'));
|
||||
assert.ok(h3.runUntil(() => !!zone(), 2), 'the unit is down');
|
||||
const v = u3.s.atk * bb.hp_recovery_per_sec_ratio;
|
||||
approx(zone().mods.hpRegen, v, 'ATK × ratio');
|
||||
yak3.hp = yak3.s.maxHp * 0.3;
|
||||
const hp0 = yak3.hp, t1 = h3.b.time;
|
||||
h3.run(5);
|
||||
assert.ok(Math.abs(yak3.hp - hp0 - v * (h3.b.time - t1)) <= 1.5, `regenerated ${yak3.hp - hp0} ≈ ${v * 5}`);
|
||||
assert.ok(h3.runUntil(() => !zone(), bb.projectile_delay_time + 1), 'gone with the unit');
|
||||
done(h3);
|
||||
|
||||
const bbB = bbOf(idB), w = tal(idB, 1)['skill@damage_scale'], mb = hid(idB);
|
||||
const h2 = run({ defs: { enemies: { e: dummy('e') } }, units: [{ chessId: idB, row: 10, col: 4 }], enemies: [{ key: 'e', pos: [10, 5] }] });
|
||||
@@ -1018,7 +1033,7 @@ test('2_13 蒂比: an incoming attack triggers 紧急赶场通知 and is dodged;
|
||||
done(h2);
|
||||
});
|
||||
|
||||
test('2_14 调香师: 精调 ATK +atk / ASPD −50; 熏衣草 heals every ally atk_to_hp_recovery_ratio × ATK per s; elite heals 4', () => {
|
||||
test('2_14 调香师: 精调 ATK +atk / ASPD −50; 熏衣草 every ally 生命回复速度 +atk_to_hp_recovery_ratio × ATK (PRTS 备注: no heal); elite heals 4', () => {
|
||||
const id = 'chess_char_2_14_a', bb = bbOf(id), t = tal(id);
|
||||
const h = run({ units: [{ chessId: id, row: 10, col: 4, carryState: READY }, { chessId: 'chess_char_1_02_a', row: 9, col: 9 }, { chessId: 'chess_char_1_10_a', row: 10, col: 5 }] });
|
||||
const u = h.unit(id), far = h.unit('chess_char_1_02_a'), near = h.unit('chess_char_1_10_a');
|
||||
@@ -1029,9 +1044,12 @@ test('2_14 调香师: 精调 ATK +atk / ASPD −50; 熏衣草 heals every ally a
|
||||
assert.ok(u.skill.active);
|
||||
approx(u.s.atk, u.base.atk * (1 + bb.atk));
|
||||
approx(u.s.aspd, u.base.aspd + bb.attack_speed);
|
||||
const aura = heals(h, u, (c) => c.target === far);
|
||||
assert.equal(aura.length, 3, 'one pulse per second, out of range too');
|
||||
for (const c of aura) approx(c.amount, u.s.atk * t.atk_to_hp_recovery_ratio);
|
||||
const v = u.s.atk * t.atk_to_hp_recovery_ratio;
|
||||
for (const x of [far, near, u]) approx(x.s.hpRegen - x.base.hpRecoveryPerSec, v, `${x.defId}: 生命回复速度 +ATK × ratio (out of range too)`);
|
||||
assert.equal(heals(h, u, (c) => c.target === far).length, 0, 'no heal of hers');
|
||||
const hp0 = far.hp, t1 = h.b.time;
|
||||
h.run(2);
|
||||
assert.ok(Math.abs(far.hp - hp0 - far.s.hpRegen * (h.b.time - t1)) <= 1.5, `regenerated ${far.hp - hp0}`);
|
||||
done(h);
|
||||
const idb = 'chess_char_2_14_b';
|
||||
const ids = ['chess_char_1_02_a', 'chess_char_1_10_a', 'chess_char_1_12_a', 'chess_char_2_07_a', 'chess_char_1_18_a'];
|
||||
|
||||
@@ -855,7 +855,7 @@ test('卡涅利安 / 蜜蜡 elite trait: keep DEF +100 % / RES +10 during the sk
|
||||
}
|
||||
});
|
||||
|
||||
test('魔王 S3: range up, trait heal 65 % ATK/s, 鼓舞 +65 % of her max HP, HP redistributed every 2 s; motes ×1.5; 萨卡兹 −10 %', () => {
|
||||
test('魔王 S3: range up, trait 65 % ATK/s (生命回复速度), 鼓舞 +65 % of her max HP, HP redistributed every 2 s; motes ×1.5; 萨卡兹 −10 %', () => {
|
||||
const id = 'chess_char_4_25_a', bb = D(id).skill.bb, t0 = D(id).talents[0].bb, t1 = D(id).talents[1].bb;
|
||||
const sarkaz = withTags(dummy({ key: 'enemy_sarkaz' }), ['sarkaz']);
|
||||
const h = makeBattle({ defs: { enemies: { enemy_sarkaz: sarkaz } }, units: [{ chessId: id, row: 10, col: 5 }, { chessId: 'chess_char_1_02_a', row: 10, col: 6 }, { chessId: 'chess_char_4_17_a', row: 9, col: 5 }],
|
||||
@@ -868,10 +868,11 @@ test('魔王 S3: range up, trait heal 65 % ATK/s, 鼓舞 +65 % of her max HP, HP
|
||||
const n0 = noisy(h, 'damaged').length;
|
||||
h.b.dealDamage(e, a, { amount: 1000, type: 'true' });
|
||||
approx(noisy(h, 'damaged')[n0].amount, 1000 * (1 - t1.damage_resistance), 1e-9);
|
||||
// trait heal ×1.5 with a mote
|
||||
h.runUntil(() => noisy(h, 'heal').some((c) => c.source === u && c.target === a), 3);
|
||||
const hl = noisy(h, 'heal').find((c) => c.source === u && c.target === a);
|
||||
approx(hl.amount, u.s.atk * D(id).traitBb['attack@atk_to_hp_recovery_ratio'] * t0['attack@trait_mul'] * a.s.healingTakenMul, 1e-6);
|
||||
// the trait: 生命回复速度 (an hpRegen buff — PRTS 分支特性信息 吟游者; professions.js bardRegen), ×1.5 with a mote
|
||||
const trait = (x) => x.findBuff(`trait:bard:${u.id}`)?.mods.hpRegen ?? 0;
|
||||
h.run(0.3);
|
||||
approx(trait(a), u.s.atk * D(id).traitBb['attack@atk_to_hp_recovery_ratio'] * t0['attack@trait_mul'], 1e-6, 'trait ×1.5 with a mote');
|
||||
assert.equal(noisy(h, 'heal').filter((c) => c.source === u && c.target === a).length, 0, 'no heal of hers');
|
||||
// S3
|
||||
h.b.dealDamage(null, b, { amount: b.s.maxHp * 0.7, type: 'true' });
|
||||
assert.ok(u.skill.activate('test', { free: true }));
|
||||
@@ -882,9 +883,8 @@ test('魔王 S3: range up, trait heal 65 % ATK/s, 鼓舞 +65 % of her max HP, HP
|
||||
for (let i = 0; i < 90 && !redistributed; i++) { h.step(); redistributed = h.events.some((ev) => ev[0] === 'fx' && ev[1] === 'redistribute'); }
|
||||
assert.ok(redistributed);
|
||||
approx(a.hpRatio, b.hpRatio, 1e-9, 'equal HP ratios after redistribution');
|
||||
h.runUntil(() => noisy(h, 'heal').filter((c) => c.source === u && c.target === b && c.t > 0.8).length >= 1, 3);
|
||||
const sh = noisy(h, 'heal').filter((c) => c.source === u && c.target === b).pop();
|
||||
approx(sh.amount, u.s.atk * bb['attack@atk_to_hp_recovery_ratio'] * t0['attack@trait_mul'] * b.s.healingTakenMul, 1e-6, 'trait 65 %');
|
||||
h.run(0.3);
|
||||
approx(trait(b), u.s.atk * bb['attack@atk_to_hp_recovery_ratio'] * (b.findBuff(`cetsyr:mote:${u.id}`) ? t0['attack@trait_mul'] : 1), 1e-6, 'trait 65 %');
|
||||
h.runUntil(() => !u.skill.active, 40);
|
||||
h.run(0.3);
|
||||
approx(a.s.maxHp, a.base.maxHp, 1e-9, '鼓舞 gone');
|
||||
|
||||
@@ -457,7 +457,7 @@ test('号角 S3: cast with an enemy in range (DEFAULT, DESIGN §21.29); ATK +25
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised every 2 s; T1 orbiting motes ×1.5 aura heal; T2 −10 % from Sarkaz', () => {
|
||||
test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised every 2 s; T1 orbiting motes ×1.5 trait (生命回复速度); T2 −10 % from Sarkaz', () => {
|
||||
const sark = dummy('enemy_sark');
|
||||
sark.tags = ['sarkaz'];
|
||||
const h = makeBattle({
|
||||
@@ -469,17 +469,22 @@ test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised ev
|
||||
const a = h.unit('t_a'), b = h.unit('t_b'), c = h.unit('t_c');
|
||||
const bb = bbOf(u), t0 = tal(u, 0), t1 = tal(u, 1);
|
||||
const aura = () => u.s.atk * u.def.traitBb['attack@atk_to_hp_recovery_ratio'];
|
||||
// the trait: 生命回复速度 on the allies in range (an hpRegen buff — PRTS 分支特性信息 吟游者; professions.js bardRegen)
|
||||
const trait = (x) => x.findBuff(`trait:bard:${u.id}`)?.mods.hpRegen ?? 0;
|
||||
h.step();
|
||||
a.hp = 2000; b.hp = 8000; c.hp = 2000;
|
||||
h.run(1.05);
|
||||
// the 3 motes orbit at range_radius 1.15 (dynamic_spd 30°/s, 120° apart): none sits on the left neighbour at t = 1 s
|
||||
assert.ok(!a.hasBuff('cetsyr:mote'), 'motes orbit: not on the left neighbour yet');
|
||||
approx(a.hp - 2000, aura(), 1e-6, 'plain aura before the mote arrives');
|
||||
approx(b.hp - 8000, aura(), 1e-6, 'plain aura (2 tiles away)');
|
||||
approx(trait(a), aura(), 1e-6, 'plain trait before the mote arrives');
|
||||
approx(trait(b), aura(), 1e-6, 'plain trait (2 tiles away)');
|
||||
assert.ok(a.hp > 2000 && b.hp > 8000, 'regenerating');
|
||||
assert.ok(h.runUntil(() => a.hasBuff('cetsyr:mote'), 2), 'an orbiting mote reaches the adjacent operator');
|
||||
h.run(0.3);
|
||||
approx(trait(a), aura() * t0['attack@trait_mul'], 1e-6, 'mote ×1.5');
|
||||
const hpA = a.hp;
|
||||
h.run(1);
|
||||
approx(a.hp - hpA, aura() * t0['attack@trait_mul'], 1e-6, 'mote ×1.5');
|
||||
assert.ok(Math.abs(a.hp - hpA - aura() * t0['attack@trait_mul']) <= 1.5, `mote ×1.5: +${a.hp - hpA} in 1 s`);
|
||||
assert.ok(h.runUntil(() => c.hasBuff('cetsyr:mote'), 12), 'a diagonal neighbour (1.41 tiles) is on the orbit too');
|
||||
h.run(8);
|
||||
assert.ok(!fxOf(h, 'mote').some((e) => e[4].id === b.id), 'two tiles away: off the orbit');
|
||||
@@ -502,7 +507,7 @@ test('魔王 S3: inspire +65 % of her max HP to others in range, HP equalised ev
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3 no attack, range-wide sluggish, heals; T1 Supporter SP aura', () => {
|
||||
test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3 no attack, range-wide sluggish, 生命回复速度 (none in the first second); T1 Supporter SP aura', () => {
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_dummy: dummy() }, chess: { t_sup: chessRec({ id: 't_sup', profession: 'SUPPORT', stats: { atk: 0 } }), t_hurt: ally('t_hurt') } },
|
||||
units: [{ chessId: 'chess_char_5_10_a', row: 10, col: 4 }, { chessId: 't_sup', row: 12, col: 3 }, { chessId: 't_hurt', row: 11, col: 5 }],
|
||||
@@ -521,12 +526,21 @@ test('铃兰 T2 画地为牢 fragile 20 % on sluggish enemies (×1.4 in S3); S3
|
||||
hurt.hp = 5000;
|
||||
u.skill.gainSp(1000);
|
||||
assert.ok(h.runUntil(() => u.skill.active, 5));
|
||||
const lastAtk = u.lastAttackAt;
|
||||
h.run(2.1);
|
||||
const lastAtk = u.lastAttackAt, cast = h.b.time;
|
||||
// PRTS 技能3 备注: an hpRegen buff (no heal), 0 in the first second, its amount refreshed every second
|
||||
const fox = () => hurt.findBuff(`lisa:fox:${u.id}`);
|
||||
h.run(0.9);
|
||||
assert.equal(fox(), null, 'nothing in the first second');
|
||||
assert.equal(hurt.hp, 5000);
|
||||
h.run(1.2);
|
||||
assert.equal(u.lastAttackAt, lastAtk, 'no attacks during S3');
|
||||
assert.ok(e.s.flags && e.findBuff('sluggish'), 'enemies in range are sluggish');
|
||||
approx(e.s.dmgTakenMul, 1 + (t1.damage_scale - 1) * bb.scale_delta_to_one, 1e-9, 'T2 ×1.4');
|
||||
approx(hurt.hp - 5000, 2 * u.s.atk * bb['attack@atk_to_hp_recovery_ratio'], 1e-6, 'heals 9 % ATK/s');
|
||||
const v = u.s.atk * bb['attack@atk_to_hp_recovery_ratio'];
|
||||
approx(fox().mods.hpRegen, v, 1e-9, '生命回复速度 +9 % ATK');
|
||||
assert.ok(Math.abs(hurt.hp - 5000 - v * (h.b.time - cast - 1)) <= 2, `regenerated ${hurt.hp - 5000} from the first second on`);
|
||||
assert.ok(h.runUntil(() => !u.skill.active, 40));
|
||||
assert.equal(fox(), null, 'gone with the skill');
|
||||
clean(h);
|
||||
});
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ test('“小自在”: arts melee blocker, 25 s lifetime, kills emit summonKill
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('斯卡蒂的海嗣: heals allies in its range; during the owner skill: true dmg/s + 鼓舞; expires, then redeploys after respawnTime', REAL, () => {
|
||||
test('斯卡蒂的海嗣: its owner\'s trait (生命回复速度) on the allies in its range; during the owner skill: true dmg/s + 鼓舞; expires, then redeploys after respawnTime', REAL, () => {
|
||||
const bb = tokDef(TOKEN_IDS.seaborn, 'chess_char_6_04_a').skill.bb;
|
||||
const ratio = tokDef(TOKEN_IDS.seaborn, 'chess_char_6_04_a').traitBb['attack@atk_to_hp_recovery_ratio'];
|
||||
// heal mode (owner skill idle)
|
||||
@@ -130,8 +130,13 @@ test('斯卡蒂的海嗣: heals allies in its range; during the owner skill: tru
|
||||
g.hp = 1000;
|
||||
const sea = spawnOn(h, sk, TOKEN_IDS.seaborn, 10, 7);
|
||||
assert.equal(sea.s.flags.untargetable, true);
|
||||
h.run(3.05);
|
||||
approx(g.hp - 1000, 3 * sk.s.atk * ratio, 1e-6, 'heal 3 pulses');
|
||||
h.run(1.05);
|
||||
// an hpRegen buff keyed by the owner (one trait effect per ally — professions.js bardRegen), refreshed every second
|
||||
const v = sk.s.atk * ratio;
|
||||
approx(g.findBuff(`trait:bard:${sk.id}`)?.mods.hpRegen ?? 0, v, 1e-6, 'the owner\'s trait');
|
||||
const hp0 = g.hp, t1 = h.b.time;
|
||||
h.run(2);
|
||||
assert.ok(Math.abs(g.hp - hp0 - v * (h.b.time - t1)) <= 1.5, `regenerated ${g.hp - hp0}`);
|
||||
const life = tokDef(TOKEN_IDS.seaborn, 'chess_char_6_04_a').talents[0].bb.duration;
|
||||
h.runUntil(() => !sea.alive, life + 2);
|
||||
approx(sea.deathAt - sea.deployedAt, life, 0.01, 'lifetime');
|
||||
@@ -263,14 +268,14 @@ test('hand-authored summoner kits: the board 狼群 deploys before 伺夜 (one p
|
||||
assert.equal(packs.length, 1, 'a single pack');
|
||||
assert.equal(packs[0].uid, 2, 'the player\'s piece');
|
||||
assert.equal(vigil.trait.reinforcement, packs[0]);
|
||||
// a summoner with a hand kit (no skill here): the seaborn does not run its own heal pulses
|
||||
// a summoner with a hand kit (no skill here): the seaborn does not run its own trait pulses
|
||||
const h2 = makeBattle({ defs: { chess: { test_guard: guard() } }, kits: { chess_char_6_04_a: bare }, units: [{ chessId: 'chess_char_6_04_a', row: 12, col: 3 }, { chessId: 'test_guard', row: 10, col: 8 }], autoFinish: false, timeLimit: 30 });
|
||||
h2.step();
|
||||
const g = h2.unit('test_guard');
|
||||
g.hp = 1000;
|
||||
const sea = spawnOn(h2, h2.unit('chess_char_6_04_a'), TOKEN_IDS.seaborn, 10, 7);
|
||||
h2.run(3);
|
||||
assert.equal(g.hp, 1000, 'managed: no token-side heal');
|
||||
assert.equal(g.hp, 1000, 'managed: no token-side trait');
|
||||
assert.equal(sea.mem.expiresAt, undefined, 'managed: lifetime left to the owner kit');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user