feedback5 WE1: 庇护 is one effect per unit — 宴, 余, 缪尔赛思, 赫拉格, 左乐 and 赫德雷 join the shared key (follow-up 18)
gamedata_const ba.protect / PRTS 术语 庇护 "受到的物理和法术伤害降低相应比例(同名效果取最高)": the strongest source holds; the
自选 kits already used battle.applyStrongest('protect'), these six multiplied private cuts with it.
- kits/shared/tier1.js: PROTECT, protectMods, PROTECT_TICK_HOLD and holdProtect (applyStrongest on the shared key).
- 宴 / 赫拉格 / 左乐 (below their HP threshold): refreshed every tick and at each hit on them; 余 (blocking) and 缪尔赛思's
melee copies at their old cadence; 赫德雷's 庇护 joins the key, his CRU-X 物理伤害 + becomes its own instance. 左乐 keeps the
PRTS S2 ordering (his own 庇护 newer than his barrier cuts behind it); 行险 drops his own held 庇护 for its 0.1 s
re-judgement [ASSUMED: another source's returns with its next refresh]. 遥's bubbles stay separate (the client's
damage_resistance[bonus] multiplies with the common damage_resistance[inf]).
Golden: no scenario moved — no unit of the corpus holds two 庇护 at once, and one source alone cuts exactly as before.
Tests: test/content/feedback5-protect.test.js (new, 2); kits_t6 (余), kits_alt_t6 (缪尔赛思), op_hodrer updated to the
shared key; kits_t1t2, op_helage, op_zuole pass. Docs: SIM.md §3, kits/README.md checklist 9.
This commit is contained in:
1 parent
a7d62b5fc1
commit
cd180bc752
13 files changed
+161
-37
No files matched your search
@@ -0,0 +1,81 @@
|
||||
// test/content/feedback5-protect.test.js — 庇护 is one effect per unit whoever grants it (gamedata_const ba.protect / PRTS 术语
|
||||
// 庇护 "受到的物理和法术伤害降低相应比例(同名效果取最高)"): the older kits — 宴, 余, 缪尔赛思 (chess), 赫拉格, 左乐, 赫德雷 (自选) —
|
||||
// hold it through the shared key the other 自选 kits use (kits/shared/tier1.js PROTECT / holdProtect → battle.applyStrongest),
|
||||
// so the strongest value applies instead of a product of every source. 遥's bubbles keep their own key (the client's
|
||||
// damage_resistance[bonus], which multiplies with the common damage_resistance[inf]).
|
||||
// Run: node --test test/content/feedback5-protect.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { PROTECT, holdProtect } from '../../server/sim/content/kits/shared/tier1.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const approx = (a, b, msg, eps = 1e-6) => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg}: ${a} vs ${b}`);
|
||||
const done = (h) => { checkInvariants(h.b); assert.equal(h.b.errors.length, 0, JSON.stringify(h.b.errors[0])); };
|
||||
|
||||
/** 宴 (elite: 庇护 below hp_ratio) at 20 % HP with nothing to hit her. */
|
||||
function utage() {
|
||||
const h = makeBattle({ units: [{ chessId: 'chess_char_1_18_b', row: 9, col: 5 }], autoFinish: false, timeLimit: 60, seed: 3 });
|
||||
h.step();
|
||||
const u = h.unit('chess_char_1_18_b');
|
||||
u.hp = u.s.maxHp * 0.2;
|
||||
h.step();
|
||||
return { h, u, dr: ds.rawChess('chess_char_1_18_b').talents.filter((t) => t.index !== -1)[0].bb.damage_resistance };
|
||||
}
|
||||
/** Physical damage she takes from a 1000-point hit (her DEF taken off). */
|
||||
const physTaken = (h, u) => { const hp = u.hp; h.b.dealDamage(null, u, { amount: 1000, type: 'phys', canDodge: false }); const d = hp - u.hp; u.hp = hp; return d; };
|
||||
|
||||
test('宴 below her threshold holds the shared 庇护; a stronger one from another source replaces it (no product), a weaker one waits under it', () => {
|
||||
const { h, u, dr } = utage();
|
||||
assert.ok(dr > 0 && dr < 0.5);
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === dr, 'her own 庇护, on the shared key');
|
||||
const base = 1000 - u.s.def;
|
||||
approx(physTaken(h, u), base * (1 - dr), 'hers alone');
|
||||
// a stronger 庇护 from another source: the highest counts
|
||||
holdProtect(h.b, u, 0.5, 10, { id: 'other' });
|
||||
h.step();
|
||||
approx(physTaken(h, u), base * (1 - 0.5), 'the stronger one only — not ×(1 − dr) as well');
|
||||
assert.equal(u.buffs.filter((x) => x.key === PROTECT).length, 1, 'one 庇护 effect');
|
||||
done(h);
|
||||
// a weaker one: hers holds
|
||||
const w = utage();
|
||||
holdProtect(w.h.b, w.u, 0.1, 10, { id: 'other' });
|
||||
w.h.step();
|
||||
approx(physTaken(w.h, w.u), (1000 - w.u.s.def) * (1 - w.dr), 'hers — the weaker 庇护 waits');
|
||||
// back above the threshold her own lapses within a tick; the weaker one resumes for its remaining time
|
||||
w.u.hp = w.u.s.maxHp;
|
||||
w.h.step(3);
|
||||
const after = w.u.findBuff(PROTECT);
|
||||
assert.ok(after && after.data.value === 0.1, 'the weaker 庇护 resumed');
|
||||
done(w.h);
|
||||
});
|
||||
|
||||
test('余 (blocking), 缪尔赛思\'s melee copies, 赫拉格 / 左乐 (SBL-X, below 50 %) and 赫德雷 (余火之氅) all hold 庇护 on the shared key', () => {
|
||||
const src = (f) => readFileSync(new URL(`../../server/sim/content/kits/ops/${f}`, import.meta.url), 'utf8');
|
||||
for (const f of ['chess_char_1_18-utage.js', 'chess_char_6_03-yu.js', 'chess_char_6_11-mlyss.js', 'op-helage.js', 'op-zuole.js', 'op-hodrer.js']) {
|
||||
assert.match(src(f), /holdProtect\(/, `${f} uses the shared 庇护`);
|
||||
assert.doesNotMatch(src(f), /physTakenMul: 1 - dr|dmg\.mul \*= 1 - (dr|protectCut|num\(t\.damage_resistance\))/, `${f}: no private 庇护 multiplier left`);
|
||||
}
|
||||
// 遥's bubble stays its own effect (damage_resistance[bonus])
|
||||
assert.doesNotMatch(src('op-haruka.js'), /holdProtect\(/);
|
||||
// 赫拉格 (SBL-X, tier 5) at 40 % HP: his 庇护 on the shared key
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { e: enemyRec({ key: 'e', hp: 1e9, speed: 0 }) } },
|
||||
units: [{ diy: { slot: 5, charId: 'char_188_helage', skillIndex: 0, uniEquipId: 'uniequip_002_helage' }, elite: true, row: 10, col: 5 }],
|
||||
autoFinish: false, timeLimit: 30, seed: 3,
|
||||
});
|
||||
h.step();
|
||||
const u = h.b.allyUnits.find((x) => x.def?.charId === 'char_188_helage');
|
||||
u.hp = u.s.maxHp * 0.4;
|
||||
h.step();
|
||||
const b = u.findBuff(PROTECT);
|
||||
assert.ok(b && b.source === u && b.data.value === 0.25, 'his SBL-X 庇护');
|
||||
u.hp = u.s.maxHp;
|
||||
h.step(3);
|
||||
assert.equal(u.findBuff(PROTECT), null, 'gone above 50 %');
|
||||
done(h);
|
||||
});
|
||||
@@ -708,7 +708,11 @@ test('6_11 缪尔赛思 S2 生态耦合: +cost DP; melee copies regenerate and t
|
||||
const eco = tok().findBuff('mlyss:eco');
|
||||
assert.ok(eco, 'melee copy buffed');
|
||||
approx(eco.mods.hpRegenRatio, bb.hp_recovery_per_sec_by_max_hp_ratio);
|
||||
approx(eco.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
// 庇护: the shared effect of every source (同名效果取最高 — kits/shared/tier1.js holdProtect)
|
||||
const protect = tok().findBuff('protect');
|
||||
assert.ok(protect && protect.source === u, 'the 庇护 she grants');
|
||||
approx(protect.mods.physTakenMul, 1 - bb.damage_resistance);
|
||||
approx(protect.mods.artsTakenMul, 1 - bb.damage_resistance);
|
||||
done(h);
|
||||
|
||||
// ranged copy: a sniper to copy
|
||||
|
||||
@@ -281,8 +281,8 @@ test('余: blocked enemies take 40 % ATK arts + 12 % burn per second, 庇护 whi
|
||||
assert.ok(dot.length >= 2);
|
||||
const burn = h.dmg.filter((d) => d.src === u.id && d.element === 'burn');
|
||||
assert.ok(burn.length >= 2);
|
||||
// 庇护 (ba.protect): physical and arts damage only
|
||||
assert.ok(u.findBuff('yu:shelter') && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
// 庇护 (ba.protect): physical and arts damage only — the shared effect of every source (kits/shared/tier1.js holdProtect)
|
||||
assert.ok(u.findBuff('protect')?.source === u && near(u.s.physTakenMul, 1 - t0.damage_resistance) && near(u.s.artsTakenMul, 1 - t0.damage_resistance));
|
||||
assert.equal(u.s.trueTakenMul, 1, '庇护 does not reduce true damage');
|
||||
assert.equal(u.s.dmgTakenMul, 1);
|
||||
const base = u.base.maxHp;
|
||||
|
||||
@@ -20,7 +20,8 @@ const FORMS = BACKUPS.units[HOD].forms;
|
||||
const SLOT = { 5: 'chess_char_5_diy1_a', 6: 'chess_char_6_diy1_a' };
|
||||
const CRUX = 'uniequip_002_hodrer', CRUY = 'uniequip_003_hodrer';
|
||||
const S1 = 'skchr_hodrer_1', S2 = 'skchr_hodrer_2', S3 = 'skchr_hodrer_3';
|
||||
const PROTECT = 'talent:hodrer:protect';
|
||||
const PROTECT = 'protect'; // the shared 庇护 of every source (kits/shared/tier1.js holdProtect)
|
||||
const DMG_UP = 'talent:hodrer:dmgUp'; // CRU-X stage 3's 物理伤害 + (its own one instance per ally)
|
||||
const formOf = (tier, elite) => FORMS[elite ? (tier === 5 ? '2/60/7/1' : '2/60/7/3') : '2/1/4/0'];
|
||||
const skillOf = (tier, elite, id) => formOf(tier, elite).skills.find((s) => s.skillId === id);
|
||||
const modOf = (tier, mod) => (mod ? formOf(tier, true).modules.find((m) => m.uniEquipId === mod) : null);
|
||||
@@ -173,7 +174,7 @@ test('T2 余火之氅: he and the ally on the tile behind him get 18 % 庇护 (C
|
||||
assert.ok(m, `${label(f)}: ${a.def.charId} protected`);
|
||||
approx(m.physTakenMul, 1 - dr, `${label(f)}: phys`);
|
||||
approx(m.artsTakenMul, 1 - dr, `${label(f)}: arts`);
|
||||
assert.equal(m.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
assert.equal(a.findBuff(DMG_UP)?.mods.physDealtMul ?? 1, x3 ? 1.1 : 1, `${label(f)}: 物理伤害`);
|
||||
}
|
||||
assert.equal(beside.findBuff(PROTECT), null, `${label(f)}: not beside him`);
|
||||
const hp = behind.hp;
|
||||
|
||||
Reference in new issue
Block a user