feedback5: merge W5C — 琳琅诗怀雅 S2 bomb on x-6 (#116), 新硎 / 沉沙 one 断刃 layer (#107), the owner's ACTIVE_RANGE trigger rule, AUTO-skill audit (伺夜 S2, 浊心斯卡蒂 S2)

# Conflicts:
#	data/backups.json
#	tools/build-data.mjs
This commit is contained in:
sganggs committed 2026-10-05 17:51:44 +08:00
commit 65ac4afbd8
32 files changed
+2432 -1959

No files matched your search

+20 -4
View File
@@ -17,16 +17,32 @@ const TEA_BOOM_RADIUS = 1.25;
// ---------------------------------------------------------------------------------------------------------------
// kits
/**
* 沉沙 / 新硎 断刃 (PRTS 天赋): 「攻击力+80%,持有4个【断刃】 / 每次成功攻击后消耗1个【断刃】,攻击结束时若已耗尽【断刃】,则立刻
* 切换为无断刃模式并失去攻击力加成」 (popup: the switch 「清空当次攻击间隔」, so it attacks again at once) / 「因坠落以外的原因死亡后…
* 召唤自身剩余【断刃】数量的铜矛头 / 铁矛头(至少召唤1个)」. ONE layer of Atkup.atk / AtkUp.atk (沉沙 0.7 — its PRTS page repeats
* 新硎's 80 %, the blackboard says 0.7 — / 新硎 0.8) from the spawn while it holds a blade (DeadSpawn.cnt = 4), removed when
* an attack spends the last one; that attack's cooldown is then cleared (ai.js sets it after the 'attack' hook, so this
* tick's 'tick' clears it and the next attack comes on the next tick). Unspent blades (cnt + cnt_add × spent, ≥ 1)
* become 矛头 when it is killed (a leak is no death; the mode has no 坠落), at once and side by side (spawnChildren — the
* 0.7 s random delay and the 1.0 square of the text are not modelled). Until 0.2.0 each attack stacked one more layer
* and none was ever removed (GitHub #107: 新硎 1350 → 3510 ATK after two hits).
*/
function kitBlades(ab) {
const cnt = T(ab, 'DeadSpawn.cnt') ?? 0, add = T(ab, 'DeadSpawn.cnt_add') ?? 0, atk = T(ab, 'Atkup.atk', 'AtkUp.atk') ?? 0;
const key = ab.tS['DeadSpawn.enemy_key'];
return [{
spawn(b, e, a) { a.used = 0; },
spawn(b, e, a) {
a.used = 0;
if (cnt > 0 && atk) b.addBuff(e, { key: 'ab:blade', mods: { atkPct: atk }, persist: true, visible: true });
},
attack(c, b, e, a) {
if (a.used >= cnt) return;
a.used++;
b.addBuff(e, { key: 'ab:blade', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, cnt), mods: { atkPct: atk }, persist: true, visible: true });
if (++a.used < cnt) return;
b.removeBuff(e, 'ab:blade'); // 无断刃模式
a.clearCd = true;
},
tick(b, e, a) { if (a.clearCd) { a.clearCd = false; e.atkCd = 0; } },
death(c, b, e, a) { if (c.reason === 'killed' && key) spawnChildren(b, e, key, Math.max(1, cnt + add * a.used)); },
}];
}
@@ -110,7 +126,7 @@ export const TIMES_KITS = Object.freeze({
enemy_1197_sfshu: kitDeathSpawn(), // 俗心 · death: 3 小说卷轴
enemy_1197_sfshu_2: kitDeathSpawn(), // 雅气 · death: 3 诗画卷轴
enemy_1199_sfjin: kitDeathSpawn(), // 身观 · death: 1 青铜镜 (taunt from data)
enemy_1207_sfji: kitBlades, // 沉沙 · each attack spends a blade (+ATK); death: unspent blades → 铜矛头 (≥1)
enemy_1207_sfji: kitBlades, // 沉沙 · +ATK while it holds a blade (4, one per attack); death: unspent blades → 铜矛头 (≥1)
enemy_1207_sfji_2: kitBlades, // 新硎 · same (铁矛头)
enemy_1209_sfden: kitInvisShield, // 清明 · veils nearby enemies (stealth) every 15 s; death: 铜灯盘
enemy_1209_sfden_2: kitInvisShield, // 堂皇 · same (铁灯盘)
+4 -2
View File
@@ -199,8 +199,10 @@ Each item is a mistake this project already made once. Tick every one for every
- [ ] **5. Auto-cast trigger** — the data resolves it (`tools/build-data.mjs resolveTrigger`, `data/chess.json`
`skills[].trigger`); check it, and change the data rule rather than hand-coding a trigger in the kit. The owner's
rules (0.2.0): a skill whose attack range is larger than the normal one casts as soon as an enemy is inside the
SKILL range (a deliberate deviation from the official 技能策略, like the 重装 exception; existing kits that predate
the rule, such as 烛煌 S3 in `feedback1e-skillrange.test.js`, are not examples of it); an AUTO skill that acts on
SKILL range (a deliberate deviation from the official 技能策略, like the 重装 exception: data `ACTIVE_RANGE` for a
MANUAL skill on the basic strategy whose running range strictly contains the operator's own,
`test/sim/feedback5-active-range.test.js`; 烛煌 S3 in `feedback1e-skillrange.test.js` keeps DEFAULT — its 4-11 does
not contain her 3-1); an AUTO skill that acts on
allies or itself fires at full SP (引星棘刺 S1, `kits_alt_t5.test.js` "… fires as soon as its SP is full, no enemy
needed (GitHub #124)"); the 重装 exception list (`TRIGGER_DEVIATIONS`: 深巡 S2, 雷蛇 S2, 号角 S2 / S3, 灰毫 S1 / S2,
余 S2; the 预备干员-重装 / Mechanist stand-ins: `STANDIN_TRIGGER_DEVIATIONS`, every skill), DESIGN §21.29 / §22.10,
@@ -11,6 +11,13 @@ import {
/** 琳琅诗怀雅 S3's coin range (PRTS 备注 "前方范围2-4"; range_table "2-4", facing right). */
const SWIRE2_COIN_GRID = Object.freeze([[1, 1], [0, 0], [0, 1], [0, 2], [-1, 1]]);
/**
* 琳琅诗怀雅 S2 “见面礼”'s placement range: range_table "x-6" — her tile and the two tiles beyond it in each of the four
* directions (symmetric: her facing does not matter). PRTS 备注 "香槟炸弹放置范围:x-6"; the client charpack (char_1033_swire2,
* mode S2) gives its SpawnToken selector `_rangeId` "x-6". The skill's own rangeId (1-1) is her attack range, not this.
*/
const SWIRE2_BOMB_GRID = Object.freeze([[2, 0], [1, 0], [0, -2], [0, -1], [0, 0], [0, 1], [0, 2], [-1, 0], [-2, 0]]);
/** Merchant trait with a callback on every successful DP payment (琳琅诗怀雅 大买家). Same rules as professions.js. */
function merchantInstall(onPay) {
return (battle, unit) => {
@@ -30,7 +37,8 @@ function merchantInstall(onPay) {
}
export default {
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): each attack spends a coin to drop a champagne bomb in range;
// ---- 3_04 琳琅诗怀雅 · 行商 — S2 “见面礼” (passive): with a coin and a free tile of range x-6 her attack's turn
// throws a champagne bomb instead (onto an enemy's tile when one stands there; installS2);
// 大买家: coin at skill start + coin & ATK stack per trait payment; 破财消灾: DP-paid revive (cost doubles)
// S1 仗义疏财 (passive, 2 coins): a coin heals the most injured ally (< 70 % HP) of the 8 surrounding tiles for
// attack@heal_scale × ATK — on her attack, or with no enemy to attack on her own attack timer (owner's decision
@@ -118,6 +126,53 @@ export default {
battle.on('attack', (ctx) => { if (ctx.attacker === unit) tryHeal(); }, { owner: unit, priority: -10 });
battle.on('tick', () => { if (unit.deployed && unit.canAct && !unit.trait?.hadTarget) tryHeal(); }, { owner: unit });
};
/**
* “见面礼” — 「消耗一枚金币在范围内一个可放置且可通行的地面放置香槟炸弹」. The client charpack (char_1033_swire2, mode S2)
* makes her attack a priority composite: first the SpawnToken ability (animation Skill_2) whose trigger needs BOTH a
* tile of its selector (range x-6; melee-buildable, walkable, low ground — the tile options of the selector) and her
* coin buff (swire2_can_use_gold), else her normal attack (which needs a target). So a bomb takes her attack's turn,
* enemies or not: with no enemy around she still spends her coins on the free tiles of x-6 (PRTS 备注 「否则随机放置香槟
* 至可部署的地块」), and only a full x-6 (or no coin) lets her coins pile up to the cap. Tile (PRTS 备注 「放置香槟动画进行
* 前若放置范围内存在敌人,则按攻击索敌规律放置香槟至其所在地块」; the selector's `_targetMotion` WALK): the ground enemies
* standing on a free tile of x-6, in her attack's target order (blocked first, …, nearest the goal) — the first one's
* tile; with none, a random free tile of x-6. A free tile: inside the field, nobody on it, no knocked-out operator's
* tile (freeTile; an enemy on it does not count), walkable ground a melee unit may stand on (groundTile).
* [ASSUMED] timing: the throw is instant and costs her attack one tick (PRTS 备注 「每次放置香槟时若正在攻击敌人,会短暂
* 延长攻击抬手间隔」 — the composite clears its cooldown when the throw ends; the length of the Skill_2 animation is in
* no table): `canAttack` holds her attack back while a throw is due and the 'tick' hook (after the attacks) throws
* when her attack is ready. Until 0.2.0 every attack also dropped a bomb on her 1-1 range, at random.
*/
const bombTiles = (battle, unit) => {
const out = [];
for (const k of gridKeys(SWIRE2_BOMB_GRID, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
if (freeTile(battle, r, c) && groundTile(battle, r, c)) out.push(k);
}
return out;
};
// (`bombHold`: a refused spawn — a tile freeTile let through — never holds her attack back for longer than one cycle)
const hasCoin = (battle, unit) => (unit.mem.coins ?? 0) >= coinCost && !(battle.time < (unit.mem.bombHold ?? -1));
const throwDue = (battle, unit) => hasCoin(battle, unit) && bombTiles(battle, unit).length > 0;
const installS2 = (battle, unit) => {
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
const ground = { ...unit.profile, canHitFly: false };
battle.on('tick', () => {
if (!alive(unit) || !unit.canAct || unit.atkCd > 1e-9 || unit.s.flags.disarm || !hasCoin(battle, unit)) return;
const keys = bombTiles(battle, unit);
if (!keys.length) return;
const free = new Set(keys);
let key = null;
for (const e of enemiesOn(battle, unit, keys, 0, ground)) {
const k = Math.round(e.y) * COLS + Math.round(e.x);
if (!e.isFlying && free.has(k)) { key = k; break; }
}
if (key == null) key = battle.rng.pick(keys);
const bomb = battle.spawnToken(unit, tokenId, (key / COLS) | 0, key % COLS, { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) { unit.mem.bombHold = battle.time + unit.s.interval; return; }
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit });
};
const installS3 = (battle, unit) => { // 千金一掷: "击倒敌人时获得一枚金币"
battle.on('kill', (ctx) => {
if (ctx.killer === unit && ctx.victim.side === 'enemy' && unit.skill?.active) addCoins(battle, unit, 1);
@@ -171,35 +226,24 @@ export default {
};
},
}),
trait: { install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}) },
trait: {
// S2: a throw due takes her attack's turn (installS2)
...(sel === S2 || sel == null ? { canAttack: (battle, unit) => !throwDue(battle, unit) } : {}),
install: merchantInstall((battle, unit) => {
// MER-Y "每次特性消耗费用时攻击力+4%,最多可以叠加5次" (any time, not only during the skill)
if (modTal && num(modTal.atk) > 0) {
battle.addBuff(unit, { key: 'trait:swire2_module', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(modTal.max_stack_cnt, 5)), mods: { atkPct: num(modTal.atk) } });
}
if (!unit.skill?.active) return; // "技能期间"
addCoins(battle, unit, num(t0.trait_sp, 1));
battle.addBuff(unit, { key: 'talent:swire2_buyer', refresh: 'stack', stacks: 1, maxStacks: Math.max(1, num(t0.max_stack_cnt, 8)), mods: { atkPct: num(t0.atk) } });
}),
},
install(battle, unit) {
if (sel === S1) { installS1(battle, unit); return; }
if (sel === S3) { installS3(battle, unit); return; }
if (sel !== S2 && sel != null) return;
const switchT = num(battle.tokenDef(tokenId, unit)?.skill?.bb?.duration_switch, 3);
battle.on('attack', (ctx) => {
if (ctx.attacker !== unit || !alive(unit) || (unit.mem.coins ?? 0) < coinCost) return;
const tiles = [];
for (const k of gridKeys(skillGrid ?? unit.rangeGrid, unit)) {
const r = (k / COLS) | 0, c = k % COLS;
// (never on the home tile of a dead operator: it could not redeploy until an enemy triggers the bomb)
if (freeTile(battle, r, c) && groundTile(battle, r, c)) tiles.push([r, c]);
}
const tile = battle.rng.pick(tiles);
if (!tile) return;
const bomb = battle.spawnToken(unit, tokenId, tile[0], tile[1], { untargetable: true, kit: bombKit(unit, switchT) });
if (!bomb) return;
unit.mem.coins -= coinCost;
fx(battle, 'summon', bomb, { src: unit.id, token: tokenId, coins: unit.mem.coins });
}, { owner: unit, priority: -10 });
installS2(battle, unit);
},
talents: [
{ install(battle, unit) { // 大买家: "开启技能时获得1枚金币" (a passive starts at every deployment, S3 when cast)
@@ -116,6 +116,11 @@ export default {
const dpGain = (battle, unit, n) => { if (n > 0) { battle.addDp(unit.ownerId, n); fx(battle, 'dp', unit, { n }); } };
// S2: the pack's empowered next attack (armed by the cast; ×scale on its hits; a kill pays once)
const installGift = (battle, unit) => {
battle.on('tick', () => { // the cast: ready, the pack on the field, no unused gift on it (PRTS 备注)
const sk = unit.skill, w = wolfOf(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 });
battle.on('beforeAttack', (ctx) => {
const w = wolfOf(unit);
if (!w || ctx.attacker !== w || !w.mem.vigilGift) return;
@@ -201,8 +206,12 @@ export default {
if (w) addShadow(battle, unit, w);
},
}),
// (自动触发, 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)
skchr_vigil_2: (s) => ({
kind: 'instant',
trigger: 'NEVER',
onStart({ battle, unit }) {
dpGain(battle, unit, num(s.bb.cost, 0));
const w = wolfOf(unit);
@@ -60,8 +60,10 @@ function skadi2(bb, chess, def) {
onStart({ battle, unit }) { unit.hp = unit.s.maxHp; battle.fx('heal', { x: unit.x, y: unit.y, id: unit.id }); },
},
// S2 同葬无光之愿 (toggle): 鼓舞 ATK / DEF = atk / def × her ATK / DEF on every other ally of her (+ 海嗣) range,
// trait heal attack@atk_to_hp_recovery_ratio (trait pulse)
skchr_skadi2_2: { kind: 'toggle' },
// trait heal attack@atk_to_hp_recovery_ratio (trait pulse). 自动触发 with effects on her allies only (nothing to target):
// on as soon as it is ready (SP_FULL, like 魔王 S1 往昔萦绕身旁); until 0.2.0 the data's DEFAULT left it off until an
// enemy came into her (or a 海嗣's) range.
skchr_skadi2_2: { kind: 'toggle', trigger: 'SP_FULL' },
};
return {
skills,
+5 -3
View File
@@ -16,9 +16,11 @@
// talents / traits read the resolved record, so a module choice ('none' ⇒ traitBase / talentsBase, module.active
// false) is honoured. Per-skill triggers come from data (the official 技能策略: every MANUAL 重装 skill ⇒ TAKE_DAMAGE —
// but the six of the owner's deliberate deviation, DESIGN §21.29 / tools/build-data.mjs TRIGGER_DEVIATIONS, which are
// DEFAULT —, a MANUAL skill with its own 技能范围 ⇒ SKILL_RANGE, AUTO skills keep their own rule); the few spec rules are
// documented at the skill (冲锋号令 AUTO ⇒ SP_FULL, 花香疗法 heal-type DEFAULT; the two 哨戒铁卫 S2s 深巡 行动能力剥夺 and
// 雷蛇 反击电弧 state the DEFAULT their data carries since that deviation — GitHub issue #4, PR #12).
// DEFAULT —, a MANUAL skill with its own 技能范围 ⇒ SKILL_RANGE, a MANUAL skill on the basic strategy whose running attack
// range strictly contains the own one ⇒ ACTIVE_RANGE (the owner's rule, 2026-10-05), AUTO skills keep their own rule);
// the few spec rules are documented at the skill (冲锋号令 AUTO ⇒ SP_FULL, 花香疗法 heal-type DEFAULT; the two 哨戒铁卫
// S2s 深巡 行动能力剥夺 and 雷蛇 反击电弧 state the DEFAULT their data carries since that deviation — GitHub issue #4,
// PR #12).
// Tests: test/content/kits_alt_t1.test.js.
//
// fx kinds emitted (battle.fx(kind, {x, y, …})): aoe {radius, id, skill} · zone {radius, dur, id, skill} ·
+2 -1
View File
@@ -23,7 +23,8 @@
// Lv7 elite blackboard, range, charges) — the loader picks the selected one. Talents / trait / install are shared: the
// parts that belong to one skill only (琳琅诗怀雅 bombs, 菲莱 counters, 雪猎 special bullets…) check the selected skill id.
// Triggers come from the data record of each skill (skills[i].trigger: tools/build-data.mjs resolveTrigger — the official
// 技能策略 incl. SKILL_RANGE for a MANUAL skill's own 技能范围 and the class rows for every MANUAL skill), except
// 技能策略 incl. SKILL_RANGE for a MANUAL skill's own 技能范围 and the class rows for every MANUAL skill; ACTIVE_RANGE, the
// owner's rule of 2026-10-05, for a MANUAL basic-strategy skill whose running range strictly contains the own one), except
// 薄绿 S1 (the 阵法术师 row SEARCH = "在初始攻击范围内存在敌人时" ⇒ the engine's DEFAULT for a phalanx, checked every
// tick) and the kits' own automatic casts (雪猎 special bullets). Non-default modules: 能天使 MAR-Y (ASPD vs ground), 琳琅诗怀雅 MER-Y (ATK per payment),
// 斯卡蒂 DRE-X (× vs blocked), 瑕光 GUA-X (heal × under 50 %), 伺夜 TAC-Y (×165 % trait, pack-blocked enemies taunt +1),
+3 -1
View File
@@ -26,7 +26,9 @@
// uses the engine's DEFAULT rule with `allies` / `hpAtMost` (about to attack + an ally of the skill area at ≤ half HP:
// the heal replaces that attack). Every other
// kit keeps the data rule (tools/build-data.mjs resolveTrigger, the official 技能策略: DEFAULT = about to attack + an
// enemy in the INITIAL range; SKILL_RANGE for a MANUAL skill's own 技能范围; the class rows — 重装 TAKE_DAMAGE … — for
// enemy in the INITIAL range; SKILL_RANGE for a MANUAL skill's own 技能范围; ACTIVE_RANGE for a MANUAL skill on the basic
// strategy whose running attack range strictly contains the own one (the owner's rule, 2026-10-05); the class rows —
// 重装 TAKE_DAMAGE … — for
// every MANUAL skill; AUTO skills never take a class row).
// - fx kinds emitted (battle.fx(kind, {x, y, id, …})): 'aoe' {r, skill}, 'healAoe' {r}, 'summon' {token}, 'anchor'
// {fromX, fromY, r}, 'teleport', 'zone' {r, duration}, 'iceSpike' {r}, 'extraAttack', 'downed', 'revive' {r},
+18 -6
View File
@@ -14,6 +14,10 @@
// too), checked every tick, no attack needed. Kit option `trigger.allies` (+ `hpAtMost`, default 1): a healable,
// injured ally of the grid whose HP ratio is at most that instead (an AUTO heal skill's own rule — 古米 S1 waits in
// its heal mode until it has healed);
// ACTIVE_RANGE — the owner's rule (2026-10-05, a deliberate deviation): a MANUAL skill on the basic strategy whose attack
// range while it runs strictly contains the unit's own range checks the DEFAULT condition on that larger range
// (trigger grid = the running range, grown by the unit's permanent rangeExtend unless the skill ignores 攻击距离), every
// tick, no attack needed — an enemy the unit can target there (or one it blocks), a heal skill an injured ally;
// TAKE_DAMAGE (ready + just took a hit: 重装 "不受技能范围影响,受到伤害时释放技能"), SP_FULL/ALWAYS (as soon as ready),
// CUSTOM_RANGE (enemy inside the custom trigger grid), SEARCH (an enemy inside the INITIAL range, checked every tick
// without waiting for an attack: "不受基础策略影响,在初始攻击范围内存在敌人时释放技能" — not any enemy on the field,
@@ -44,7 +48,7 @@
import { absoluteRangeKeys, canTargetEnemy } from './targeting.js';
import { AUTO_OP_COOLDOWN, COLS, ROWS } from './constants.js';
const TICK_RULES = new Set(['SP_FULL', 'SEARCH', 'CUSTOM_RANGE', 'SKILL_RANGE', 'GDGLOW_SKILL_2']);
const TICK_RULES = new Set(['SP_FULL', 'SEARCH', 'CUSTOM_RANGE', 'SKILL_RANGE', 'ACTIVE_RANGE', 'GDGLOW_SKILL_2']);
/** True when a SkillSpec `targeting` changes the unit's range while the skill runs (Battle._refreshRange). */
const changesRange = (tg) => !!(tg && (tg.rangeGrid || tg.rangeExtend || tg.noRangeExtend));
/** Enemies that satisfy a content trigger range (any targetable enemy, flyers included). */
@@ -325,15 +329,21 @@ export class SkillRuntime {
/** Public form of the operation cooldown, for kits with their own automatic cast of a MANUAL skill. */
get opCooling() { return this._opCooling(); }
/** Absolute tile keys of the trigger grid at the unit's current tile and direction (cached, with their Set). */
/**
* Absolute tile keys of the trigger grid at the unit's current tile and direction (cached, with their Set). ACTIVE_RANGE
* grows it by the unit's permanent rangeExtend (the range the skill would run with — Battle._refreshRange) unless the
* skill's range ignores 攻击距离 (targeting.noRangeExtend).
*/
_triggerKeys() {
const u = this.unit;
const tile = u.tileR * COLS + u.tileC;
if (!this._trigKeys || this._trigTile !== tile || this._trigDir !== u.dir) {
this._trigKeys = absoluteRangeKeys(this.triggerGrid, u.tileR, u.tileC, u.dir, 0);
const ext = this.rule === 'ACTIVE_RANGE' && !this.spec.targeting?.noRangeExtend ? (u.s.baseRangeExtend || 0) : 0;
if (!this._trigKeys || this._trigTile !== tile || this._trigDir !== u.dir || this._trigExt !== ext) {
this._trigKeys = absoluteRangeKeys(this.triggerGrid, u.tileR, u.tileC, u.dir, ext);
this._trigSet = new Set(this._trigKeys);
this._trigTile = tile;
this._trigDir = u.dir;
this._trigExt = ext;
}
return this._trigKeys;
}
@@ -364,6 +374,7 @@ export class SkillRuntime {
if (!this.triggerGrid) return this._defaultCondition();
return b.anyEnemyInKeys(this._triggerKeys());
}
if (this.rule === 'ACTIVE_RANGE') return this._defaultCondition(this.triggerGrid ? this._triggerKeys() : null);
// GDGLOW_SKILL_2 "全场存在可选目标时释放技能": a targetable enemy anywhere (heal skill: an ally that needs healing)
if (this.rule === 'GDGLOW_SKILL_2') {
if (this.healSkill) return b.injuredAlliesInKeys(ALL_TILES, u, !!u.profile?.heal?.elementHealRatio).length > 0;
@@ -375,12 +386,13 @@ export class SkillRuntime {
/**
* DEFAULT rule condition: an enemy (or injured ally for heal skills) inside the initial range (baseRangeKeys: own
* grid + permanent rangeExtend), or an enemy inside a content trigger range (addTriggerRange; not for heal skills).
* `range` (ACTIVE_RANGE): those absolute tile keys instead of the initial range.
*/
_defaultCondition() {
_defaultCondition(range = null) {
const b = this.battle;
const u = this.unit;
if (b.rangeChanged(u)) b._refreshRange(u);
const keys = u.baseRangeKeys || u.rangeKeys;
const keys = range || u.baseRangeKeys || u.rangeKeys;
if (keys) {
if (this.healSkill) return b.injuredAlliesInKeys(keys, u).length > 0;
if (b.enemiesInKeys(keys, u, u.profile).length > 0) return true;