feedback5 WL: Touch follows PRTS — S3's extra heal is 30 % of the base with its own recipient's boost, 「低于」 half HP, 攫升 on every heal she outputs, 超脱 on a dying forced exit
The shared code of the 外勤医疗 strategy's Touch and the Touch 补位 stand-in (content/tokens.js touchGospel / mapCharTalents), against PRTS Touch(卫戍协议) and its 备注 / 修正: - S3 恳切福音's extra heal: 30 % of the main heal's base (before the ×1.35 / ×1.40), a heal of its own — its recipient below half HP gets the boost (and the elite's PHY-X), 备注 「额外治疗可触发技能后半段的治疗量提高效果」; it goes to the lowest HP ratio of the main target and its 4 orthogonal neighbours, a full-HP unit included (x-5, 「可治疗生命值已满单位」). The touchExtra / touchMainHeal block is gone. - The boost needs HP strictly below half (修正 「低于」, was 「不高于」). - 攫升: every heal she outputs gives SP — a heal on herself and one that restores nothing too (备注 「无需实际产生治疗量」); still no device, no unit without a skill, and not her own HP-regeneration tick. - 超脱: a forced exit that plays the death animation counts like a knock-out (备注 「如史尔特尔的天赋效果」). Battle.retreat takes `dying` and the death hook carries it: 史尔特尔's 余烬 and 骑士戒律 + 竞技旗 set it; planned exits (耀骑士临光 S2, 伊内丝 S3), the 商人's automatic 撤退 and the 突袭 jump do not. A battle has no player 撤退 command. Golden moved: roster-041 (the strategy's Touch heals differently: S3 extra heal / boost), standin-03, standin-04, standin-08, standin-09, standin-10 (Touch standing in for 塞雷娅 / 塑心 / 纯烬艾雅法拉), coop2-NORMAL-14-standins (the human seat plays 外勤医疗, so its Touch takes part). Docs: SIM.md (death hook `dying`, retreat option, the forced-exit paragraph).
This commit is contained in:
1 parent
ce0d85c78a
commit
8b7316d93e
12 files changed
+244
-127
No files matched your search
@@ -24,6 +24,7 @@ const DEFS = {
|
||||
test_tank2_a: chessRec({ id: 'test_tank2_a', profession: 'TANK', stats: { maxHp: 100000, atk: 1, def: 0 }, skill: null }),
|
||||
// SP only from 攫升: an attack-SP skill and no enemy to attack
|
||||
test_sp_a: chessRec({ id: 'test_sp_a', profession: 'TANK', stats: { maxHp: 100000, atk: 1, def: 0 }, skill: { spType: 'INCREASE_WHEN_ATTACK', spCost: 1000, initSp: 0 } }),
|
||||
test_sp2_a: chessRec({ id: 'test_sp2_a', profession: 'TANK', stats: { maxHp: 1000, atk: 1, def: 0 }, skill: { spType: 'INCREASE_WHEN_ATTACK', spCost: 1000, initSp: 0 } }),
|
||||
test_medic_a: chessRec({ id: 'test_medic_a', profession: 'MEDIC', subProfessionId: 'physician', stats: { atk: 300 }, skill: null, rangeGrid: [[0, 0]] }),
|
||||
},
|
||||
};
|
||||
@@ -97,9 +98,9 @@ test('S3 恳切福音: cast with an injured ally inside the 5-2 only (no heal be
|
||||
h.run(0.5);
|
||||
const first = h.heals.filter((x) => x.t === h.heals[0].t);
|
||||
assert.ok(first[0].t >= cast, `${id}: the first heal comes with the cast`);
|
||||
assert.deepEqual(first.map((x) => x.uid), [2, 2], `${id}: the main heal on the far ally, then the extra on it (its neighbours are full)`);
|
||||
assert.deepEqual(first.map((x) => x.uid), [2, 2], `${id}: the main heal on the far ally, then the extra on it (the lowest HP ratio of it and its neighbours)`);
|
||||
close(first[0].amount, u.s.atk * phy * boost, `${id}: main heal ×${boost}${phy > 1 ? ' ×1.15 PHY-X' : ''}`);
|
||||
close(first[1].amount, first[0].amount * 0.3, `${id}: extra heal = 30 % of the main one`);
|
||||
close(first[1].amount, u.s.atk * 0.3 * phy * boost, `${id}: extra heal = 30 % of the main heal's base, its still-low recipient's bonuses on it`);
|
||||
// two injured allies: both healed per action
|
||||
h.b.loseHp(near, near.hp * 0.2);
|
||||
const n0 = h.heals.length;
|
||||
@@ -121,6 +122,41 @@ test('S3 恳切福音: cast with an injured ally inside the 5-2 only (no heal be
|
||||
}
|
||||
});
|
||||
|
||||
test('S3 extra heal (PRTS 备注): 30 % of the main heal\'s base, its own recipient\'s ×heal_scale (and PHY-X); the lowest HP ratio of the main target and its 4 neighbours; exactly half HP is not 「低于」', () => {
|
||||
for (const id of ['chess_char_5_11_a', 'chess_char_6_09_b']) {
|
||||
const phy = moduleOf(id) ? 1.15 : 1;
|
||||
const boost = skillOf(id, S3).bb.heal_scale;
|
||||
// the main target on (10,8) (inside the 5-2, outside her 3-3); its neighbour (10,9) is beyond the 5-2 (never a main target)
|
||||
const firstAction = (mainRatio, nearRatio) => {
|
||||
const h = battle({ id, carry: { sp: 999 } }, [['test_tank_a', 10, 8, mainRatio], ['test_tank2_a', 10, 9, nearRatio]]);
|
||||
const u = h.unit(1);
|
||||
assert.ok(h.runUntil(() => h.heals.length >= 2, 5), `${id}: healed`);
|
||||
assert.ok(u.skill.active, `${id}: S3 running`);
|
||||
const t0 = h.heals[0].t;
|
||||
const act = h.heals.filter((x) => x.t === t0);
|
||||
assert.equal(act.length, 2, `${id}: one main heal and one extra heal`);
|
||||
checkInvariants(h.b);
|
||||
return { u, main: act[0], extra: act[1] };
|
||||
};
|
||||
// main above half: no bonus; the extra goes to the neighbour below half (lower ratio), with both bonuses
|
||||
let r = firstAction(0.55, 0.45);
|
||||
assert.deepEqual([r.main.uid, r.extra.uid], [2, 3], `${id}: the extra on the lower-ratio neighbour`);
|
||||
close(r.main.amount, r.u.s.atk, `${id}: no bonus at 55 %`);
|
||||
close(r.extra.amount, r.u.s.atk * 0.3 * boost * phy, `${id}: the extra takes its own recipient's ×${boost}`);
|
||||
// main at exactly half: "低于" — no ×heal_scale, no PHY-X; the extra on it (its neighbour is full), no bonus either
|
||||
r = firstAction(0.5, 1);
|
||||
assert.deepEqual([r.main.uid, r.extra.uid], [2, 2], `${id}: the full neighbour has the higher ratio`);
|
||||
close(r.main.amount, r.u.s.atk, `${id}: exactly 50 % is not below half`);
|
||||
close(r.extra.amount, r.u.s.atk * 0.3, `${id}: extra = 30 % of the base`);
|
||||
// main just below half: boosted; the extra lands on it after it crossed half — 30 % of the base, not of the boosted heal
|
||||
r = firstAction(0.499, 1);
|
||||
assert.deepEqual([r.main.uid, r.extra.uid], [2, 2]);
|
||||
close(r.main.amount, r.u.s.atk * boost * phy, `${id}: ×${boost} below half`);
|
||||
assert.ok(r.extra.ratio >= 0.5, `${id}: its recipient is above half now`);
|
||||
close(r.extra.amount, r.u.s.atk * 0.3, `${id}: the extra's base excludes the main heal's bonuses`);
|
||||
}
|
||||
});
|
||||
|
||||
test('S1 慨赠: cast on an injured ally in her range; 20 s of ATK +40 % / +50 %, each heal a 40 % / 50 % chance of a second heal of the same ally', () => {
|
||||
for (const [id, atk, p] of [['chess_char_5_11_a', 0.4, 0.4], ['chess_char_5_11_b', 0.5, 0.5]]) {
|
||||
assert.deepEqual([skillOf(id, S1).bb.atk, skillOf(id, S1).bb['attack@prob'], skillOf(id, S1).duration], [atk, p, 20]);
|
||||
@@ -187,9 +223,9 @@ test('S2 宛如天启: cast with an injured ally inside the 3-10 only; 30 s on t
|
||||
assert.equal(h.unit(1).skill.activations, 0);
|
||||
});
|
||||
|
||||
test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an operator knocked out in her range +5 SP (+8 with PHY-X Lv3)', () => {
|
||||
test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an operator knocked out — or forced out — in her range +5 SP (+8 with PHY-X Lv3)', () => {
|
||||
for (const [id, sp] of [['chess_char_5_11_a', 5], ['chess_char_5_11_b', 5], ['chess_char_6_20_b', 8]]) {
|
||||
const h = battle({ id }, [['test_sp_a', 10, 5, 0.6], ['test_tank_a', 9, 4, 1], ['test_tank2_a', 9, 10, 1]]);
|
||||
const h = battle({ id }, [['test_sp_a', 10, 5, 0.6], ['test_tank_a', 9, 4, 1], ['test_tank2_a', 9, 10, 1], ['test_tank_a', 11, 4, 1], ['test_tank2_a', 11, 5, 1], ['test_tank_a', 9, 5, 1], ['test_tank2_a', 11, 6, 1]]);
|
||||
const u = h.unit(1), ally = h.unit(2);
|
||||
const gains = [];
|
||||
h.b.on('spGain', (c) => { if (c.unit === ally) gains.push([c.reason, c.amount]); });
|
||||
@@ -203,6 +239,60 @@ test('攫升 / 超脱 (the strategy\'s mapCharTalents): a healed unit +3 SP; an
|
||||
const mid = u.skill.sp;
|
||||
h.b.kill(h.unit(4)); // (9,10): out of it
|
||||
close(u.skill.sp, mid, `${id}: not out of range`);
|
||||
// PRTS 备注: a forced exit with a death animation (史尔特尔's 余烬: a `dying` retreat) counts; not a skill's planned
|
||||
// retreat (耀骑士临光 S2, 伊内丝 S3: a plain 'retreat'), the 商人's automatic 撤退 ('merchant') or the 突袭 jump ('raid')
|
||||
h.b.retreat(h.unit(5), { reason: 'retreat', dying: true }); // (11,4)
|
||||
close(u.skill.sp, mid + sp, `${id}: 超脱 on a dying forced exit`);
|
||||
h.b.retreat(h.unit(6), { reason: 'retreat' }); // (11,5)
|
||||
h.b.retreat(h.unit(7), { reason: 'merchant' }); // (9,5)
|
||||
h.b.retreat(h.unit(8), { reason: 'raid' }); // (11,6)
|
||||
close(u.skill.sp, mid + sp, `${id}: not on a planned, a 商人's or a 突袭 retreat`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('超脱 on 史尔特尔\'s 余烬 exit (PRTS 备注 "如史尔特尔的天赋效果"): her forced exit 8 s after the lethal blow gives Touch the SP', () => {
|
||||
const id = 'chess_char_5_11_a', sp = 5;
|
||||
const h = battle({ id }, [['chess_char_5_07_a', 9, 4, 1]]);
|
||||
const u = h.unit(1), surtr = h.unit(2);
|
||||
const exits = [];
|
||||
h.b.on('death', (c) => { if (c.unit === surtr) exits.push([c.reason, c.dying]); });
|
||||
h.b.dealDamage(null, surtr, { amount: 1e7, type: 'true' });
|
||||
assert.ok(surtr.alive && surtr.findBuff('surtr:ember'), '余烬: kept alive');
|
||||
const before = u.skill.sp;
|
||||
const gains = [];
|
||||
h.b.on('spGain', (c) => { if (c.unit === u && c.reason === 'talent') gains.push(c.amount); });
|
||||
assert.ok(h.runUntil(() => !surtr.alive, 12), 'she withdraws');
|
||||
assert.deepEqual(exits, [['retreat', true]], 'a dying forced exit');
|
||||
assert.deepEqual(gains, [sp], '超脱');
|
||||
assert.ok(u.skill.sp >= before + sp - 1e-6);
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('攫升 (PRTS 备注 "只需Touch输出治疗便能触发(无需实际产生治疗量)"): her heal on herself and a heal that restores nothing (S3\'s extra heal on a full unit) give SP too', () => {
|
||||
for (const id of ['chess_char_5_11_a', 'chess_char_6_09_b']) {
|
||||
// herself: injured, she heals herself (her 3-3 holds her own tile)
|
||||
let h = battle({ id }, []);
|
||||
let u = h.unit(1);
|
||||
h.b.loseHp(u, u.hp * 0.4);
|
||||
const own = [];
|
||||
h.b.on('spGain', (c) => { if (c.unit === u && c.reason === 'talent') own.push(c.amount); });
|
||||
assert.ok(h.runUntil(() => h.heals.length > 0, 5), `${id}: a self-heal`);
|
||||
h.step();
|
||||
assert.equal(h.heals[0].uid, 1);
|
||||
assert.deepEqual(own, [3], `${id}: 攫升 on her own heal`);
|
||||
checkInvariants(h.b);
|
||||
// S3: the main heal fills a 1000-HP unit at 90 %, then the extra heal lands on it at full HP — still +3 SP
|
||||
h = battle({ id, carry: { sp: 999 } }, [['test_sp2_a', 10, 8, 0.9]]);
|
||||
u = h.unit(1);
|
||||
const tgt = h.unit(2);
|
||||
const got = [];
|
||||
h.b.on('spGain', (c) => { if (c.unit === tgt && c.reason === 'talent') got.push(c.amount); });
|
||||
assert.ok(h.runUntil(() => h.heals.length >= 2, 5), `${id}: S3 heals`);
|
||||
h.step();
|
||||
assert.ok(u.skill.active);
|
||||
assert.deepEqual(h.heals.slice(0, 2).map((x) => [x.uid, x.ratio === 1]), [[2, false], [2, true]], `${id}: the extra heal on the now full unit`);
|
||||
assert.deepEqual(got, [3, 3], `${id}: 攫升 for the main heal and for the extra heal that restored nothing`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -495,7 +495,7 @@ test('炎佑: spawnYanyou — flying ally, bond stats, 3 targets with burn + ele
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage slot; Touch 恳切福音 heals ×heal_scale on ≤50 % HP allies', REAL, () => {
|
||||
test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage slot; Touch 恳切福音 heals ×heal_scale on allies below 50 % HP', REAL, () => {
|
||||
const g = guard({ stats: { maxHp: 10000 } });
|
||||
const h = makeBattle({ stageId: 'act2autochess_m01', defs: { chess: { test_guard: g } }, units: [{ chessId: 'test_guard', row: 10, col: 3 }], autoFinish: false, timeLimit: 60,
|
||||
setup: (b) => b.on('heal', (c) => { if (c.source?.defId === TOKEN_IDS.touch) (b.mem ??= []).push({ amount: c.amount, ratio: c.target.hpRatio, active: c.source.skill.active }); }, { priority: -500 }) });
|
||||
@@ -515,7 +515,7 @@ test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage sl
|
||||
gu.hp = 3000;
|
||||
h.run(4);
|
||||
const bb = touch.skill.bb;
|
||||
const low = h.b.mem.find((x) => x.active && x.ratio <= bb.hp_ratio);
|
||||
const low = h.b.mem.find((x) => x.active && x.ratio < bb.hp_ratio);
|
||||
assert.ok(low, 'healed a low ally');
|
||||
approx(low.amount, touch.s.atk * bb.heal_scale, 1e-6, 'boosted heal');
|
||||
checkInvariants(h.b);
|
||||
@@ -1090,7 +1090,7 @@ test('预备干员-医疗: stat talent 攻击提升 (+4 % ATK) on top of the dat
|
||||
approx(med.s.atk, tokDef(TOKEN_IDS.reserveMedic).stats.atk * (1.04 + 0.5), 1e-9, '治疗强化·β型 +50 %');
|
||||
});
|
||||
|
||||
test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an operator in range is knocked out; extra heal = 30 % of the main heal', REAL, () => {
|
||||
test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an operator in range is knocked out; extra heal = 30 % of the main heal\'s base, ×heal_scale on its own low recipient', REAL, () => {
|
||||
const g = chessRec({ id: 'test_sp', stats: { maxHp: 10000, atk: 0, spRecovery: 0 }, skill: { spCost: 100, initSp: 0 } });
|
||||
const h = makeBattle({ stageId: 'act2autochess_m01', defs: { chess: { test_sp: g } }, units: [{ chessId: 'test_sp', row: 10, col: 3, uid: 1 }, { chessId: 'test_sp', row: 11, col: 3, uid: 2 }], autoFinish: false, timeLimit: 60,
|
||||
setup: (b) => b.on('heal', (c) => { if (c.source?.defId === TOKEN_IDS.touch) (b.mem ??= []).push({ t: c.target.id, amount: c.amount, ratio: c.target.hpRatio }); }, { priority: -500 }) });
|
||||
@@ -1105,14 +1105,17 @@ test('Touch talents: 攫升 +3 SP to the healed unit, 超脱 +5 SP when an opera
|
||||
h.b.dealDamage(null, b2, { amount: 1e6, type: 'true' });
|
||||
assert.equal(b2.alive, false);
|
||||
approx(touch.skill.sp - sp0, 5, 0.1, '超脱');
|
||||
// skill: main heal (×1.5 at ≤ 50 %) + extra 30 % of it, not boosted again
|
||||
// skill: main heal (×1.5 below 50 %) + an extra heal of 30 % of its base (ATK) on the lowest-ratio unit of the target and
|
||||
// its neighbours — the target again, still below half after the main heal: ×1.5 on its own (PRTS 技能3 备注)
|
||||
h.b.mem = [];
|
||||
touch.skill.activate('test', { free: true });
|
||||
a.hp = 4000;
|
||||
a.hp = 2000;
|
||||
assert.ok(h.runUntil(() => h.b.mem.length >= 2, 5));
|
||||
const [main, extra] = h.b.mem;
|
||||
assert.deepEqual([main.t, extra.t], [a.id, a.id]);
|
||||
approx(main.amount, touch.s.atk * 1.5, 1e-6, 'main heal boosted');
|
||||
approx(extra.amount, main.amount * 0.3, 1e-6, 'extra = 30 % of the main heal');
|
||||
assert.ok(extra.ratio < 0.5);
|
||||
approx(extra.amount, touch.s.atk * 0.3 * 1.5, 1e-6, 'extra = 30 % of the base, boosted on its low recipient');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user