feedback5: merge WE1 — fidelity (sim rules): 秘术师 store at the attack check (深靛; 维伊 / 黑键 plug into it), enemy attacks land at their damage frame so a short stun / freeze / sleep / 浮空 / 缴械 / 恐惧 before it cancels the swing, 武者 heal per damage instance through 禁疗, 庇护 the strongest only, PHY-X 不高于 50 %, 链愈师 jumps in the 3×3, a summon readied at its owner's redeploy, Misery's duration skills, 伺夜 S2 in 战术点形态, centurions hit up to block-count targets; merge fixes: 黑键's kit keeps the up() import WE2's S2 cast needs (WE1 dropped it with its tick store — the first regeneration had stored 9288 swallowed errors in diy-114), the 自选 refusal names 集成战略 / 生息演算, tools/golden.mjs prints a missing value, test/golden.test.js fails on any stored engine error; golden: vs WE1 only the W4B / WE2 scenarios differ (roster-042, bond-egirShip-high, coop2-ABYSS-10-boosted, coop2-NORMAL-70-diy, diy-045 / 078 / 114 / 115 / 116)
This commit is contained in:
commit
e2bb090f44
60 files changed
+6888
-6103
No files matched your search
@@ -320,7 +320,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".
|
||||
|
||||
@@ -51,7 +51,8 @@ export default {
|
||||
install(battle, unit) {
|
||||
if (tb.hp_ratio == null || num(tb.heal_scale, 1) === 1) return;
|
||||
// GUA-X on her skill heals (S1's heal-mode attack, S2's attack heals while it runs — herself included, she stands
|
||||
// in the grid), never her natural / self regeneration
|
||||
// in the grid), never her natural / self regeneration; strictly below hp_ratio (the client's sunbr_e_trait is
|
||||
// set_heal_scale_by_hpratio: FilterByTargetHpRatio LT)
|
||||
battle.on('heal', (ctx) => {
|
||||
if (ctx.source !== unit || !unit.skill?.active || ctx.opts?.regen || ctx.opts?.self) return;
|
||||
if (ctx.target.hpRatio < num(tb.hp_ratio)) ctx.amount *= num(tb.heal_scale, 1);
|
||||
|
||||
@@ -11,7 +11,8 @@ export default {
|
||||
// 柔光缚目: attacks bind the target `duration` s with `prob`; bound enemies are never chosen as targets.
|
||||
// Alternate S1 灯塔守卫者: for `duration` s the skill range (4-1), attack interval ×(1 + base_attack_time) (PRTS:
|
||||
// "大幅度缩短(-80%)" ⇒ ×0.2 — a flat −0.8 s on 3 s would be less than S2's "略微缩短 ×0.7"), every attack hits for
|
||||
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic).
|
||||
// attack@atk_scale × ATK arts. Elite module MSC-X (store 4) is the mystic profile of the module trait (TUNE.mystic);
|
||||
// the store itself is the shared 秘术师 profile (professions.js installMystic: at the attack check, PRTS 分支特性信息).
|
||||
chess_char_1_17_a: (bb, chess, def) => {
|
||||
const t = talentBb(chess, 0);
|
||||
const dScale = num(bb['indigo_s_2[damage].atk_scale']);
|
||||
@@ -28,12 +29,11 @@ export default {
|
||||
},
|
||||
},
|
||||
trait: {
|
||||
// with only bound enemies in range (or blocked by her — always her targets, Battle.blockedTargets) she holds her
|
||||
// fire (the mystic trait stores the energy meanwhile)
|
||||
// with only bound enemies in range (or blocked by her — always her targets, Battle.blockedTargets) she has no valid
|
||||
// target: she holds her fire and the mystic trait stores an energy at the attack check (professions.js
|
||||
// installMystic; a bind on her only target included)
|
||||
canAttack(battle, u) {
|
||||
const ok = battle.enemiesInKeys(u.rangeKeys, u, u.profile).some((e) => !bound(e)) || battle.blockedTargets(u, u.profile).some((e) => !bound(e));
|
||||
if (!ok) u.trait.hadTarget = false;
|
||||
return ok;
|
||||
return battle.enemiesInKeys(u.rangeKeys, u, u.profile).some((e) => !bound(e)) || battle.blockedTargets(u, u.profile).some((e) => !bound(e));
|
||||
},
|
||||
afterHit(battle, u, target) {
|
||||
if (!target || !target.alive || target.side !== 'enemy') return;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
} }],
|
||||
};
|
||||
|
||||
@@ -136,7 +136,8 @@ export default {
|
||||
if (num(tb.damage_resistance) > 0) {
|
||||
kit.install = (battle, unit) => battle.addBuff(unit, { key: 'trait:blemsh_guard', mods: { dmgTakenMul: 1 - num(tb.damage_resistance) }, persist: true, allowDead: true });
|
||||
} else if (num(tb.heal_scale) > 0) {
|
||||
// GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%" (the HP before the heal)
|
||||
// GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%" (the HP before the heal; strictly below — the client's blemsh_e_trait
|
||||
// is set_heal_scale_by_hpratio, FilterByTargetHpRatio LT, unlike 黍's heal_scale_up[hpratio][LE])
|
||||
kit.install = (battle, unit) => battle.on('heal', (ctx) => {
|
||||
if (ctx.source === unit && ctx.target && ctx.target.hpRatio < num(tb.hp_ratio, 0.5)) ctx.amount *= num(tb.heal_scale, 1);
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -162,10 +162,10 @@ export default {
|
||||
};
|
||||
// S2: the pack's empowered next attack (armed by the cast; ×scale on its hits; a kill pays once)
|
||||
const installGift = (battle, unit) => {
|
||||
// the cast: ready, the pack standing, no unused gift on it (PRTS 备注 「仅场上存在狼群,且狼群未获得此技能的充能时可触发
|
||||
// 技能」; a pack in its 战术点形态 does not count — the 备注 does not say [ASSUMED])
|
||||
// the cast: ready, the pack on the field in either form, no unused gift on it (PRTS 备注 「仅场上存在狼群,且狼群未获得此技能的
|
||||
// 充能时可触发技能」 — the words of S1's, whose 备注 counts the 战术点形态 [ASSUMED the same for S2])
|
||||
battle.on('tick', () => {
|
||||
const sk = unit.skill, w = wolfOf(unit);
|
||||
const sk = unit.skill, w = packOf(unit);
|
||||
if (!alive(unit) || !sk || !sk.ready || sk.active || !unit.canAct || unit.s.flags.silence || !w || w.mem.vigilGift) return;
|
||||
sk.activate('SP_FULL');
|
||||
}, { owner: unit });
|
||||
@@ -272,18 +272,20 @@ export default {
|
||||
}),
|
||||
// (自动触发, PRTS 备注 「仅场上存在狼群,且狼群未获得此技能的充能时可触发技能」: no enemy needed — the kit casts it as soon
|
||||
// as it is ready while the pack stands and holds no unused gift (installGift); until 0.2.0 the data's DEFAULT made it
|
||||
// wait for 伺夜's own next attack, so it never fired with no enemy in his range. A pack in its 战术点形态 does not
|
||||
// count: S1's 备注 lists that form, S2's says nothing of it, so S2 waits for the pack to come back [ASSUMED]; a gift
|
||||
// the pack holds stays on it through the form [ASSUMED])
|
||||
// wait for 伺夜's own next attack, so it never fired with no enemy in his range. A pack in its 战术点形态 counts as on the
|
||||
// field — S2's 备注 uses S1's words and S1's lists that form [ASSUMED for S2; the 0.2.0 follow-up's recommended
|
||||
// reading]: the cast pays its DP at once and the gift waits on the pack, its next attack once it is back (until 0.2.0
|
||||
// S2 waited for the pack); a gift the pack holds stays on it through the form [ASSUMED])
|
||||
skchr_vigil_2: (s) => ({
|
||||
kind: 'instant',
|
||||
trigger: 'NEVER',
|
||||
onStart({ battle, unit }) {
|
||||
dpGain(battle, unit, num(s.bb.cost, 0));
|
||||
const w = wolfOf(unit);
|
||||
const w = packOf(unit);
|
||||
if (!w) return;
|
||||
// (a pack in its 战术点形态 has no HP to restore: it comes back at full HP, the gift kept on it)
|
||||
const hr = num(s.bb['vigil_wolf_s_2.hp_ratio'], 0);
|
||||
if (hr > 0) battle.heal(w, w, w.s.maxHp * hr, { self: true });
|
||||
if (hr > 0 && w.alive) battle.heal(w, w, w.s.maxHp * hr, { self: true });
|
||||
w.mem.vigilGift = { scale: num(s.bb['vigil_wolf_s_2.atk_scale'], 1), dp: num(s.bb['vigil_wolf_s_2.cost'], 0), paid: false };
|
||||
fx(battle, 'buff', w, { src: unit.id, skill: 'vigil_2' });
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ export default withDefaults({
|
||||
if (t.side !== 'ally' || !t.alive || !(c.amount > 0) || isHpLoss(c.dmg) || !t.findBuff(`reckpr:guard:${unit.id}`)) return;
|
||||
battle.heal(unit, t, num(bb['attack@fixed_heal_value'], 80));
|
||||
}, { owner: unit });
|
||||
installLowHpHealBonus(battle, unit, tb);
|
||||
installLowHpHealBonus(battle, unit, tb); // PHY-X: reckpr_e_002_tr filters LT (strictly below)
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ export default withDefaults({
|
||||
}, { owner: unit });
|
||||
} }],
|
||||
install(battle, unit) {
|
||||
installLowHpHealBonus(battle, unit, tb);
|
||||
installLowHpHealBonus(battle, unit, tb, { atOrBelow: true }); // PHY-X: heal_scale_up[hpratio][LE] (≤)
|
||||
if (!S1) return;
|
||||
battle.on('beforeAttack', (c) => {
|
||||
if (c.attacker !== unit) return;
|
||||
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
c.amount += c.target.s.maxHp * ratio * num(unit.s.healingDealtMul, 1) * num(c.target.s.healingTakenMul, 1);
|
||||
battle.fx('healAoe', { x: c.target.x, y: c.target.y, id: c.target.id, r: 0.5 });
|
||||
}, { owner: unit, priority: 10 });
|
||||
lowHpHealUp(battle, unit, tb);
|
||||
lowHpHealUp(battle, unit, tb, { atOrBelow: true }); // PHY-X: heal_scale_up[hpratio][LE] (≤)
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
install(battle, unit) {
|
||||
// S2 only: the NEVER-trigger skill casts itself when an ally in its area is injured
|
||||
if (!sid || sid === 'skchr_demkni_2') autoCast(battle, unit, () => zone(battle, unit).some((a) => a.hp < a.s.maxHp - 1e-6));
|
||||
lowHpHealUp(battle, unit, tb);
|
||||
lowHpHealUp(battle, unit, tb); // GUA-X: set_heal_scale_by_hpratio filters LT (strictly below)
|
||||
if (num(tb.damage_resistance) > 0) permBuff(battle, unit, 'saria:moduleY', { dmgTakenMul: 1 - num(tb.damage_resistance) });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
const b = a.findBuff('reckpr:guard');
|
||||
if (b && b.data.src === unit) battle.heal(unit, a, num(b.data.value));
|
||||
}, { owner: unit });
|
||||
lowHpHealUp(battle, unit, tb);
|
||||
lowHpHealUp(battle, unit, tb); // PHY-X: reckpr_e_002_tr filters LT (strictly below)
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
// - Trait (秘术师) "攻击造成法术伤害,在找不到攻击目标时可以将攻击能量储存起来之后一齐发射(最多3个)" (bb times; MSC-X 4):
|
||||
// ranged arts bolts, hits air units (分支特性信息 "可对空"), blocks 1, ground enemies target him. The store (分支特性信息:
|
||||
// "能量储存与攻击占用相同的攻击间隔:…若无有效目标且能量储存数未满,则改为储存一份攻击能量(属于攻击行为)"): when his attack is
|
||||
// ready and he has no valid target he stores one energy and his attack interval starts again — this kit's own store,
|
||||
// as 维伊's (op-veen.js; the engine's mystic profile stores one interval after the attack is ready). His next attack
|
||||
// ready and he has no valid target he stores one energy and his attack interval starts again — the shared 秘术师
|
||||
// profile (professions.js installMystic) with this kit's `storeEnergy` (his elite energy, S3), as 维伊's. His next attack
|
||||
// releases them at its target ("一齐发射"), each hitting for ATK × the attack's scale × the energy scale (talent 1) as
|
||||
// an arts 普通伤害 attack hit ("由储存能量形成的弹道造成攻击力100%的法术普通伤害"); they land with the main bolt (one
|
||||
// volley to one target [ASSUMED], as 维伊's), the later ones only on a target still alive. [ASSUMED] a redeployment holds
|
||||
@@ -65,7 +65,6 @@
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, up, toggleBuff } from '../shared/tier1.js';
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
import { acquireTargets, effectiveProfile } from '../../../ai.js';
|
||||
import { hasHp } from '../../../damage.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
|
||||
@@ -215,21 +214,19 @@ export default {
|
||||
trait: {
|
||||
hitsFn: () => 1, // the engine's main hit only: the stored energies are this kit's (land)
|
||||
onEachHit(battle, unit, victim, hc) { if (hc.kind === 'main' && victim && victim.side === 'enemy') land(battle, unit, victim, hc); },
|
||||
// the store (the shared profile calls it at his attack check with no valid target — professions.js installMystic:
|
||||
// an attack action, the interval restarts): the trait's energies first, then the elite one; during S3 elite ones
|
||||
// only (false = a full store: he idles)
|
||||
storeEnergy(battle, unit) {
|
||||
const v = stateOf(unit);
|
||||
if (skillOn(unit, S3)) { if (v.n + v.e >= N + E) return false; v.e++; }
|
||||
else if (v.n < N) v.n++;
|
||||
else if (v.e < E) v.e++;
|
||||
else return false;
|
||||
return true;
|
||||
},
|
||||
install(battle, unit) {
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit && !ctx.move) unit.trait.ebn = null; }, { owner: unit });
|
||||
// the store: his attack ready and no valid target ⇒ one energy, an attack action (the interval restarts); the
|
||||
// attack loop ran first this tick (a valid target was attacked instead)
|
||||
battle.on('tick', () => {
|
||||
if (!up(unit) || !unit.canAct || unit.s.flags.disarm || unit.atkCd > 0) return;
|
||||
const prof = effectiveProfile(unit);
|
||||
if (prof.noAttack || acquireTargets(battle, unit, prof).length) return;
|
||||
const v = stateOf(unit);
|
||||
if (skillOn(unit, S3)) { if (v.n + v.e >= N + E) return; v.e++; }
|
||||
else if (v.n < N) v.n++;
|
||||
else if (v.e < E) v.e++;
|
||||
else return;
|
||||
unit.atkCd = unit.s.interval;
|
||||
}, { owner: unit });
|
||||
battle.on('death', (ctx) => { if (ctx.unit && ctx.unit.side === 'enemy') stateOf(unit).q.delete(ctx.unit.id); }, { owner: unit });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
// 属性加成,损失一定比例时达最大加成(同类属性取最高)") and ba.protect 庇护 ("受到的物理和法术伤害降低相应比例(同名效果取最高)"), his
|
||||
// battle skeleton char_188_helage.skel (Skill: two OnAttack, Skill_2: two, Skill_3_Loop: one).
|
||||
// - Trait (武者) "不成为其他角色的治疗目标,每次攻击到敌人后回复自身70生命": the profession default (professions.js `musha`: no
|
||||
// heal from others, value HP per enemy an attack hits), ground-only melee on his 1-1, block 1. The branch heals on every
|
||||
// damage instance he deals: the engine heals once per enemy an attack strikes, so the second hit of S1 / S2's 二连击 on
|
||||
// an enemy heals once more here (the trait's `value`, a self heal).
|
||||
// heal from others, `value` HP on every damage instance he deals — PRTS 分支特性信息 武者 — so the second hit of S1 / S2's
|
||||
// 二连击 heals too), ground-only melee on his 1-1, block 1.
|
||||
// - T1 月盈星亏 "在场时,自身获得最高+100攻击速度的坚忍(损失70%生命值时达到最大加成)": ASPD + min_attack_speed × the share of
|
||||
// the HP lost up to 1 − min_hp_ratio (linear, 坚忍), refreshed every tick. SBL-X stage 3: +130 at 50 % lost (the module
|
||||
// talent change).
|
||||
@@ -26,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).
|
||||
@@ -34,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';
|
||||
@@ -90,22 +89,13 @@ export default {
|
||||
} },
|
||||
],
|
||||
install(battle, unit) {
|
||||
// 武者: the trait heal on every damage instance — the engine heals once per enemy an attack strikes, so each further
|
||||
// hit of the same attack on that enemy (S1, S2's 二连击) heals once more
|
||||
battle.on('damaged', (c) => {
|
||||
const d = c.dmg;
|
||||
if (c.source !== unit || !unit.alive || !d || !d.isAttack || !d.attackId || !c.target || c.target.side !== 'enemy') return;
|
||||
const m = unit.mem;
|
||||
if (m.helageAttackId !== d.attackId) { m.helageAttackId = d.attackId; m.helageStruck = new Set(); }
|
||||
if (!m.helageStruck.has(c.target.id)) { m.helageStruck.add(c.target.id); return; }
|
||||
battle.heal(unit, unit, num(unit.profile?.selfHeal, num(tb.value, 70)), { self: true });
|
||||
}, { owner: unit, priority: -10 });
|
||||
// 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 });
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
// [ASSUMED: given here]); its DEF is ×0 while its tile is outside her attack range (kalts_t_1[token_def_down]; PHY-Y stage
|
||||
// 3: inside it ASPD +attack_speed, DEF +def — kalts_t_1[token_def_down_finish]). It is withdrawn when she leaves (no
|
||||
// rattle); it comes back on its tile its respawnTime (25 s) after it left, paying its cost (10 DP), only while she stands —
|
||||
// her (re)deployment readies a waiting one at once (charge_token[born]) [ASSUMED: the 卫戍 auto redeploy of a placed summon].
|
||||
// her (re)deployment readies a waiting one at once (charge_token[born]: RechargeToken with timing NORMAL, while its own timer
|
||||
// is the summon's charge_token[finish], ON_FINISH — the one reading of every summoner kit, 傀影's twin included)
|
||||
// [ASSUMED: the 卫戍 auto redeploy of a placed summon].
|
||||
// - T2 不毁重构: Mon3tr knocked out (not withdrawn; S3's 流失 counts) ⇒ every selectable enemy of its 3×3 (the talent's x-4,
|
||||
// air units too) takes `value` (1200) true damage (溅射, not dodgeable) and is stunned `stun` (3) s. PHY-X stage 3 (the
|
||||
// token's module talent: 3.5 s, 1500, hp_ratio): also once per deployment when a hit leaves it at or below hp_ratio of
|
||||
@@ -41,8 +43,9 @@
|
||||
// - S1 指令:结构加固 (MANUAL, data DEFAULT — a heal skill: cast as she is about to heal): her DEF +def and Mon3tr's DEF
|
||||
// +attack@def while it runs; 物理格挡 prob (damage_block[phy]: a physical damage instance she takes is blocked whole —
|
||||
// not a dodge, nothing ignores it; a 流失 is no damage).
|
||||
// - S2 指令:战术协同 (MANUAL, data DEFAULT, 绑定 Mon3tr): her ASPD +attack_speed; Mon3tr ATK +attack@atk and each of its attacks
|
||||
// strikes every enemy it blocks (one target when it blocks none [ASSUMED]).
|
||||
// - S2 指令:战术协同 (MANUAL, data DEFAULT, 绑定 Mon3tr): her ASPD +attack_speed; Mon3tr ATK +attack@atk and "可以攻击阻挡的所有
|
||||
// 敌人": each of its attacks takes up to its block count (3) of targets, the blocked ones first (the 强攻手 rule — PRTS 分支特性信息
|
||||
// "普通攻击最大目标数等于阻挡数(不会低于1)"; its mode's _limitedMaxTargetNumToBlockedCnt).
|
||||
// - S3 指令:熔毁 (MANUAL, data DEFAULT, 绑定 Mon3tr, 18 s): Mon3tr DEF +attack@def, its attacks deal true damage, its ATK
|
||||
// +attack@atk × the remaining share of the skill, updated every second from the cast (+190 % … +10.6 %); unless it killed
|
||||
// an enemy meanwhile, at the skill's normal end it loses attack@hp_ratio × its max HP (a 流失 that may knock it out).
|
||||
@@ -52,6 +55,7 @@
|
||||
import { num, talentBb, traitBb, moduleOn, skillRec, up } from '../shared/tier1.js';
|
||||
import { absoluteRangeKeys } from '../../../targeting.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { acquireTargets } from '../../../ai.js';
|
||||
|
||||
const S1 = 'skchr_kalts_1';
|
||||
const S2 = 'skchr_kalts_2';
|
||||
@@ -130,11 +134,12 @@ function mon3trKit(owner, { t0, b1, b2, b3 }) {
|
||||
}
|
||||
holdBuff(battle, m, 'kalts:s3:mon3tr', skillOn(owner, S3), { atkPct: s3Atk, defPct: num(b3['attack@def']) });
|
||||
}, { owner: m });
|
||||
// S2: each attack strikes every enemy it blocks
|
||||
// S2: "Mon3tr可以攻击阻挡的所有敌人" — the 强攻手 rule (its mode's _limitedMaxTargetNumToBlockedCnt): up to its block count
|
||||
// of targets, the blocked ones first (ai.js acquireTargets with `hitAllBlocked`)
|
||||
battle.on('beforeAttack', (c) => {
|
||||
if (c.attacker !== m || !skillOn(owner, S2)) return;
|
||||
const blocked = battle.blockedTargets(m, c.profile);
|
||||
if (blocked.length) c.targets = blocked;
|
||||
if (c.attacker !== m || !skillOn(owner, S2) || !c.profile) return;
|
||||
const t = acquireTargets(battle, m, { ...c.profile, hitAllBlocked: true });
|
||||
if (t.length) c.targets = t;
|
||||
}, { owner: m });
|
||||
// S3: true damage; a kill keeps its HP
|
||||
battle.on('hit', (c) => {
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
// prosts_t_1[born_charge] BLOCK_CNT ×0, interval 0.1, firstTriggerInterval 1, FixedValueDamage PURE `damage` on itself);
|
||||
// buff_template_data (monstr_t_2[atk_spd] ON_OUTPUT_MODIFIER IsHeal; monstr_s_3[undead] ON_BEFORE_TRY_SET_HP_ZERO).
|
||||
// - Trait (链愈师) "恢复友方单位生命,且会在3个友方单位间跳跃,每次跳跃治疗量降低25%" — the chain is this kit's (the profession's
|
||||
// 2.5-tile chain is replaced: heal mode single + `chainFrom`), as her heal projectiles have it: after the heal on the main
|
||||
// target the heal jumps to the lowest-HP-ratio injured ally of the 3×3 around the last one healed (x-4), each jump ×
|
||||
// chain is replaced: heal mode single + `chainFrom`, its links the profession's — ai.js chainHealNext): after the heal on
|
||||
// the main target the heal jumps to the lowest-HP-ratio ally (a full-HP one too — PRTS 分支特性信息 链愈师; ties: the latest
|
||||
// deployed) of the 3×3 around the last one healed (x-4), each jump ×
|
||||
// attack@chain.atk_scale (0.75; XAH-X 0.85), up to attack@chain.max_target units healed (3). A 重构体 (and she herself while S3
|
||||
// runs: monstr[free_jump]) is a free link: healing it uses no target of the count and the next jump does not decay (XAH-X
|
||||
// stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection
|
||||
@@ -54,8 +55,9 @@
|
||||
// - S3 策略:熔毁 (MANUAL, data DEFAULT, 25 s; only while her 重构体 stands — the cast is refused otherwise): the 重构体 is withdrawn
|
||||
// (its piece waits: back once its tile is free again) and she 【移动】s onto its tile; meanwhile range x-4, ATK +atk, base attack
|
||||
// time base_attack_time (a flat −1.5 s on 2.85 s), block +block_cnt, max HP +max_hp (the HP ratio kept), 流失 of
|
||||
// damage_per_second HP/s (8 every 0.1 s); her attacks hit every ground enemy she blocks (one in range when she blocks none —
|
||||
// [ASSUMED] as 凯尔希's Mon3tr S2) for ATK true damage, and each attack starts a chain heal on herself at attack@heal_scale × ATK
|
||||
// damage_per_second HP/s (8 every 0.1 s); "同时攻击阻挡的所有敌人" (her selector's _limitedMaxTargetNumToBlockedCnt): each attack
|
||||
// takes up to her block count of ground enemies of her range, the blocked ones first (ai.js `hitAllBlocked`, the 强攻手 rule
|
||||
// of PRTS 分支特性信息) for ATK true damage, and each attack starts a chain heal on herself at attack@heal_scale × ATK
|
||||
// (she is a free link). 不死: a hit or 流失 that would knock her out leaves her at 1 HP, untargetable and blocking nothing, and
|
||||
// the skill ends on the next tick. At the end she 【返回】s to the tile she left (her SP emptied; the max HP drop keeps the ratio,
|
||||
// so after a fatal blow she stands at ≤ 1 HP) with 1 s of 不死. The 路标形态 marker on her tile is not modelled: should another
|
||||
@@ -63,6 +65,7 @@
|
||||
|
||||
import { num, talentBb, traitBb, skillRec, batMod, installAura, up } from '../shared/tier1.js';
|
||||
import { COLS } from '../../../constants.js';
|
||||
import { chainHealNext } from '../../../ai.js';
|
||||
|
||||
const S1 = 'skchr_monstr_1';
|
||||
const S2 = 'skchr_monstr_2';
|
||||
@@ -101,28 +104,13 @@ const prostsInRange = (battle, unit) => prostsOf(battle, unit).find((t) => up(t)
|
||||
/** `a` stands on the 3×3 around `b` (x-4: a heal jump's reach, the 重构体's aura). */
|
||||
const nearX4 = (a, b) => Math.abs(a.tileR - b.tileR) <= 1 && Math.abs(a.tileC - b.tileC) <= 1;
|
||||
|
||||
/**
|
||||
* The next link of a chain heal from `cur`: the lowest-HP-ratio injured ally of its 3×3 not healed yet by this chain (her
|
||||
* own 重构体 even at full HP when `fullProsts`); never a 禁疗 unit but her 重构体.
|
||||
*/
|
||||
function nextLink(battle, unit, cur, seen, fullProsts) {
|
||||
let best = null;
|
||||
for (const a of battle.allyUnits) {
|
||||
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id) || !nearX4(a, cur)) continue;
|
||||
const mine = isProstsOf(a, unit);
|
||||
if (a !== unit && ((a.s.flags.noHeal && !mine) || (a.profile && a.profile.noHeal))) continue;
|
||||
if (!(a.hp < a.s.maxHp - 1e-6) && !(fullProsts && mine)) continue;
|
||||
if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.deploySeq < best.deploySeq)) best = a;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chain heal of hers from `main` (already healed by her attack when `mainDone`): up to `count` units healed, × `step` per
|
||||
* jump; a free link (her 重构体; herself with `selfFree`) uses no target of the count and the next 1 + `extra` jumps keep the
|
||||
* amount.
|
||||
* amount. Each link is the 链愈师 one (ai.js chainHealNext — the 3×3 around the last one healed, the lowest HP ratio a
|
||||
* full-HP ally included, then the latest deployed; no 禁疗 unit but her 重构体, her `healThrough`).
|
||||
*/
|
||||
function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts = false, mainDone = false, selfFree = false }) {
|
||||
function chainFrom(battle, unit, main, base, { count, step, extra, mainDone = false, selfFree = false }) {
|
||||
const free = (t) => isProstsOf(t, unit) || (selfFree && t === unit);
|
||||
let amount = base;
|
||||
if (!mainDone) battle.heal(unit, main, amount, HEAL_OPTS);
|
||||
@@ -131,7 +119,7 @@ function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts =
|
||||
const seen = new Set([main.id]);
|
||||
let cur = main;
|
||||
while (counted < count) {
|
||||
const next = nextLink(battle, unit, cur, seen, fullProsts);
|
||||
const next = chainHealNext(battle, unit, cur, seen);
|
||||
if (!next) break;
|
||||
seen.add(next.id);
|
||||
if (calm > 0) calm--; else amount *= step;
|
||||
@@ -212,7 +200,7 @@ export default {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess), blockCnt: num(b3.block_cnt), hpFlat: num(b3.max_hp) },
|
||||
targeting: s3?.rangeGrid ? { rangeGrid: s3.rangeGrid } : undefined,
|
||||
attack: { dmgType: 'true', attack: 'melee', projectile: 'none', canHitFly: false, groundOnly: true, hitAllBlocked: true, maxTargets: 1 },
|
||||
attack: { dmgType: 'true', attack: 'melee', projectile: 'none', canHitFly: false, groundOnly: true, hitAllBlocked: true },
|
||||
onStart({ battle, unit, skill }) {
|
||||
const r = prostsOf(battle, unit).find(up);
|
||||
if (!r) { skill.end('noToken'); skill.addCharge(1); return; } // (refused before: the activate guard)
|
||||
@@ -280,12 +268,12 @@ export default {
|
||||
const base = unit.s.atk * (prof.atkScale ?? 1) * (prof.healScale ?? 1) * unit.s.atkScaleMul;
|
||||
for (const t of c.targets || []) {
|
||||
if (!t || t.side !== 'ally') continue;
|
||||
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, fullProsts: s2on, mainDone: true });
|
||||
chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, mainDone: true });
|
||||
// S2: 0.6 s after her attack healed her 重构体, a second chain on it (her ATK × 1)
|
||||
if (s2on && isProstsOf(t, unit)) {
|
||||
battle.after(S2_EXTRA_DELAY, () => {
|
||||
if (!unit.alive || !up(t)) return;
|
||||
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, { ...chain, fullProsts: skillOn(unit, S2) });
|
||||
chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, chain);
|
||||
}, { owner: unit });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
// each of its own deployments. When he leaves the field (knocked out, withdrawn, forced out) it is withdrawn (PRTS "强制
|
||||
// 撤退"; the client's KillTokens); a twin off the field comes back on its tile — paying its cost (5 DP) as a deployment,
|
||||
// the 卫戍 auto redeploy of a placed summon [ASSUMED, as 鸿雪's 打字机 / 凯尔希's Mon3tr] — once its redeploy time has
|
||||
// passed since its LAST DEPLOYMENT (PRTS 备注: the timer starts at the summon's deployment) and only while he stands (it
|
||||
// is recharged by his deployment, withdrawn by his leaving) [ASSUMED: his redeployment does not cut the twin's own timer
|
||||
// short — the client's RechargeToken NORMAL; 虚影精通 / EXE-X would matter little otherwise; the 凯尔希 kit reads the same
|
||||
// charge_token[born] as "readies at once"]. EXE-Y stage 2+ "本体和虚影同时在场时,攻击力各+10%": the twin's hidden module
|
||||
// passed since its LAST DEPLOYMENT (PRTS 备注: the timer starts at the summon's deployment; the twin's charge_token[finish]
|
||||
// recharges it ON_FINISH) and only while he stands; his (re)deployment readies a waiting twin at once (his
|
||||
// charge_token[born]: RechargeToken with timing NORMAL — read the same way for every summoner, 凯尔希's Mon3tr included;
|
||||
// until 0.2.0 this kit let the twin's own timer run on). EXE-Y stage 2+ "本体和虚影同时在场时,攻击力各+10%": the twin's hidden module
|
||||
// talent `atk` — ATK +atk on both while both are on the field.
|
||||
// - T2 虚影精通 "虚影的再部署时间-10秒" (EXE-X stage 3: −16): the twin's own talent respawn_time, added to its 45 s.
|
||||
// - S1 暗夜魅影 (被动, at each deployment): physical dodge `prob` and a 屏障 of hp_ratio × max HP that absorbs physical damage
|
||||
@@ -210,6 +210,13 @@ export default {
|
||||
// "傀影退场时强制撤退场上的镜中虚影" (KillTokens): a withdrawal, not a knock-out
|
||||
for (const t of twinsOf(battle, unit)) if (t.alive) battle.retreat(t, { reason: 'retreat' });
|
||||
}, { owner: unit });
|
||||
// charge_token[born] (RechargeToken, timing NORMAL): his (re)deployment readies a waiting twin at once — the
|
||||
// twin's own timer is the ON_FINISH recharge of its charge_token[finish] (as every summoner kit reads it)
|
||||
// [ASSUMED: NORMAL = usable now, from the enum and the first deployment]
|
||||
battle.on('deploy', (c) => {
|
||||
if (c.unit !== unit || c.move) return;
|
||||
for (const t of twinsOf(battle, unit)) if (!t.alive && !t.removed) t.mem.twinReadyAt = battle.time;
|
||||
}, { owner: unit });
|
||||
// 本体和虚影同时在场时,攻击力各+N%: the twin's hidden module talent (his text when no twin def resolves)
|
||||
const both = num(tokTalentBb(battle.tokenDef(TWIN, unit), 'atk').atk, /本体和虚影同时在场时/.test(t0desc)
|
||||
? num(+(/攻击力各\+(\d+)%/.exec(t0desc)?.[1]) / 100) : 0);
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
// - Trait (医师) "恢复友方单位生命": the profession default (one heal on the lowest-HP% injured ally of her 3-3 range).
|
||||
// Module PHY-Y 干枯剑鞘 "治疗地面单位时治疗量提升15%" (trait bb heal_scale): ×heal_scale on a heal whose target stands on
|
||||
// a 地面 tile (unit.ground, as 赫默's PHY-Y); PHY-X “使徒” "治疗生命值低于50%的友方单位时治疗量提升15%" (heal_scale,
|
||||
// hp_ratio): ×heal_scale when the target is below hp_ratio of its max HP before the heal (as 录武官's / 华法琳's PHY-X).
|
||||
// hp_ratio): ×heal_scale when the target is at or below hp_ratio of its max HP before the heal — the client's
|
||||
// shining_e_003_tr is heal_scale_up[hpratio][LE] (as 华法琳's PHY-X; 录武官's reckpr_e_002_tr is LT, strictly below).
|
||||
// - T1 黑恶魔的庇护 "攻击范围内的友方单位防御力+60": every ally (operators and summons: 友方单位) standing in her current
|
||||
// attack range — her own tile included — DEF +def (flat). PHY-Y stage 2+ "防御力+100,地面单位防御力额外+40" (talent
|
||||
// change def / def_lowland): def_lowland more on a 地面 tile. Refreshed every AURA s; two sources keep the strongest.
|
||||
@@ -130,7 +131,7 @@ export default {
|
||||
// module traits: PHY-Y heals on 地面 units, PHY-X heals on allies below hp_ratio, ×heal_scale
|
||||
const hs = num(tb.heal_scale, 1);
|
||||
if (!mod || hs === 1) return;
|
||||
const boosted = mod === PHY_X ? (t) => t.hpRatio < num(tb.hp_ratio) : mod === PHY_Y ? (t) => !!t.ground : null;
|
||||
const boosted = mod === PHY_X ? (t) => t.hpRatio <= num(tb.hp_ratio) + 1e-9 : mod === PHY_Y ? (t) => !!t.ground : null;
|
||||
if (!boosted) return;
|
||||
battle.on('heal', (ctx) => {
|
||||
if (ctx.source !== unit || ctx.opts?.regen || !ctx.target || !boosted(ctx.target)) return;
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
// - Trait (秘术师) "攻击造成法术伤害,在找不到攻击目标时可以将攻击能量储存起来之后一齐发射" (ranged arts, hits air units,
|
||||
// targetable by ground enemies). PRTS 分支特性信息 秘术师: "能量储存与攻击占用相同的攻击间隔:在进行攻击判定时,若范围内存在
|
||||
// 有效目标,则进行普通攻击;若无有效目标且能量储存数未满,则改为储存一份攻击能量(属于攻击行为)" — when her attack is ready
|
||||
// and she has no valid target she stores one energy and her attack interval starts again (this kit replaces the
|
||||
// engine's mystic profile, which stores one interval after the attack is ready); the energies fly with her next attack,
|
||||
// at its target ("由储存能量形成的弹道造成攻击力100%的法术普通伤害").
|
||||
// and she has no valid target she stores one energy and her attack interval starts again (the shared 秘术师 profile,
|
||||
// professions.js installMystic, with this kit's `storeEnergy`); the energies fly with her next attack, at its target
|
||||
// ("由储存能量形成的弹道造成攻击力100%的法术普通伤害").
|
||||
// T1 “在挥刀之前” "特性储存的能量达到3个时,合成为1个转置能量(至多储存等同于9倍的能量)" (trait bb merge_cnt 3, times 9):
|
||||
// 3 stored make 1 转置能量, at most times / merge_cnt of those, then no more storing (备注 "转置能量数量达到上限时,停止储存
|
||||
// 攻击能量"); a 转置能量 hits for merge_cnt × ATK (备注 "攻击力300%的法术普通伤害"). Every shot uses the ATK it left with
|
||||
@@ -39,7 +39,7 @@
|
||||
// preferring an enemy not hit yet by it, else another one, else the same (text "优先不同目标" [ASSUMED order]), each
|
||||
// attack@bounce_atk_scale × the cached ATK arts. The skill ends when the bullets run out.
|
||||
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, up, toggleBuff } from '../shared/tier1.js';
|
||||
import { num, talentBb, moduleBb, traitBb, skillRec, batMod, toggleBuff } from '../shared/tier1.js';
|
||||
import { canTargetEnemy, sortEnemyTargets } from '../../../targeting.js';
|
||||
|
||||
const S1 = 'skchr_veen_1';
|
||||
@@ -146,23 +146,19 @@ export default {
|
||||
trait: {
|
||||
hitsFn: () => 0, // the volley's hits are the kit's (land)
|
||||
onEachHit(battle, unit, victim, hc) { if (hc.kind === 'main' && victim && victim.side === 'enemy') land(battle, unit, victim, hc.attackId, !!hc.isSkill); },
|
||||
// only enemies under her 战争技艺 in range: she holds her fire (and stores energy)
|
||||
canAttack(battle, u) {
|
||||
const ok = validTargets(battle, u, u.profile).length > 0;
|
||||
if (!ok) u.trait.hadTarget = false;
|
||||
return ok;
|
||||
// only enemies under her 战争技艺 in range: no valid target — she holds her fire (and stores energy)
|
||||
canAttack(battle, u) { return validTargets(battle, u, u.profile).length > 0; },
|
||||
// the mystic store (the shared profile calls it at her attack check with no valid target — professions.js
|
||||
// installMystic: an attack action, the interval restarts): one energy, 3 merge into a 转置能量; none once the
|
||||
// 转置能量 are at their cap (a full store: she idles)
|
||||
storeEnergy(battle, unit) {
|
||||
const v = stateOf(unit);
|
||||
if (v.t >= tMax) return false;
|
||||
if (++v.s >= merge) { v.s -= merge; v.t++; }
|
||||
return true;
|
||||
},
|
||||
install(battle, unit) {
|
||||
battle.on('deploy', (ctx) => { if (ctx.unit === unit) unit.trait.veen = null; }, { owner: unit });
|
||||
// the mystic store: her attack ready and no valid target ⇒ one energy, an attack action (the interval restarts);
|
||||
// 3 merge into a 转置能量 (the attack loop ran first this tick: a valid target was attacked instead)
|
||||
battle.on('tick', () => {
|
||||
const v = stateOf(unit);
|
||||
if (!up(unit) || !unit.canAct || unit.s.flags.disarm || unit.atkCd > 0 || v.t >= tMax) return;
|
||||
if (validTargets(battle, unit, unit.profile).length) return;
|
||||
if (++v.s >= merge) { v.s -= merge; v.t++; }
|
||||
unit.atkCd = unit.s.interval;
|
||||
}, { owner: unit });
|
||||
battle.on('death', (ctx) => { if (ctx.unit && ctx.unit.side === 'enemy') stateOf(unit).q.delete(ctx.unit.id); }, { owner: unit });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
// `[shield_core]`, `zuole_s_3` / `[shield]`, `zuole_e_002[resistance]`, `zuole_e_003_talent`, `zuole_e_003_trait`);
|
||||
// Arknights Terra Wiki "Zuo Le" (the barrier caps count each skill's own barrier only).
|
||||
// - Trait (武者) "不成为其他角色的治疗目标,每次攻击到敌人后回复自身70生命": the profession default (musha: no heal from
|
||||
// others, `value` per enemy hit; zuole_trait heals on every damage he outputs — the profession heals a normal attack
|
||||
// per target and any other damage per instance, so S1's extra strikes are separate damage instances).
|
||||
// others; zuole_trait heals on every damage he outputs — the profession heals `value` per damage instance, PRTS 分支特性信息
|
||||
// 武者, so S1's extra strikes heal too).
|
||||
// - T1 秉烛照影 "在场时,自身获得最高+50攻击速度和技力自然回复速度+2/秒的坚忍(损失70%生命值时达到最大加成)" (bb
|
||||
// min_attack_speed / min_sp_recovery_per_sec / min_hp_ratio): 坚忍 (ba.berserk "根据已损失的生命值获得相应比例的属性
|
||||
// 加成") — ASPD and SP recovery × min(1, lost HP ÷ (1 − min_hp_ratio)) while he is deployed. SBL-X stage 3: +70 /
|
||||
@@ -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.
|
||||
@@ -29,7 +30,8 @@
|
||||
// not while a 不死 already holds him ("_dontConsumeWhenUndeadable": 坚固维式重锤's window, items/battle.js holdsUndying).
|
||||
// - S1 破虏 (AUTO, 可充能 2 次): the next attack at atk_scale × ATK; "自身生命低于80%时额外攻击1次,低于50%时额外攻击2次"
|
||||
// (hp_ratio_double / hp_ratio_tripple): one / two more strikes on that target at the same scale, separate damage
|
||||
// instances [ASSUMED: the same target — nothing when it fell]. The data's DEFAULT trigger (an AUTO "next attack").
|
||||
// instances [ASSUMED: the same target — nothing when it fell; the HP read when the skill fires, before the strike's
|
||||
// own trait heal]. The data's DEFAULT trigger (an AUTO "next attack").
|
||||
// - S2 行险 (MANUAL, 12 s): "立即流失50%当前生命" (zuole_s_2[shield] DamageViaCurHpRatio, undeadable: never below 1 HP;
|
||||
// a 流失), then a barrier of scale × max HP (ba.barrier "可以吸收一定数值的伤害") added to the one he still has, capped at
|
||||
// max_scale × max HP at each gain (PRTS "上限于每次获取屏障时实时发生变化"; Terra: the cap counts this skill's barrier
|
||||
@@ -45,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';
|
||||
@@ -96,10 +98,14 @@ export default {
|
||||
skills: {
|
||||
[S1]: {
|
||||
kind: num(s1?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
// the HP that decides the extra strikes: when the skill fires, before its strike lands (the trait heals at each
|
||||
// damage instance — professions.js musha — so after the first strike he would read healed) [ASSUMED]
|
||||
onStart({ unit }) { unit.mem.zuoleS1Hp = unit.hpRatio; },
|
||||
attack: {
|
||||
atkScale: num(b1.atk_scale, 1),
|
||||
onHit({ battle, unit, target }) {
|
||||
const r = unit.hpRatio;
|
||||
const r = unit.mem.zuoleS1Hp ?? unit.hpRatio;
|
||||
unit.mem.zuoleS1Hp = null;
|
||||
const extra = (r < num(b1.hp_ratio_double, 0.8) ? 1 : 0) + (r < num(b1.hp_ratio_tripple, 0.5) ? 1 : 0);
|
||||
for (let i = 0; i < extra && target && target.alive; i++) {
|
||||
battle.dealDamage(unit, target, { amount: unit.s.atk * num(b1.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
|
||||
@@ -123,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]: {
|
||||
@@ -197,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; }
|
||||
@@ -206,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 });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default {
|
||||
// Talents 攫升 / 超脱: content/tokens.js mapCharTalents (the strategy's Touch too); the module's 超脱 upgrade (8 SP at
|
||||
// module Lv3) comes with the record's talents.
|
||||
// Module PHY-X (elite, trait addition): "治疗生命值低于50%的友方单位时治疗量提升15%" — every heal of hers on an ally
|
||||
// below `hp_ratio` HP (strictly: "低于") ×`heal_scale` (the trait blackboard). With S3's ×1.35 / ×1.40 it
|
||||
// below `hp_ratio` HP (strictly: "低于"; the client's acmedc_e_tr filters LT) ×`heal_scale` (the trait blackboard). With S3's ×1.35 / ×1.40 it
|
||||
// multiplies (×1.61 at Lv7): both are `heal_scale` blackboards, the 治疗倍率, and PRTS 游戏数据基础 §倍率 says "同种
|
||||
// 倍率间叠乘" — no Touch-specific note or buff template exists. S3's extra heal is a heal of hers like any other: its
|
||||
// recipient below `hp_ratio` gets the ×1.15 too (its base is taken before either bonus).
|
||||
|
||||
@@ -12,8 +12,10 @@ import { bodyInKeys } from '../../../body.js';
|
||||
const S1 = 'skchr_acspec_1';
|
||||
const S2 = 'skchr_acspec_2';
|
||||
const S3 = 'skchr_acspec_3';
|
||||
const S1_KEY = 'acspec:s1';
|
||||
const PULL_TAG = 'acspec:pull';
|
||||
/** A 被动 ON_DEPLOY skill with a duration: a duration skill every deployment starts (no SP, never cast otherwise). */
|
||||
const ON_DEPLOY = Object.freeze({ kind: 'duration', activateOnDeploy: true, spCost: 0, spType: 'none', trigger: 'NEVER' });
|
||||
const s1On = (unit) => !!(unit.skill && unit.skill.active && unit.skill.id === S1);
|
||||
/**
|
||||
* 四维分离's "周围四格": her tile and the four tiles beside it [ASSUMED: her own tile counts — an enemy she blocks stands
|
||||
* on it, and the talent is about her being alone with one enemy].
|
||||
@@ -63,9 +65,12 @@ export default {
|
||||
// attack@prob (PRTS 备注: it is her normal attack's basic logic; EXE-X levels 2 / 3 make it +atk ATK and a higher
|
||||
// chance). Talent 2 四维分离: ATK +atk while exactly `cnt` enemy stands on her tile or the four beside it.
|
||||
// All three skills are 被动, ON_DEPLOY — they start at every deployment (the battle-start one included, when no enemy
|
||||
// is on the field yet: S3 then hits nothing, as in the official mode; it matters on a redeploy):
|
||||
// S1 物理的服从: for the skill's duration (10 s) the talent-1 chance is attack@prob and physical dodge prob.
|
||||
// S2 战争的恭顺: for the skill's duration (10 s) ATK +atk, ASPD +attack_speed.
|
||||
// is on the field yet: S3 then hits nothing, as in the official mode; it matters on a redeploy). S1 / S2 last the
|
||||
// skill's duration (10 s): duration skills started by the deployment (`activateOnDeploy`, the deploy-timed skill
|
||||
// contract of PR #109 — their own skillStart / skillEnd and the client's skill on / off), not a passive holding a timed
|
||||
// buff:
|
||||
// S1 物理的服从: while it runs the talent-1 chance is attack@prob and physical dodge prob.
|
||||
// S2 战争的恭顺: while it runs ATK +atk, ASPD +attack_speed.
|
||||
// S3 空间的归依: spaceBurst above (ground only), right after each deployment (install).
|
||||
// Module EXE-X (trait atk): ATK +atk while no ally stands on the four tiles beside her (PRTS: unlike the usual EXE-X;
|
||||
// shared/tier4.js lonely, as 缄默德克萨斯's module of the same text).
|
||||
@@ -81,11 +86,11 @@ export default {
|
||||
const cnt = Math.max(1, Math.floor(num(t1.cnt, 1)));
|
||||
return {
|
||||
skills: {
|
||||
[S1]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: S1_KEY, duration: num(r1?.duration, 10), mods: { dodgePhys: num(b1.prob) }, tags: ['skill'] }); } },
|
||||
[S2]: { kind: 'passive', onStart({ battle, unit }) { battle.addBuff(unit, { key: 'acspec:s2', duration: num(r2?.duration, 10), mods: { atkPct: num(b2.atk), aspd: num(b2.attack_speed) }, tags: ['skill'] }); } },
|
||||
[S1]: { ...ON_DEPLOY, duration: num(r1?.duration, 10), mods: { dodgePhys: num(b1.prob) } },
|
||||
[S2]: { ...ON_DEPLOY, duration: num(r2?.duration, 10), mods: { atkPct: num(b2.atk), aspd: num(b2.attack_speed) } },
|
||||
[S3]: { kind: 'passive' },
|
||||
},
|
||||
trait: { hitsFn: (battle, unit) => (battle.rng.chance(unit.findBuff(S1_KEY) ? s1Prob : talentProb) ? 2 : 1) },
|
||||
trait: { hitsFn: (battle, unit) => (battle.rng.chance(s1On(unit) ? s1Prob : talentProb) ? 2 : 1) },
|
||||
talents: [
|
||||
{ install(battle, unit) { statBuff(battle, unit, 'acspec:t1', { atkPct: num(t0.atk) }); } },
|
||||
{ install(battle, unit) { toggleBuff(battle, unit, 'acspec:t2', () => enemiesAround4(battle, unit) === cnt, { atkPct: num(t1.atk) }); } },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -146,10 +146,16 @@ function pullToFront(battle, unit, e, force) {
|
||||
}
|
||||
|
||||
/** Physician module trait (录武官, 华法琳 elites): heals on allies below hp_ratio are ×heal_scale. */
|
||||
function installLowHpHealBonus(battle, unit, tb) {
|
||||
/**
|
||||
* The module trait "治疗生命值低于50%的友方单位时治疗量提升15%" (heal_scale, hp_ratio): the client's module buff filters the
|
||||
* target's HP before the heal with its own comparison — `atOrBelow` for the template heal_scale_up[hpratio][LE] (华法琳's
|
||||
* PHY-X), strictly below otherwise (录武官's reckpr_e_002_tr: LT). [ASSUMED as before: not on herself]
|
||||
*/
|
||||
function installLowHpHealBonus(battle, unit, tb, { atOrBelow = false } = {}) {
|
||||
const s = num(tb.heal_scale, 0), r = num(tb.hp_ratio, 0);
|
||||
if (!(s > 0) || !(r > 0)) return;
|
||||
battle.on('heal', (c) => { if (c.source === unit && c.target !== unit && c.target.hpRatio < r) c.amount *= s; }, { owner: unit });
|
||||
const low = atOrBelow ? (t) => t.hpRatio <= r + 1e-9 : (t) => t.hpRatio < r;
|
||||
battle.on('heal', (c) => { if (c.source === unit && c.target !== unit && low(c.target)) c.amount *= s; }, { owner: unit });
|
||||
}
|
||||
|
||||
// DESIGN §5.6 documents `(bb, chess)`; content/index.js also passes the normalised def — rebuild it when absent.
|
||||
|
||||
@@ -150,13 +150,18 @@ function burstDamageUp(battle, unit, mul) {
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
/** Modules PHY-X / GUA-X: heals on allies below hp_ratio ×heal_scale. */
|
||||
function lowHpHealUp(battle, unit, tb) {
|
||||
/**
|
||||
* Modules PHY-X / GUA-X "治疗生命值低于50%的友方单位时治疗量提升15%": heals on allies below hp_ratio ×heal_scale. The client's
|
||||
* module buff picks its comparison: `atOrBelow` for heal_scale_up[hpratio][LE] (华法琳's PHY-X), strictly below for 塞雷娅's
|
||||
* GUA-X (set_heal_scale_by_hpratio: LT) and 录武官's PHY-X (reckpr_e_002_tr: LT).
|
||||
*/
|
||||
function lowHpHealUp(battle, unit, tb, { atOrBelow = false } = {}) {
|
||||
const mul = num(tb.heal_scale), thr = num(tb.hp_ratio);
|
||||
if (!(mul > 1) || !(thr > 0)) return;
|
||||
const low = atOrBelow ? (t) => t.hpRatio <= thr + 1e-9 : (t) => t.hpRatio < thr;
|
||||
battle.on('heal', (c) => {
|
||||
if (c.source !== unit || c.opts?.regen || !c.target || c.target.side !== 'ally') return;
|
||||
if (c.target.hpRatio < thr) c.amount *= mul;
|
||||
if (low(c.target)) c.amount *= mul;
|
||||
}, { owner: unit });
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user