feedback5 WV: the six 自选 kits that kept their own 庇护 copies (淬羽赫默, 黍, 斥罪, 森蚺, 娜斯提: PROTECT / protectMods / applyStrongest; 遥: protectMods for her separate bubble key) use the shared tier1.js PROTECT / protectMods / holdProtect, with a guard test — a pure refactor, golden unchanged

This commit is contained in:
sganggs committed 2026-10-06 18:19:26 +08:00
1 parent dfb528802e
commit 5a11926003
7 files changed
+25 -30

No files matched your search

+1 -2
View File
@@ -47,7 +47,7 @@
// invulnerable / untargetable one) and while it stays levitated takes atk_scale × her ATK arts damage per second (the
// first after 0.5 s, then every `interval` s; 持续伤害, not dodgeable; 无来源 at her last ATK once she left).
import { num, up, traitBb, talentBb, skillRec, batMod, giveSp } from '../shared/tier1.js';
import { num, up, traitBb, talentBb, skillRec, batMod, giveSp, protectMods } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
@@ -58,7 +58,6 @@ const S3 = 'skchr_haruka_3';
export const BUBBLE = 'haruka:bubble';
/** The S3 levitation damage (haruka_s_3_debuff). */
export const FLOAT_DOT = 'haruka:float';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** 浮光泡影: the break delay when the talent carries no `interval`. */
const BREAK_DELAY = 0.5;
/** S3's levitation damage: the first tick (haruka_s_3_debuff firstTriggerInterval). */
+2 -5
View File
@@ -47,7 +47,7 @@
// - S3 披荆斩棘 (MANUAL, hurt SP; data DEFAULT, rawRule TAKE_DAMAGE): at once barrier +hp_ratio × max HP (topping T1's,
// capped), then `duration` s ATK +atk, attack interval +base_attack_time s (a flat +0.9 on 1.6 s), taunt +taunt_level.
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid } from '../shared/tier1.js';
import { num, traitBb, talentBb, skillRec, toggleBuff, batMod, up, enemiesInGrid, holdProtect, PROTECT } from '../shared/tier1.js';
import { isHpLoss } from '../../../damage.js';
const S1 = 'skchr_judge_1';
@@ -60,9 +60,6 @@ const X4 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1,
/** judge_s_2[aoe]: firstTriggerInterval 0.9 s, then triggerInterval 1 s. */
const S2_FIRST = 0.9;
const S2_EVERY = 1;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const bbOf = (chess, id) => skillRec(chess, id)?.bb ?? {};
const tagged = (d, tag) => !!(d && Array.isArray(d.tags) && d.tags.includes(tag));
@@ -79,7 +76,7 @@ export default {
/** S2's 庇护 (damage_resistance[inf] for the skill's time). */
const protect = (battle, unit, skill) => {
const dr = num(b2.damage_resistance);
if (dr > 0) battle.applyStrongest(unit, PROTECT, { duration: Math.max(0.05, skill.timeLeft), value: dr, mods: protectMods, source: unit });
holdProtect(battle, unit, dr, Math.max(0.05, skill.timeLeft), unit); // 庇护: the shared effect (同名效果取最高)
};
/** judge_t_1's shield_scale: 1, + S2's shield_scale while S2 runs. */
const shieldScale = (unit) => 1 + (unit.skill?.active && unit.skill.id === S2 ? num(b2.shield_scale) : 0);
+3 -6
View File
@@ -71,7 +71,7 @@
// [ASSUMED] (beyond op-ironmn.js's): the 监工专员 loses its 1 HP per pulse with or without an operator in range (dec_blood is a
// passive buff of the ability); the combined range one step deep; the 小工程师's 1 s heal / SP timer runs from its deployment.
import { num, talentBb, traitBb, skillRec, up, giveSp } from '../shared/tier1.js';
import { num, talentBb, traitBb, skillRec, up, giveSp, holdProtect } from '../shared/tier1.js';
import { absoluteRangeKeys } from '../../../targeting.js';
import { COLS } from '../../../constants.js';
import { frontOf } from '../../../dir.js';
@@ -86,9 +86,6 @@ export const NSTBLD = 'token_10061_nasti_nstbld';
const DEVICES = new Set([NSTDEF, NSTCHR, NSTBLD]);
/** A device's 1-1 when its def carries none. */
const FRONT1 = Object.freeze([[0, 0], [0, 1]]);
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest; op-judge.js, op-zumama.js). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** 注意安全's 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const T2_PERIOD = 0.25;
const PROTECT_HOLD = T2_PERIOD + 0.05;
@@ -402,13 +399,13 @@ export default {
if (!up(unit)) { triggered = false; timers.clear(); return; }
if (!triggered) triggered = battle.enemies.some((e) => e.alive && !e.hidden && RANGED_WAYS.has(String(e.def?.applyWay ?? '').toUpperCase()));
if (!triggered) {
if (resBase > 0) battle.applyStrongest(unit, PROTECT, { duration: PROTECT_HOLD, value: resBase, mods: protectMods, source: unit });
holdProtect(battle, unit, resBase, PROTECT_HOLD, unit); // 庇护: the shared effect (同名效果取最高)
return;
}
const who = [unit, ...battle.allyUnits.filter((a) => a !== unit && up(a) && !a.hidden && a.kind === 'op' && RANGED_POS.has(String(a.def?.position ?? '').toUpperCase()))];
for (const a of [...timers.keys()]) if (!who.includes(a)) timers.delete(a);
for (const a of who) {
if (resPlus > 0) battle.applyStrongest(a, PROTECT, { duration: PROTECT_HOLD, value: resPlus, mods: protectMods, source: unit });
holdProtect(battle, a, resPlus, PROTECT_HOLD, unit);
if (!(spEvery > 0)) continue;
if (!timers.has(a)) { timers.set(a, battle.time + spEvery); continue; }
if (battle.time + 1e-9 < timers.get(a)) continue;
+2 -5
View File
@@ -54,7 +54,7 @@
// 单位时释放技能"), which tools/build-data.mjs (TRIGGER_ALLY_RULES) writes as the engine's SKILL_RANGE + `allies` on the
// skill range x-2 (an injured, healable allied unit there, herself included; 古米 S1's option).
import { num, traitBb, skillRec, batMod, up, giveSp } from '../shared/tier1.js';
import { num, traitBb, skillRec, batMod, up, giveSp, holdProtect } from '../shared/tier1.js';
import { COLS } from '../../../constants.js';
import { effectiveProfile, acquireTargets, performAttack } from '../../../ai.js';
@@ -80,9 +80,6 @@ const PROF_NEEDED = 3;
const SUI_NEEDED = 4;
/** 【岁】: character_table groupId 'sui' (data/backups.json diy.operators powers carry it for the 自选 picks). */
const SUI = new Set(['char_2014_nian', 'char_2015_dusk', 'char_2023_ling', 'char_2024_chyue', 'char_2025_shu', 'char_2026_yu', 'char_2027_wang']);
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
const FARM = 'talent:shu:farm';
const HARVEST = 'skill:shu:harvest';
@@ -122,7 +119,7 @@ export default {
if (hp > 0 && !(cur && cur.source !== unit && (cur.data?.v ?? 0) > hp && cur.timeLeft > 0.05)) {
battle.addBuff(a, { key: FARM, duration: FARM_HOLD, mods: { hpRegen: hp }, source: unit, data: { v: hp }, tags: ['talent'] });
}
if (dr > 0) battle.applyStrongest(a, PROTECT, { duration: FARM_HOLD, value: dr, mods: protectMods, source: unit });
holdProtect(battle, a, dr, FARM_HOLD, unit); // 庇护: the shared effect (同名效果取最高)
}
};
/** Sow the tiles of `grid` around (r, c) — only tiles that can be deployed on or walked. Returns the new ones. */
+3 -6
View File
@@ -59,7 +59,7 @@
// held like the hammer's (PRIO_UNDYING_HELD −99).
// Statuses: none applied to enemies. Damage: arts normal attacks only.
import { num, up, traitBb, moduleBb, talentBb, skillRec, once } from '../shared/tier1.js';
import { num, up, traitBb, moduleBb, talentBb, skillRec, once, holdProtect } from '../shared/tier1.js';
import { releaseSkillSummon } from '../../tokens.js';
import { holdsUndying, PRIO_UNDYING_HELD, PRIO_REVIVE, PRIO_RESPAWN } from '../../items/battle.js';
import { absoluteRangeKeys } from '../../../targeting.js';
@@ -69,9 +69,6 @@ const S1 = 'skchr_slent2_1';
const S2 = 'skchr_slent2_2';
const S3 = 'skchr_slent2_3';
export const LAMP = 'token_10029_slent2_protrb';
/** 庇护 (ba.protect, the client's damage_resistance[inf]): one "同名效果取最高" effect per unit (battle.applyStrongest). */
export const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** The client's trigger period of slent2_t_1 / slent2_t_2 (talent abilities, triggerInterval 0.1). */
export const TALENT_IV = 0.1;
/** Lifetime of a refreshed effect: a little longer than the refresh period, so it never lapses while it holds. */
@@ -136,7 +133,7 @@ function lampKit(t, owner, t1) {
const keys = new Set(absoluteRangeKeys(grid, lamp.tileR, lamp.tileC, lamp.dir, 0));
for (const a of alliesOn(battle, lamp, keys)) {
const v = shelterOf(tb, a, scale);
if (v > 0) battle.applyStrongest(a, PROTECT, { duration: HOLD, value: v, mods: protectMods, source: lamp });
holdProtect(battle, a, v, HOLD, lamp); // 庇护 (the client's damage_resistance[inf]): the shared effect
}
}, { owner: lamp, immediate: true });
},
@@ -194,7 +191,7 @@ export default {
const now = new Set(allies);
for (const a of allies) {
const v = shelterOf(t1, a, scale);
if (v > 0) battle.applyStrongest(a, PROTECT, { duration: HOLD, value: v, mods: protectMods, source: unit });
holdProtect(battle, a, v, HOLD, unit);
const mult = isRhine(a) ? 2 : 1;
if (maxHp > 0) battle.applyStrongest(a, MAXHP_KEY, { duration: HOLD, value: maxHp * mult, mods: (x) => ({ hpPct: x }), source: unit });
if (regenRatio > 0 && a.hpRatio < regenBelow - 1e-9) {
+2 -5
View File
@@ -42,7 +42,7 @@
// she is stunned `stun` s (zumama_s_3 ON_BUFF_FINISH 'stun'; a stunned operator blocks nobody).
// - Melee physical, ground-only (data canHitFly false), block 1, range 1-1; ground enemies target her (no 起飞 / 迷彩).
import { num, traitBb, skillRec, toggleBuff, batMod, up } from '../shared/tier1.js';
import { num, traitBb, skillRec, toggleBuff, batMod, up, holdProtect } from '../shared/tier1.js';
const S1 = 'skchr_zumama_1';
const S2 = 'skchr_zumama_2';
@@ -51,9 +51,6 @@ const S3 = 'skchr_zumama_3';
const T1_PERIOD = 0.1;
/** 庇护 refresh: a little longer than the check period, so it never lapses while it holds. */
const PROTECT_HOLD = T1_PERIOD + 0.05;
/** 庇护 (ba.protect): one "同名效果取最高" effect per unit, whoever grants it (battle.applyStrongest). */
const PROTECT = 'protect';
const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
/** S2's blockee stun, renewed every tick while it holds (three ticks long). */
const STUN_PULSE = 0.1;
const VALOR = 'talent:zumama:valor';
@@ -101,7 +98,7 @@ export default {
const has = unit.findBuff(VALOR);
if (high && !has && scale !== 1) battle.addBuff(unit, { key: VALOR, mods: { atkScaleMul: scale }, tags: ['talent'] });
else if (!high && has) battle.removeBuff(unit, VALOR);
if (up(unit) && !high && dr > 0) battle.applyStrongest(unit, PROTECT, { duration: PROTECT_HOLD, value: dr, mods: protectMods, source: unit });
if (up(unit) && !high) holdProtect(battle, unit, dr, PROTECT_HOLD, unit); // 庇护: the shared effect
};
battle.every(T1_PERIOD, check, { owner: unit, immediate: true });
battle.on('deploy', (ctx) => { if (ctx.unit === unit) check(); }, { owner: unit });
+12 -1
View File
@@ -7,7 +7,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { readFileSync, readdirSync } 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';
@@ -79,3 +79,14 @@ test('余 (blocking), 缪尔赛思\'s melee copies, 赫拉格 / 左乐 (SBL-X, b
assert.equal(u.findBuff(PROTECT), null, 'gone above 50 %');
done(h);
});
test('0.2.0 WV: no kit keeps its own copy of the shared 庇护 key or mods — 淬羽赫默, 黍, 斥罪, 森蚺, 娜斯提 hold it through holdProtect, 遥\'s bubbles share only the mods', () => {
const dir = new URL('../../server/sim/content/kits/ops/', import.meta.url);
for (const f of readdirSync(dir).filter((x) => x.endsWith('.js'))) {
const s = readFileSync(new URL(f, dir), 'utf8');
assert.doesNotMatch(s, /const (PROTECT|protectMods) =|applyStrongest\([^)]*['"]protect['"]|applyStrongest\(\w+, PROTECT\b/, `${f}: the shared tier1.js PROTECT / protectMods / holdProtect`);
}
for (const f of ['op-slent2.js', 'op-shu.js', 'op-judge.js', 'op-zumama.js', 'op-nasti.js']) {
assert.match(readFileSync(new URL(f, dir), 'utf8'), /holdProtect\(/, `${f} uses holdProtect`);
}
});