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:
commit
65ac4afbd8
32 files changed
+2432
-1959
No files matched your search
@@ -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 (铁灯盘)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} ·
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user