feedback7 R22-OSIM: a skill that 白铁's 铁钳号 opens now acts on it — 蕾缪安 S3 / 艾丽妮 S3 / 陈 S2 / 引星棘刺 S2 spend on it, skills that pick enemies only stay closed; golden: unchanged
Grok's review of the 铁钳号 change (HIGH): the device opened every non-heal skill, but kits that pick their victims with enemiesInKeys only were spent on nothing — 蕾缪安 S3 went active with 0 locks, 艾丽妮 S3 and 陈 S2 struck nothing, 引星棘刺 S2 threw at the empty far tile. Audit: every skill of every chess (loadouts) and of the 自选 pool, placed with only the device in range; of the 351 skills the device opened, 67 never touched it. - skills.js `allyTargetsOk`: the device counts for a skill's start only if the skill acts on it — SkillSpec `allyTargets` (true / false) from the kit, else [ASSUMED] a skill acting through the unit's own attacks (timed and still attacking, or a cast that is the next attack) on a unit that attacks. An onStart-only cast or a no-attack timed skill picks enemies itself and waits for one (德克萨斯 S2 剑雨, 锡人 S1/S2, 铃兰 S3 … about 50 skills). - Spend on the device (allyTargets: true; its kit cancels the hits, its 受击回复 counts): 蕾缪安 S3 locks it and spends her bullets (ammoUsed, so the per-bullet layers come) and shells it; 艾丽妮 S3 strikes it; 陈 S2 draws on it after every enemy; 引星棘刺 S2 throws onto it and its pulses hit it. queries.js allyTargetsInRadius for their areas. - Explicitly closed ([ASSUMED], allyTargets: false): 普罗旺斯 S2 (never shoots above 80 % HP), 卡涅利安 S1 (charged: no attack), 山 S2 (blocked enemies), 余 S2 / S3, 迷迭香 S3 (blocked only), 荒芜拉普兰德 S1 / S2 (drone locks), 淬羽赫默 S1–S3 and 遥 S1–S3 (heal attacks), 机械师 S3, 珊比 S2 / S3, 陈 S3 / 艾丽妮 S2 (their guards already refused). - 雪猎's empty-magazine cast back to enemies only (its S2 does not act on the device). Re-audit: 281 skills open on the device alone and all act on it (蕾缪安 S3 through her locks); none of those that acted on it before stopped. Docs: SIM.md, design/engine.md, PLAYING.md. Tests: test/content/feedback7-allytarget-kits.test.js (5; all fail on the parent).
This commit is contained in:
1 parent
e733171b13
commit
69d791f9c6
21 files changed
+217
-23
No files matched your search
@@ -47,7 +47,8 @@ export class BattleQueries {
|
||||
* 铁钳号·原型机, a summon of the enemy camp that "可被我方干员攻击但不受伤害" (PRTS 铁钳号·原型机 备注 "该召唤物阵营为敌方";
|
||||
* kits/ops/op-ironmn.js). Those standing on one of `keys` (tile keys, or a Set of them), alive and deployed — never
|
||||
* `attacker` itself, and an ally target selects none. ai.js acquireTargets appends them after the enemies (their 嘲讽等级
|
||||
* −2 puts them last); a non-heal skill's automatic start counts them like an enemy (skills.js `_allyTargetIn`). [] at
|
||||
* −2 puts them last); a skill that acts on them counts them like an enemy for its automatic start (skills.js
|
||||
* `_allyTargetIn` / `allyTargetsOk`). [] at
|
||||
* once while none is registered, so every other battle runs exactly as before.
|
||||
*/
|
||||
allyTargetsInKeys(keys, attacker) {
|
||||
@@ -59,6 +60,19 @@ export class BattleQueries {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The registered ally targets within `r` of (x, y) (centre distance) — the radius form of allyTargetsInKeys, for a kit
|
||||
* whose area selection takes them too (a skill flagged `allyTargets`, skills.js allyTargetsOk). [] while none is registered.
|
||||
*/
|
||||
allyTargetsInRadius(x, y, r, attacker) {
|
||||
const set = this._allyTargets;
|
||||
if (!set || !set.size || !attacker || set.has(attacker)) return [];
|
||||
const r2 = r * r + 1e-9;
|
||||
const out = [];
|
||||
for (const a of set) if (a.alive && a.deployed && !a.hidden && (a.x - x) * (a.x - x) + (a.y - y) * (a.y - y) <= r2) out.push(a);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Register (`on`) or drop an ally target (allyTargetsInKeys); one off the field is skipped while registered. */
|
||||
setAllyTarget(unit, on = true) {
|
||||
if (!unit || unit.side !== 'ally') return false;
|
||||
|
||||
@@ -28,7 +28,9 @@ export default {
|
||||
const s2On = (u) => !!(u.skill && u.skill.active && u.skill.id === S2);
|
||||
return {
|
||||
skill: { kind: 'passive' },
|
||||
skills: { [S2]: { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, S2).atk) } } },
|
||||
// [ASSUMED] S2 never shoots 白铁's 铁钳号 (its damage is cancelled, so it never drops below 80 %): the device alone does
|
||||
// not open it (skills.js allyTargetsOk)
|
||||
skills: { [S2]: { kind: 'duration', mods: { atkPct: num(skillBbOf(chess, S2).atk) }, allyTargets: false } },
|
||||
trait: {
|
||||
canAttack: (battle, u) => !s2On(u) || battle.enemiesInKeys(u.rangeKeys, u, u.profile).some(huntable),
|
||||
},
|
||||
|
||||
@@ -58,9 +58,7 @@ export default {
|
||||
battle.on('tick', () => {
|
||||
const sk = unit.skill;
|
||||
if (!alive(unit) || !unit.canAct || !sk || !sk.ready || sk.opCooling || unit.s.flags.silence || (unit.trait.ammo ?? 1) > 0) return;
|
||||
// (白铁's 铁钳号·原型机, an ally target, counts like an enemy — skills.js `_allyTargetIn`)
|
||||
const keys = unit.baseRangeKeys || unit.rangeKeys;
|
||||
if (battle.enemiesInKeys(keys, unit, unit.profile).length || battle.allyTargetsInKeys(keys, unit).length) sk.activate('DEFAULT');
|
||||
if (battle.enemiesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit, unit.profile).length) sk.activate('DEFAULT');
|
||||
}, { owner: unit });
|
||||
}
|
||||
if (extra > 0) {
|
||||
|
||||
@@ -37,6 +37,8 @@ export default withDefaults({
|
||||
skills: alt(def, {
|
||||
skchr_billro_1: () => ({
|
||||
kind: 'duration',
|
||||
// [ASSUMED] a charged cast makes no attack at all: 白铁's 铁钳号 alone does not open it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
mods: { atkPct: num(bb.atk), defPct: num(bb.def) },
|
||||
onStart({ battle, unit, skill }) {
|
||||
unit.mem.billroCharged = isCharged(skill);
|
||||
|
||||
@@ -121,10 +121,13 @@ export default {
|
||||
}),
|
||||
skill: {
|
||||
kind: maxCharges(chess, def) > 1 ? 'charges' : 'instant',
|
||||
// 白铁's 铁钳号·原型机 (a registered ally target) is a target like an enemy when no enemy is in her range — the owner's
|
||||
// rule of 2026-10-08: the unit lands on it and its pulses hit it (cancelled by its kit) [ASSUMED] (skills.js allyTargetsOk)
|
||||
allyTargets: true,
|
||||
onStart({ battle, unit }) {
|
||||
const list = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
|
||||
sortEnemyTargets(battle, unit, list, null);
|
||||
let t = list.find((e) => !e.isFlying) ?? list[0];
|
||||
let t = list.find((e) => !e.isFlying) ?? list[0] ?? battle.allyTargetsInKeys(unit.rangeKeys, unit)[0];
|
||||
if (!t) {
|
||||
// PRTS 备注: no enemy in range → thrown at the farthest tile straight ahead inside her range
|
||||
let best = null;
|
||||
@@ -191,6 +194,8 @@ export default {
|
||||
if (z.acc >= 1 - 1e-9) {
|
||||
z.acc -= 1;
|
||||
for (const e of foes) battle.dealDamage(unit, e, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'alchemy'] });
|
||||
// (白铁's 铁钳号 too, like a ground enemy — its kit cancels the hit)
|
||||
for (const a of battle.allyTargetsInRadius(z.x, z.y, r, unit)) battle.dealDamage(unit, a, { amount: unit.s.atk * num(bb.atk_scale), type: 'arts', isSkill: true, tags: ['skill', 'alchemy'] });
|
||||
const regen = unit.s.atk * num(bb.hp_recovery_per_sec_ratio_chr); // 生命回复速度 (see the header)
|
||||
if (regen > 0) {
|
||||
for (const a of battle.alliesInRadius(z.x, z.y, r, null)) {
|
||||
|
||||
@@ -39,6 +39,9 @@ export default {
|
||||
}),
|
||||
skill: {
|
||||
kind: 'toggle',
|
||||
// [ASSUMED] its attacks reach the enemies he blocks; the unblockable 铁钳号 of 白铁 never: it does not open it (skills.js
|
||||
// allyTargetsOk)
|
||||
allyTargets: false,
|
||||
mods: mods({ atkPct: num(bb.atk), defPct: num(bb.def), blockCnt: num(bb.block_cnt), hpRegenRatio: num(bb.hp_recovery_per_sec_by_max_hp_ratio) }),
|
||||
targeting: skillGrid(chess, def) ? { rangeGrid: skillGrid(chess, def) } : undefined,
|
||||
attack: { hitAllBlocked: true },
|
||||
|
||||
@@ -58,7 +58,11 @@ function lemuen(bb, chess, def) {
|
||||
const d2 = num(bb['attack@dist_2'], num(bb['attack@projectile_range'], 1.5));
|
||||
const s1 = num(bb['attack@proj_atk_scale_1'], 1), s2 = num(bb['attack@proj_atk_scale_2'], 1);
|
||||
const pickLock = (battle, unit, locks) => {
|
||||
const cands = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
|
||||
let cands = battle.enemiesInKeys(unit.rangeKeys, unit, unit.profile);
|
||||
// 白铁's 铁钳号·原型机 (a registered ally target, an enemy-camp summon) is locked like an enemy when no enemy is in her
|
||||
// range — the owner's rule of 2026-10-08 (it draws aggro like an enemy): her bullets are spent on it, the shells' hits
|
||||
// are cancelled by it [ASSUMED: after every enemy, its 嘲讽等级 −2]
|
||||
if (!cands.length) cands = battle.allyTargetsInKeys(unit.rangeKeys, unit);
|
||||
if (!cands.length) return null;
|
||||
sortEnemyTargets(battle, unit, cands, 'lowDef');
|
||||
const cnt = (e) => locks.reduce((n, L) => n + (L.e === e ? 1 : 0), 0);
|
||||
@@ -85,6 +89,10 @@ function lemuen(bb, chess, def) {
|
||||
const d = bodyDist(e, x, y);
|
||||
battle.dealDamage(unit, e, { amount: atk * (d <= d1 + 1e-9 ? s1 : s2), type: 'phys', isSkill: true, isSplash: true, tags: ['skill', 'bombard'] });
|
||||
}
|
||||
// (the 铁钳号 too, like an enemy — its kit cancels the hit)
|
||||
for (const a of battle.allyTargetsInRadius(x, y, d2, unit)) {
|
||||
battle.dealDamage(unit, a, { amount: atk * s2, type: 'phys', isSkill: true, isSplash: true, tags: ['skill', 'bombard'] });
|
||||
}
|
||||
};
|
||||
const bombard = (battle, unit, locks) => {
|
||||
const atk = unit.s.atk;
|
||||
@@ -164,6 +172,7 @@ function lemuen(bb, chess, def) {
|
||||
kind: 'ammo',
|
||||
ammo: Math.max(1, Math.floor(num(bb['attack@trigger_time'], 5))),
|
||||
attack: { noAttack: true },
|
||||
allyTargets: true, // its locks take 白铁's 铁钳号 too (pickLock) — skills.js allyTargetsOk
|
||||
onStart({ unit }) { unit.mem.lemLocks = []; unit.mem.lemAcc = aim; },
|
||||
onTick({ battle, unit, skill, dt }) {
|
||||
const m = unit.mem;
|
||||
|
||||
@@ -51,6 +51,8 @@ function yu(bb, chess, def) {
|
||||
// 'teleport' fx). PRTS's 0.13 s between the damage and the teleport is not modelled (same tick).
|
||||
skchr_yu_2: {
|
||||
kind: 'duration',
|
||||
// [ASSUMED] its burst and teleport pick enemies; 白铁's 铁钳号 alone does not open it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), blockCnt: num(bb.block_cnt) },
|
||||
attack: { dmgType: 'arts' },
|
||||
onStart({ battle, unit }) {
|
||||
@@ -79,6 +81,7 @@ function yu(bb, chess, def) {
|
||||
skill: {
|
||||
kind: 'duration',
|
||||
mods: { hpPct: num(bb.max_hp), atkPct: num(bb.atk), defPct: num(bb.def) },
|
||||
allyTargets: false, // [ASSUMED] its wall burns enemies crossing it; 白铁's 铁钳号 alone does not open it
|
||||
// the wall runs through his tile perpendicular to his direction (RIGHT / LEFT: his column; UP / DOWN: his row —
|
||||
// it turns with the deploy direction like a range) [ASSUMED orientation for UP / DOWN]
|
||||
onStart({ battle, unit }) {
|
||||
|
||||
@@ -31,6 +31,8 @@ function rosmon(bb, chess, def) {
|
||||
// two 战术装备 on melee tiles of her range (the token kit: appear stun, blocked enemies DEF −160, lifetime)
|
||||
skchr_rosmon_3: {
|
||||
kind: 'duration',
|
||||
// [ASSUMED] blocked enemies only; 白铁's unblockable 铁钳号 never: it does not open it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
mods: { batPct: batOf(bb.base_attack_time, def, true), atkPct: num(bb.atk) },
|
||||
targeting: { maxTargets: Math.max(1, Math.floor(num(bb['attack@max_target'], 2))) },
|
||||
onStart({ battle, unit }) {
|
||||
|
||||
@@ -128,6 +128,8 @@ function whitw2(bb, chess, def) {
|
||||
// grid only selects those targets — no rangeId, no 攻击范围 in the text — so the card keeps her 3-1 (showOwnRange)
|
||||
skchr_whitw2_1: {
|
||||
kind: 'toggle',
|
||||
// [ASSUMED] her drones lock enemies only (install): 白铁's 铁钳号 alone does not open S1 / S2 (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
mods: { atkPct: num(bb.atk) },
|
||||
targeting: { rangeGrid: WHOLE_FIELD, showOwnRange: true },
|
||||
onStart({ unit }) { unit.mem.lazyLock = null; },
|
||||
@@ -136,6 +138,7 @@ function whitw2(bb, chess, def) {
|
||||
// (install); each drone hit ramps on its own target (trait init → max) and fears it attack@fear s with attack@prob
|
||||
skchr_whitw2_2: {
|
||||
kind: 'duration',
|
||||
allyTargets: false,
|
||||
mods: { atkPct: num(bb.atk) },
|
||||
...(skillGridW ? { targeting: { rangeGrid: skillGridW } } : {}),
|
||||
attack: {
|
||||
|
||||
@@ -114,9 +114,13 @@ export default {
|
||||
},
|
||||
[S2]: {
|
||||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
// 白铁's 铁钳号·原型机 (a registered ally target) is drawn on like an enemy, after every enemy (嘲讽等级 −2) — the
|
||||
// owner's rule of 2026-10-08; its kit cancels the hits [ASSUMED] (skills.js allyTargetsOk)
|
||||
allyTargets: true,
|
||||
onStart({ battle, unit }) {
|
||||
const list = battle.enemiesInKeys(keysOf(unit, grid2), unit, AIR);
|
||||
sortEnemyTargets(battle, unit, list, null);
|
||||
list.push(...battle.allyTargetsInKeys(keysOf(unit, grid2), unit));
|
||||
const v = list.slice(0, Math.max(1, Math.floor(num(b2.max_target, 1))));
|
||||
battle.fx('slash', { x: unit.x, y: unit.y, id: unit.id, n: v.length, skill: 'chen:draw' });
|
||||
const amount = () => unit.s.atk * num(b2.atk_scale, 1);
|
||||
@@ -132,6 +136,9 @@ export default {
|
||||
duration: S3_CLIP,
|
||||
flags: { invulnerable: true, noBlock: true },
|
||||
attack: { noAttack: true },
|
||||
// [ASSUMED] its slashes and its guard (guardActivation) take ground enemies only: 白铁's 铁钳号 alone does not open
|
||||
// it (skills.js allyTargetsOk) — the skill ends at once without a target
|
||||
allyTargets: false,
|
||||
onStart({ battle, unit }) {
|
||||
battle.releaseBlocked(unit); // 无法阻挡
|
||||
unit.mem.chenS3 = { t: 0, n: 0, target: null };
|
||||
|
||||
@@ -168,9 +168,10 @@ export default {
|
||||
return {
|
||||
trait: atkTargets > 1 ? { maxTargets: atkTargets } : null,
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) }, attack: healAttack(healTargets) },
|
||||
// [ASSUMED] her skills' attacks heal: 白铁's 铁钳号 (禁疗, no patient) alone does not open them (skills.js allyTargetsOk)
|
||||
[S1]: { kind: 'duration', mods: { aspd: num(b1.attack_speed) }, attack: healAttack(healTargets), allyTargets: false },
|
||||
[S2]: {
|
||||
kind: 'duration', attack: healAttack(healTargets + Math.max(0, Math.floor(num(b2['attack@max_target_heal_add'])))),
|
||||
kind: 'duration', attack: healAttack(healTargets + Math.max(0, Math.floor(num(b2['attack@max_target_heal_add'])))), allyTargets: false,
|
||||
onStart({ battle, unit }) {
|
||||
unit.mem.harukaS2Uses = (unit.mem.harukaS2Uses ?? 0) + 1;
|
||||
unit.mem.harukaS2Endless = unit.mem.harukaS2Uses >= 2;
|
||||
@@ -185,6 +186,7 @@ export default {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
|
||||
targeting: { rangeGrid: s3?.rangeGrid ?? null },
|
||||
allyTargets: false,
|
||||
attack: healAttack(healTargets),
|
||||
onStart({ battle, unit }) { rescale(battle, unit); },
|
||||
onEnd({ battle, unit }) { rescale(battle, unit); }, // (`active` is already false: ×1 again)
|
||||
|
||||
@@ -100,6 +100,9 @@ export default {
|
||||
},
|
||||
[S2]: {
|
||||
kind: num(s2?.maxChargeTime, 1) > 1 ? 'charges' : 'instant',
|
||||
// [ASSUMED] its strike and its guard (guardActivation) take ground enemies only: 白铁's 铁钳号 alone does not open
|
||||
// it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
onStart({ battle, unit }) {
|
||||
const list = battle.enemiesInKeys(keysOf(unit, grid2), unit, GROUND);
|
||||
sortEnemyTargets(battle, unit, list, null);
|
||||
@@ -116,9 +119,12 @@ export default {
|
||||
kind: 'duration',
|
||||
duration: S3_DURATION,
|
||||
attack: { noAttack: true },
|
||||
// 白铁's 铁钳号·原型机 (a registered ally target) is struck like an enemy — the owner's rule of 2026-10-08; its kit
|
||||
// cancels the hits [ASSUMED: its strikes take it like a ground enemy of the skill range] (skills.js allyTargetsOk)
|
||||
allyTargets: true,
|
||||
onStart({ battle, unit }) {
|
||||
unit.mem.ireneS3 = { t: 0, n: 0 };
|
||||
const v = battle.enemiesInKeys(keysOf(unit, grid3), unit, GROUND);
|
||||
const v = [...battle.enemiesInKeys(keysOf(unit, grid3), unit, GROUND), ...battle.allyTargetsInKeys(keysOf(unit, grid3), unit)];
|
||||
battle.fx('aoe', { x: unit.x, y: unit.y, radius: 1.5, id: unit.id, skill: 'irene:judgment' });
|
||||
for (const e of v) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b3.atk_scale, 1), type: 'phys', isSkill: true, tags: ['skill'] });
|
||||
@@ -132,11 +138,12 @@ export default {
|
||||
const iv = Math.max(0.05, num(b3.multi_hit_interval, 0.3)), times = Math.max(0, Math.floor(num(b3.multi_times, 10)));
|
||||
while (m.n < times && m.t + 1e-9 >= (m.n + 1) * iv) {
|
||||
m.n++;
|
||||
const cands = battle.enemiesInKeys(keysOf(unit, grid3), unit, AIR);
|
||||
const foes = battle.enemiesInKeys(keysOf(unit, grid3), unit, AIR);
|
||||
const cands = foes.length ? foes : battle.allyTargetsInKeys(keysOf(unit, grid3), unit); // (the 铁钳号 when no enemy)
|
||||
if (!cands.length) continue;
|
||||
const c = battle.rng.pick(cands);
|
||||
battle.fx('aoe', { x: c.x, y: c.y, radius: S3_SPLASH, id: unit.id, skill: 'irene:judgment' });
|
||||
for (const e of battle.foesInRadius(c.x, c.y, S3_SPLASH, true)) {
|
||||
for (const e of [...battle.foesInRadius(c.x, c.y, S3_SPLASH, true), ...battle.allyTargetsInRadius(c.x, c.y, S3_SPLASH, unit)]) {
|
||||
battle.dealDamage(unit, e, { amount: unit.s.atk * num(b3.multi_atk_scale, 1), type: 'phys', isSkill: true, isSplash: e !== c, tags: ['skill'] });
|
||||
}
|
||||
if (!unit.alive || !unit.skill?.active) return;
|
||||
|
||||
@@ -269,6 +269,8 @@ export default {
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
mods: { atkPct: num(b3.atk), batPct: batMod(b3.base_attack_time, chess) },
|
||||
// [ASSUMED] the cross lands on an enemy target only: 白铁's 铁钳号 alone does not open it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
// the attack is the cross: no damage on the target itself (its 0.8 s later landing does it)
|
||||
attack: { dmgType: 'arts', hitsFn: () => 0 },
|
||||
onAttack({ battle, unit, targets }) {
|
||||
|
||||
@@ -158,9 +158,10 @@ export default {
|
||||
return {
|
||||
trait: targets > 1 ? { maxTargets: targets } : null,
|
||||
skills: {
|
||||
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) }, attack: healAttack },
|
||||
// [ASSUMED] her skills' attacks heal: 白铁's 铁钳号 (禁疗, no patient) alone does not open them (skills.js allyTargetsOk)
|
||||
[S1]: { kind: 'duration', mods: { atkPct: num(b1.atk) }, attack: healAttack, allyTargets: false },
|
||||
[S2]: {
|
||||
kind: 'duration', mods: { aspd: num(b2.attack_speed) }, attack: healAttack,
|
||||
kind: 'duration', mods: { aspd: num(b2.attack_speed) }, attack: healAttack, allyTargets: false,
|
||||
onStart({ battle, unit }) {
|
||||
// a 夜灯 already up (the battle-start deploy) is this skill's drone; else her placed piece takes the field
|
||||
if (!battle.allyUnits.some((t) => t.kind === 'token' && t.defId === LAMP && t.ownerUnit === unit && t.alive)) releaseSkillSummon(battle, unit, LAMP);
|
||||
@@ -168,7 +169,7 @@ export default {
|
||||
onEnd({ battle, unit }) { withdrawLamps(battle, unit); },
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration', mods: { atkPct: num(b3.atk) }, attack: healAttack,
|
||||
kind: 'duration', mods: { atkPct: num(b3.atk) }, attack: healAttack, allyTargets: false,
|
||||
onStart({ unit }) { unit.mem.slent2Grave = true; }, // the one 不死 of this cast (slent2_shallow_grave[self])
|
||||
onEnd({ unit }) { unit.mem.slent2Grave = false; },
|
||||
},
|
||||
|
||||
@@ -177,6 +177,8 @@ export default {
|
||||
},
|
||||
[S2]: {
|
||||
kind: 'duration',
|
||||
// [ASSUMED] its glue / belt act on ground enemies only: 白铁's 铁钳号 alone does not open it (skills.js allyTargetsOk)
|
||||
allyTargets: false,
|
||||
// the owner's rule for a skill whose area is larger than her range (her tile): cast as soon as a ground enemy she
|
||||
// can affect is on the glue's first five tiles — her tile, the front one, its two sides and the one beyond
|
||||
// (glueTiles' PRTS order), as 余 S2's x-1 (§22.10); the data's DEFAULT (重装 exception) only saw her own tile
|
||||
@@ -200,6 +202,7 @@ export default {
|
||||
},
|
||||
[S3]: {
|
||||
kind: 'duration',
|
||||
allyTargets: false,
|
||||
// likewise on the conveyor: her tile and the four ahead (the owner's larger-area rule; 余 S2 precedent)
|
||||
trigger: { rule: 'ACTIVE_RANGE', grid: [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4]] },
|
||||
mods: nz({ atkPct: num(b3.atk), defPct: num(b3.def), blockCnt: num(b3.block_cnt) }),
|
||||
|
||||
+26
-3
@@ -30,7 +30,9 @@
|
||||
// rule AND such an ally on the trigger grid — the cast replaces the attack about to be made (塞雷娅 S1 "触发时会替换当
|
||||
// 次攻击"); should that ally condition fail before the attack, the cast is withdrawn and its charge returned.
|
||||
// A registered ally target (Battle.setAllyTarget: 白铁's 铁钳号·原型机, an enemy-camp summon our attacks select) counts
|
||||
// like an enemy in each of these enemy conditions for a non-heal skill (`_allyTargetIn` — the owner's rule of 2026-10-08).
|
||||
// like an enemy in each of these enemy conditions for a skill that acts on it (`_allyTargetIn`, `allyTargetsOk` — the
|
||||
// owner's rule of 2026-10-08): one that acts through the unit's own attacks, or whose kit flags `allyTargets: true` (its
|
||||
// selectors take ally targets too); a skill that picks its own victims among enemies only is not opened by it.
|
||||
// Automatic operations cool down (constants.js AUTO_OP_COOLDOWN, "自动操作具有3s冷却,在完成一次操作或作战开始时部署的单位
|
||||
// 将进入冷却"): the engine auto-casts a MANUAL skill (def.skillType) no sooner than 3 s after its previous cast (so a
|
||||
// charged skill spends its charges 3 s apart) or after the unit's deployment at the battle start (Battle._deploy
|
||||
@@ -44,7 +46,8 @@
|
||||
// mods, flags, targeting {maxTargets, rangeGrid, priority, allInRange, rangeExtend, noRangeExtend (the range ignores
|
||||
// the unit's 攻击距离), showOwnRange (rangeGrid only selects targets: the detail card keeps the unit's own range)},
|
||||
// attack {dmgType, atkScale, splashRadius, splashScale, hits, projectile, maxTargets, dmgMul, onHit, heal…},
|
||||
// heal (bool: heal-type skill for the trigger rule), onStart(ctx), onEnd(ctx), onHit(ctx), onAttack(ctx), onTick(ctx).
|
||||
// heal (bool: heal-type skill for the trigger rule), allyTargets (bool: the skill acts on a registered ally target — see
|
||||
// `allyTargetsOk`), onStart(ctx), onEnd(ctx), onHit(ctx), onAttack(ctx), onTick(ctx).
|
||||
// ctx passed to spec callbacks: { battle, unit, skill, bb, target?, dealt?, targets?, dt?, reason? }; onAttack's ctx
|
||||
// also carries `noAmmo` (set it to true: this attack spends no ammo). onEnd runs while the skill's mods / range are
|
||||
// still applied (`active` is already false); they are removed right after it (unless onEnd re-activated the skill).
|
||||
@@ -177,11 +180,31 @@ export class SkillRuntime {
|
||||
* canTargetEnemy (every other selector keeps to enemies); a heal skill never counts it (禁疗: not a patient).
|
||||
*/
|
||||
_allyTargetIn(keys) {
|
||||
if (this.healSkill || !keys) return false;
|
||||
if (!keys || !this.allyTargetsOk) return false;
|
||||
const b = this.battle;
|
||||
return !!(b._allyTargets && b._allyTargets.size) && b.allyTargetsInKeys(keys, this.unit).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this skill act on a registered ally target, so that one may start it (`_allyTargetIn`)? Never a heal skill.
|
||||
* SkillSpec `allyTargets` (true / false) answers for a kit: true where its selectors take ally targets too
|
||||
* (allyTargetsInKeys / allyTargetsInRadius — 蕾缪安 S3, 艾丽妮 S3, 陈 S2, 引星棘刺 S2), false where they never do although
|
||||
* the skill keeps attacking. Without it: a skill that acts through the unit's own attacks — which select the device
|
||||
* (ai.js acquireTargets) —, a timed one that keeps attacking (no `attack.noAttack`) or a cast that is the next attack
|
||||
* (`spec.attack`), on a unit that attacks. [ASSUMED] Any other skill picks its victims itself among the enemies
|
||||
* (enemiesInKeys, foesInRadius …) and would be spent on nothing: the device does not open it — it waits for an enemy.
|
||||
* (Grok's review of round 22: 蕾缪安 S3 / 艾丽妮 S3 / 陈 S2 / 引星棘刺 S2 opened on the device alone and did nothing.)
|
||||
*/
|
||||
get allyTargetsOk() {
|
||||
if (this.healSkill || this.noSkill || this.kind === 'passive') return false;
|
||||
const s = this.spec;
|
||||
if (s.allyTargets != null) return !!s.allyTargets;
|
||||
const p = this.unit.profile;
|
||||
if (p && p.noAttack) return false;
|
||||
if (s.attack && s.attack.noAttack) return false;
|
||||
return this.isTimed || !!s.attack;
|
||||
}
|
||||
|
||||
get spCost() {
|
||||
const c = Math.floor(this.baseSpCost * this.spCostMul + (this.unit.s ? this.unit.s.spCostFlat : 0));
|
||||
// content writes spCostMul / spCostFlat: a NaN/Infinity cost would freeze SP forever — use the base cost instead
|
||||
|
||||
Reference in new issue
Block a user