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
+1
-1
@@ -573,7 +573,7 @@ application competes with instead of its `value`, which stays its effect (Raidia
|
||||
catalogue status the same rule (one invisible buff `key` per target whatever applies it — no immunity, 抵抗, status hooks
|
||||
or icon): the engine default for two same-named buffs (PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"). 奥术 uses
|
||||
it, so the two players of a pair field compete for one instance instead of multiplying, and so do 灵知's 坚冰 and the
|
||||
莱恩哈特 / 缄默德克萨斯 RES cuts, once keyed per unit (DESIGN §20.10). "Strongest" = the largest |value|. PRTS 盟约记录's
|
||||
莱恩哈特 / 缄默德克萨斯 RES cuts, once keyed per unit (DESIGN §20.10). 庇护 (gamedata_const ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)") is one such effect whoever grants it: every kit holds it under the shared key (`kits/shared/tier1.js` `PROTECT` / `holdProtect`, mods phys / artsTakenMul ×(1 − v)) — 宴, 余, 缪尔赛思, 赫拉格, 左乐 and 赫德雷 since 0.2.0 (they used private multipliers that multiplied with each other) —, except 遥's bubbles (the client's damage_resistance[bonus], which multiplies with the common damage_resistance[inf]). "Strongest" = the largest |value|. PRTS 盟约记录's
|
||||
奥术 note "※同一单位仅可对同一目标同时施加1个该盟约法术伤害提升效果" limits each unit to one instance per target; read with
|
||||
the engine default — a newer same-named buff waits inert until the earlier ones end (PRTS 常见同名状态 "默认叠加方式") — and
|
||||
巴哈姆特 12316 ("共享型buff會跟對面搶"), one instance is effective at a time, which is what applyStrongest keeps; strongest
|
||||
|
||||
@@ -319,7 +319,8 @@ Each item is a mistake this project already made once. Tick every one for every
|
||||
`test/match/feedback1-placement.test.js` "#9 data: 狼群 and 流形 are owner-range summons" (DESIGN §20.1, §21.3);
|
||||
`test/content/playtest6_summons.test.js`, `tokens_summons.test.js`; `feedback1b_kits.test.js` "B3 维娜·维多利亚 S3".
|
||||
- [ ] **9. Status effects** — every status applied, with its official duration and stacking (寒冷 on 寒冷 ⇒ 冻结 for the
|
||||
longer cold, 沉默 removes abilities, 恐惧, 睡眠, 浮空 …; "同名效果取最高" = `applyStrongest`). Examples:
|
||||
longer cold, 沉默 removes abilities, 恐惧, 睡眠, 浮空 …; "同名效果取最高" = `applyStrongest`; 庇护 = the shared
|
||||
`PROTECT` key, `holdProtect` in `shared/tier1.js`). Examples:
|
||||
`test/sim/combat.test.js` "cold on cold ⇒ freeze for the longer cold …", "sleep: target is untargetable and
|
||||
inactive; fear stops enemy attacks", "statuses: sleep = invulnerable except `hitSleep` attackers; levitate …";
|
||||
`enemies_bosses.test.js` "沉默: exactly the abilities whose handbook line is SILENCE-flagged can be silenced".
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// server/sim/content/kits/ops/chess_char_1_18-utage.js — 宴 (char_337_utage) kit, tier 1.
|
||||
// Conventions of the tier-1 kits: ../shared/tier1.js; kit contract and rules: ../README.md.
|
||||
|
||||
import { num, talentBb, up, onHitBy, onHitOn, skillBbOf } from '../shared/tier1.js';
|
||||
import { num, talentBb, up, onHitBy, onHitOn, skillBbOf, holdProtect, PROTECT_TICK_HOLD } from '../shared/tier1.js';
|
||||
|
||||
export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -44,11 +44,14 @@ export default {
|
||||
battle.addBuff(unit, { key: 'utage:serious', mods: { aspd: v }, data: { v }, tags: ['talent'] });
|
||||
}, { owner: unit });
|
||||
}
|
||||
// 庇护 (ba.protect): "受到的物理和法术伤害降低相应比例" — physical and arts damage only
|
||||
// 庇护 (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)") while below hp_ratio HP: the shared 庇护 — the
|
||||
// strongest of every source holds (holdProtect) —, refreshed every tick and at each hit on her (a hit that takes
|
||||
// her below it: the next ones already have it)
|
||||
if (t.damage_resistance != null && t.hp_ratio != null) {
|
||||
onHitOn(battle, unit, ({ dmg }) => {
|
||||
if ((dmg.type === 'phys' || dmg.type === 'arts') && unit.hpRatio < num(t.hp_ratio)) dmg.mul *= 1 - num(t.damage_resistance);
|
||||
});
|
||||
const dr = num(t.damage_resistance), below = num(t.hp_ratio);
|
||||
const keep = () => { if (up(unit) && unit.hpRatio < below) holdProtect(battle, unit, dr, PROTECT_TICK_HOLD, unit); };
|
||||
battle.on('tick', keep, { owner: unit });
|
||||
onHitOn(battle, unit, keep);
|
||||
}
|
||||
} }],
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
num, bv, tbb, moduleBb, live, opsOf, ANY, onDefaultSkill, selectedSkill, skillGridOf, AROUND8, onElementHit,
|
||||
elementDmg, aura,
|
||||
} from '../shared/tier6.js';
|
||||
import { holdProtect } from '../shared/tier1.js';
|
||||
|
||||
/**
|
||||
* "传送至自身位置": a ground enemy that can reach `unit`'s tile on the ground grid is moved onto it (unblocked, its
|
||||
@@ -89,9 +90,10 @@ function yu(bb, chess, def) {
|
||||
talents: [
|
||||
{ install(battle, unit) { // 礼尚往来: 庇护 while blocking + DoT on blocked enemies
|
||||
const dr = num(t0.damage_resistance), sc = bv(t0, 'atk_scale'), er = bv(t0, 'ep_damage_ratio'), iv = Math.max(0.1, bv(t0, 'interval', 1));
|
||||
// 庇护 (ba.protect): 受到的物理和法术伤害降低相应比例 — true / element damage is not reduced
|
||||
// 庇护 (ba.protect): 受到的物理和法术伤害降低相应比例(同名效果取最高)— true / element damage is not reduced; the shared
|
||||
// 庇护 (holdProtect: the strongest of every source holds)
|
||||
aura(battle, unit, 0.1, () => {
|
||||
if (dr > 0 && unit.blocking.length) battle.addBuff(unit, { key: 'yu:shelter', mods: { physTakenMul: 1 - dr, artsTakenMul: 1 - dr }, duration: 0.2, refresh: 'replace' });
|
||||
if (dr > 0 && unit.blocking.length) holdProtect(battle, unit, dr, 0.2, unit);
|
||||
});
|
||||
aura(battle, unit, iv, () => {
|
||||
for (const e of unit.blocking.slice()) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
num, bv, tbb, tdesc, moduleBb, parseN, live, isTok, onDefaultSkill, selectedSkill, AROUND8, N4, bstate, bestTile,
|
||||
pullToward,
|
||||
} from '../shared/tier6.js';
|
||||
import { holdProtect } from '../shared/tier1.js';
|
||||
|
||||
/** 莱茵生命 (character_table groupId "rhine") members of this season's pool (checked against the official table). */
|
||||
const RHINE = new Set(['char_108_silent', 'char_128_plosis', 'char_202_demkni', 'char_249_mlyss', 'char_1047_halo2']);
|
||||
@@ -139,7 +140,9 @@ function mlyss(bb, chess, def) {
|
||||
const ranged = !!(m && m.ranged);
|
||||
t.profile.hits = on && ranged && live(t) ? t.mem.mlyssHits * 2 : t.mem.mlyssHits; // 二连击
|
||||
if (on && m && !ranged && live(t)) {
|
||||
battle.addBuff(t, { key: 'mlyss:eco', duration: 0.4, refresh: 'replace', visible: true, mods: { hpRegenRatio: regen, physTakenMul: 1 - dr, artsTakenMul: 1 - dr } });
|
||||
battle.addBuff(t, { key: 'mlyss:eco', duration: 0.4, refresh: 'replace', visible: true, mods: { hpRegenRatio: regen } });
|
||||
// "获得15%的庇护": the shared 庇护 (holdProtect — 同名效果取最高 with every other source)
|
||||
holdProtect(battle, t, dr, 0.4, unit);
|
||||
}
|
||||
}
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
// parts at once].
|
||||
// - Module SBL-X “藏锋” "生命值低于50%时,获得25%的庇护" (a display trait part; the effect is the hidden talent part hp_ratio /
|
||||
// damage_resistance, merged into talent 0's blackboard): below hp_ratio of his max HP the physical and arts damage he takes
|
||||
// ×(1 − damage_resistance) (as 宴's elite module).
|
||||
// ×(1 − damage_resistance) (as 宴's elite module) — 庇护, "同名效果取最高": the shared effect (tier1.js holdProtect).
|
||||
// - Module SBL-Y “热的雪” "被击倒时不撤退且回复30%生命(单次部署只触发1次)" (trait bb hp_ratio): the first lethal blow of a
|
||||
// deployment leaves him on the field at hp_ratio of his max HP (a kit saver, priority −50 as 斯卡蒂's module).
|
||||
// - S1 新月 (AUTO, attack SP, DEFAULT): the next attack at atk_scale × ATK, twice (Skill clip: two OnAttack).
|
||||
@@ -33,7 +33,7 @@
|
||||
// - S3 满月 (MANUAL, time SP, data ACTIVE_RANGE on the running 1-1 + 1): `duration` s — ATK +atk, attack range +1 tile
|
||||
// forward (ability_range_forward_extend), up to attack@max_target targets at once; back to 1-1 after.
|
||||
|
||||
import { num, traitBb, skillRec, toggleBuff, up, onHitOn } from '../shared/tier1.js';
|
||||
import { num, traitBb, skillRec, toggleBuff, up, onHitOn, holdProtect, PROTECT_TICK_HOLD } from '../shared/tier1.js';
|
||||
|
||||
const S1 = 'skchr_helage_1';
|
||||
const S2 = 'skchr_helage_2';
|
||||
@@ -89,12 +89,13 @@ export default {
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// SBL-X “藏锋”: 生命值低于50%时,获得25%的庇护 (physical and arts damage taken)
|
||||
// SBL-X “藏锋”: 生命值低于50%时,获得25%的庇护 — the shared 庇护 (holdProtect: the strongest of every source holds),
|
||||
// refreshed every tick and at each hit on him while he is below
|
||||
const dr = num(t0.damage_resistance), drBelow = num(t0.hp_ratio);
|
||||
if (dr > 0 && drBelow > 0) {
|
||||
onHitOn(battle, unit, ({ dmg }) => {
|
||||
if ((dmg.type === 'phys' || dmg.type === 'arts') && unit.hpRatio < drBelow) dmg.mul *= 1 - dr;
|
||||
});
|
||||
const keep = () => { if (up(unit) && unit.hpRatio < drBelow) holdProtect(battle, unit, dr, PROTECT_TICK_HOLD, unit); };
|
||||
battle.on('tick', keep, { owner: unit });
|
||||
onHitOn(battle, unit, keep);
|
||||
}
|
||||
// SBL-Y “热的雪”: 被击倒时不撤退且回复30%生命(单次部署只触发1次)
|
||||
const rise = num(tb.hp_ratio);
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
// - T2 余火之氅 "使自身与身后一格的友军获得18%的庇护" (damage_resistance, talent grid b-1: his tile and the one behind; CRU-X
|
||||
// stage 3 "28%…且造成的物理伤害提升10%" — hodrer_equip_1_3_p2 gives damage_resistance[inf] and hodrer_equip[damage_scale_up]
|
||||
// (ON_OUTPUT_DAMAGE PHYSICAL ×damage_scale) to the same targets, ally side, any motion, removed when they leave): 庇护
|
||||
// (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)") = phys / artsTakenMul 1 − value as one instance per ally
|
||||
// (applyStrongest) with physDealtMul ×damage_scale, renewed every PROTECT_IV s on the allies of the grid while he is deployed.
|
||||
// (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)") = phys / artsTakenMul 1 − value — the shared 庇护 of every source
|
||||
// (tier1.js holdProtect: damage_resistance[inf] is the common key) — and physDealtMul ×damage_scale (its own one instance per
|
||||
// ally), renewed every PROTECT_IV s on the allies of the grid while he is deployed.
|
||||
// - S1 重锋不熄 (AUTO, hit SP: INCREASE_WHEN_ATTACK, data DEFAULT): the next attack (every enemy he blocks) at atk_scale × ATK;
|
||||
// hodrer_s[heal] (ON_ABILITY_SPELL_ON, HealViaMaxHpRatio) heals hp_ratio of his max HP once for that attack.
|
||||
// - S2 余烬重荷 (MANUAL, data DEFAULT): "被动效果:攻击力+16%" (atk; the skill's extra ability hodrer_s_2[passive]) while it is
|
||||
@@ -44,7 +45,7 @@
|
||||
// then on it takes attack@damage 无来源 true damage (NoSourceDamage PURE: damage.js periodicDamage, credited to him) every
|
||||
// second of its own (triggerInterval 1 s from its mark) until the skill ends (a derived buff of the skill's).
|
||||
|
||||
import { num, talentBb, talentGrid, moduleBb, traitBb, skillRec, statBuff, up, batMod, alliesInGridOf } from '../shared/tier1.js';
|
||||
import { num, talentBb, talentGrid, moduleBb, traitBb, skillRec, statBuff, up, batMod, alliesInGridOf, holdProtect } from '../shared/tier1.js';
|
||||
import { periodicDamage } from '../../../damage.js';
|
||||
|
||||
const S1 = 'skchr_hodrer_1';
|
||||
@@ -53,7 +54,8 @@ const S3 = 'skchr_hodrer_3';
|
||||
/** 余火之氅's grid when the data carries none: his tile and the one behind him (range b-1). */
|
||||
const BEHIND = Object.freeze([Object.freeze([0, -1]), Object.freeze([0, 0])]);
|
||||
const PROTECT_IV = 0.25;
|
||||
const PROTECT_KEY = 'talent:hodrer:protect';
|
||||
/** CRU-X stage 3's 物理伤害 + (hodrer_equip[damage_scale_up]): one instance per ally (the 庇护 is the shared key). */
|
||||
const DMG_UP_KEY = 'talent:hodrer:dmgUp';
|
||||
const S2_PASSIVE_KEY = 'skill:hodrer:s2passive';
|
||||
/** hodrer_s_3[damage] / hodrer_s_3[burn]: triggerInterval 1 s, waitFirstTriggerInterval. */
|
||||
const SMOKE_IV = 1;
|
||||
@@ -140,10 +142,12 @@ export default {
|
||||
const dr = num(t1.damage_resistance), ds = num(t1.damage_scale, 1);
|
||||
const grid = talentGrid(chess, 1) ?? BEHIND;
|
||||
if (!(dr > 0) && ds === 1) return;
|
||||
const mods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v, ...(ds !== 1 ? { physDealtMul: ds } : {}) });
|
||||
const give = () => {
|
||||
if (!up(unit)) return;
|
||||
for (const a of alliesInGridOf(battle, unit, grid)) battle.applyStrongest(a, PROTECT_KEY, { duration: PROTECT_IV + 0.1, value: dr, mods, source: unit });
|
||||
for (const a of alliesInGridOf(battle, unit, grid)) {
|
||||
holdProtect(battle, a, dr, PROTECT_IV + 0.1, unit);
|
||||
if (ds !== 1) battle.applyStrongest(a, DMG_UP_KEY, { duration: PROTECT_IV + 0.1, value: ds, mods: (v) => ({ physDealtMul: v }), source: unit });
|
||||
}
|
||||
};
|
||||
battle.every(PROTECT_IV, give, { owner: unit });
|
||||
battle.on('battleStart', give, { owner: unit });
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
// (zuole_e_003_talent, ON_CALCULATE_DAMAGE): "…且当次攻击的攻击力提升至120%…80%": a success also scales that damage
|
||||
// instance by atk_scale.
|
||||
// - Module SBL-X “岂苦夜长” (trait moduleDesc "生命值低于50%时,获得25%的庇护"; the hidden part merged into the first talent's
|
||||
// bb: hp_ratio / damage_resistance): 庇护 (ba.protect "受到的物理和法术伤害降低相应比例") while HP < hp_ratio
|
||||
// bb: hp_ratio / damage_resistance): 庇护 (ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)": the shared effect of
|
||||
// every source, tier1.js holdProtect) while HP < hp_ratio
|
||||
// (zuole_e_002[resistance]: the 庇护 buff is created when HP drops below it and finished when it is back). PRTS S2 备注:
|
||||
// 行险 resets it, it comes back 0.1 s later "并因此导致庇护的BUFF顺序落后于本技能的屏障" — a 庇护 created after his
|
||||
// barrier cuts only what passes the barrier (the `hpDamage` hook), one created before it cuts the hit first.
|
||||
@@ -46,7 +47,7 @@
|
||||
// at max_scale × max HP at each gain, lasting shield_duration s from the latest gain (PRTS "每次获取屏障时重置剩余持续
|
||||
// 时间为15s").
|
||||
|
||||
import { num, talentBb, traitBb, up, onHitBy, onHitOn, giveSp, skillRec } from '../shared/tier1.js';
|
||||
import { num, talentBb, traitBb, up, onHitBy, onHitOn, giveSp, skillRec, holdProtect, PROTECT, PROTECT_TICK_HOLD } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import { holdsUndying } from '../../items/battle.js';
|
||||
@@ -128,9 +129,12 @@ export default {
|
||||
data: { createdAt: battle.time } });
|
||||
}
|
||||
battle.fx('shield', { x: unit.x, y: unit.y, id: unit.id });
|
||||
// SBL-X: 行险 resets the 庇护, which is judged again PROTECT_RESET s later (after the barrier)
|
||||
// SBL-X: 行险 resets the 庇护, which is judged again PROTECT_RESET s later (after the barrier) — his own held 庇护
|
||||
// goes now (another source's comes back with its next refresh)
|
||||
P.since = null;
|
||||
P.blockedUntil = battle.time + PROTECT_RESET;
|
||||
const held = unit.findBuff(PROTECT);
|
||||
if (held && held.source === unit) battle.removeBuff(unit, held);
|
||||
},
|
||||
},
|
||||
[S3]: {
|
||||
@@ -202,8 +206,10 @@ export default {
|
||||
if (!unit.mem.zuoleS3 || c.source !== unit || !c.target || c.target.side !== 'enemy' || isHpLoss(c.dmg) || c.type === 'element') return;
|
||||
gainS3Barrier(battle, unit);
|
||||
}, { owner: unit });
|
||||
// SBL-X 庇护: active while HP < protectRatio, judged again PROTECT_RESET s after 行险
|
||||
if (protectCut > 0) {
|
||||
// SBL-X 庇护: active while HP < protectRatio, judged again PROTECT_RESET s after 行险 — the shared 庇护 (holdProtect: the
|
||||
// strongest of every source holds), refreshed every tick and at each hit on him. When the 庇护 held is his own and his
|
||||
// barrier is older than it, his cut comes after the barrier (the hpDamage hook) instead of before it
|
||||
if (protectCut > 0 && protectCut < 1) {
|
||||
const active = () => {
|
||||
const now = battle.time;
|
||||
if (!up(unit) || now < P.blockedUntil - 1e-9) { P.since = null; return false; }
|
||||
@@ -211,13 +217,16 @@ export default {
|
||||
P.since = null;
|
||||
return false;
|
||||
};
|
||||
const keep = () => { if (active()) holdProtect(battle, unit, protectCut, PROTECT_TICK_HOLD, unit); };
|
||||
const own = () => { const b = unit.findBuff(PROTECT); return !!b && b.source === unit && b.data?.value === protectCut; };
|
||||
const afterBarrier = new WeakSet();
|
||||
battle.on('tick', active, { owner: unit });
|
||||
battle.on('tick', keep, { owner: unit });
|
||||
onHitOn(battle, unit, ({ dmg }) => {
|
||||
if ((dmg.type !== 'phys' && dmg.type !== 'arts') || !active()) return;
|
||||
keep();
|
||||
if ((dmg.type !== 'phys' && dmg.type !== 'arts') || P.since == null || !own()) return;
|
||||
const bar = barrierOf(unit);
|
||||
if (bar && bar.shield > 0 && (bar.data?.createdAt ?? Infinity) < P.since) afterBarrier.add(dmg);
|
||||
else dmg.mul *= 1 - protectCut;
|
||||
// (the held buff's mods cut this hit before the barrier: undo that and cut what passes the barrier instead)
|
||||
if (bar && bar.shield > 0 && (bar.data?.createdAt ?? Infinity) < P.since) { dmg.mul /= 1 - protectCut; afterBarrier.add(dmg); }
|
||||
});
|
||||
battle.on('hpDamage', (c) => { if (c.target === unit && afterBarrier.has(c.dmg)) c.amount *= 1 - protectCut; }, { owner: unit });
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
// counter {id} · crit {id} · dp {n, id} · heal {id} · taunt {id} · summon {id, token} · pull {id} · sonic {radius} ·
|
||||
// shield {id} · overload {id} · takeoff {id} · sleep {id} · buff {id, kind} · reveal {id} · dodge (engine kind).
|
||||
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { COLS, TICK } from '../../../constants.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { offsetTile } from '../../../dir.js';
|
||||
import { bodyInKeys, bodyTileReach } from '../../../body.js';
|
||||
@@ -94,6 +94,21 @@ export function onDamagedOn(battle, unit, fn, priority = 0) {
|
||||
return battle.on('damaged', (ctx) => { if (ctx.target === unit) fn(ctx); }, { owner: unit, priority });
|
||||
}
|
||||
|
||||
/**
|
||||
* 庇护 (gamedata_const ba.protect "受到的物理和法术伤害降低相应比例(同名效果取最高)"; PRTS 术语释义 庇护): ONE effect per unit
|
||||
* whoever grants it — battle.applyStrongest under this key, the strongest value holds and a weaker one resumes when it
|
||||
* outlasts it. (遥's bubbles are a separate key, the client's damage_resistance[bonus] — they multiply with it.)
|
||||
*/
|
||||
export const PROTECT = 'protect';
|
||||
/** The mods of a 庇护 value: physical and arts damage taken ×(1 − v). */
|
||||
export const protectMods = (v) => ({ physTakenMul: 1 - v, artsTakenMul: 1 - v });
|
||||
/** A hold that outlives one tick and lapses in the next: refreshed every tick while its condition lasts. */
|
||||
export const PROTECT_TICK_HOLD = 1.5 * TICK;
|
||||
/** Hold 庇护 `value` on `target` for `duration` s (the shared PROTECT effect). */
|
||||
export function holdProtect(battle, target, value, duration, source = null) {
|
||||
if (value > 0) battle.applyStrongest(target, PROTECT, { duration, value, mods: protectMods, source });
|
||||
}
|
||||
|
||||
/** Targetable enemies inside `grid` (relative to the unit; null ⇒ current range), best targets first. */
|
||||
export function enemiesInGrid(battle, unit, grid, { n = 0, priority = null, canHitFly = true, groundOnly = false } = {}) {
|
||||
const keys = grid ? absoluteRangeKeys(grid, unit.tileR, unit.tileC, unit.dir, 0) : unit.rangeKeys;
|
||||
|
||||
@@ -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