feedback7 R22-OSIM: 白铁's 铁钳号·原型机 counts like an enemy for a non-heal skill's automatic start; golden: unchanged

Community report relayed by the owner: 圣约送葬人 hitting 白铁's device spent no bullet and gained no layer.
Our attacks already selected the device (Battle.setAllyTarget, damage cancelled), but every skill start
condition counted enemies only, so her MANUAL ammo skill (DEFAULT) never opened. The owner's rule of
2026-10-08: a registered ally target in range counts like an enemy for a non-heal skill's auto start.

- skills.js `_allyTargetIn`: DEFAULT (about to attack, the initial range; SEARCH / ACTIVE_RANGE through
  it), CUSTOM_RANGE, SKILL_RANGE, GDGLOW_SKILL_2 (anywhere) and the content trigger ranges. Heal skills
  never count it (禁疗). It stays out of enemiesInKeys / canTargetEnemy.
- 雪猎's empty-magazine S2 cast counts it too; allyTargetsInKeys takes a Set of tile keys.
- 白铁™多功能平台 stays no target. Kits whose own selectors pick enemies (陈 S3, 艾丽妮 S2 guards,
  蕾缪安 S3 locks) are unchanged.

Tests: test/sim/feedback7-allytarget-cast.test.js (4; 3 fail on the parent).
This commit is contained in:
sganggs committed 2026-10-08 19:36:35 +08:00
1 parent ae22335cfe
commit 37e45e362e
8 files changed
+158 -16

No files matched your search

+5 -4
View File
@@ -45,14 +45,15 @@ export class BattleQueries {
/**
* Ally units an ally's attack selects like an enemy — the "ally targets" content registers (setAllyTarget): 白铁's
* 铁钳号·原型机, a summon of the enemy camp that "可被我方干员攻击但不受伤害" (PRTS 铁钳号·原型机 备注 "该召唤物阵营为敌方";
* kits/ops/op-ironmn.js). Those standing on one of `keys`, 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). [] at once
* while none is registered, so every other battle runs exactly as before.
* 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
* once while none is registered, so every other battle runs exactly as before.
*/
allyTargetsInKeys(keys, attacker) {
const set = this._allyTargets;
if (!set || !set.size || !keys || !attacker || set.has(attacker)) return [];
const ks = keys === attacker.rangeKeys && attacker.rangeKeySet ? attacker.rangeKeySet : new Set(keys);
const ks = keys instanceof Set ? keys : (keys === attacker.rangeKeys && attacker.rangeKeySet ? attacker.rangeKeySet : new Set(keys));
const out = [];
for (const a of set) if (a.alive && a.deployed && !a.hidden && ks.has(a.tileR * COLS + a.tileC)) out.push(a);
return out;
@@ -58,7 +58,9 @@ 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;
if (battle.enemiesInKeys(unit.baseRangeKeys || unit.rangeKeys, unit, unit.profile).length) sk.activate('DEFAULT');
// (白铁'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');
}, { owner: unit });
}
if (extra > 0) {
+2 -1
View File
@@ -56,7 +56,8 @@
// 白铁's S2 runs: s2.interval s and s2.hp_ratio (both timers restart when the mode changes, as the client re-creates the
// buffs on its mode switch).
// - 铁钳号·原型机 (S3), an enemy-camp summon: our attacks select it like an enemy, after every real one (嘲讽等级 −2;
// Battle.setAllyTarget — the engine extension of this kit), enemies never (untargetable); every damage it takes is cancelled
// Battle.setAllyTarget — the engine extension of this kit), and a non-heal skill's automatic start counts it like an enemy
// (skills.js `_allyTargetIn`, the owner's rule of 2026-10-08), enemies never (untargetable); every damage it takes is cancelled
// and gives it 1 SP (受击回复), and +hp_ratio of its max HP when the source is a 工匠 operator (trait, ignores 禁疗). 团结的力
// 量: the operator on its b-1 (its tile and the one behind it) takes physical damage ×(1 − damage_resistance) — one effect per
// device. Its skill (AUTO, 5 SP, data DEFAULT — it never attacks, so the engine checks it every tick: an enemy, flyers too, in
+25 -6
View File
@@ -29,6 +29,8 @@
// (noAttack profiles) evaluate DEFAULT every tick instead. DEFAULT with `trigger.allies` (+ `hpAtMost`): the basic
// 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).
// 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
@@ -161,12 +163,25 @@ export class SkillRuntime {
if (Array.isArray(x)) keys = x;
else if (x && typeof x === 'object' && x.side === 'ally' && x.alive && x.deployed && !x.hidden) keys = x.rangeKeys;
else if (x && typeof x === 'object' && Array.isArray(x.keys)) { keys = x.keys; if (x.profile) prof = x.profile; }
if (keys && keys.length && b.enemiesInKeys(keys, u, prof).length) return true;
if (keys && keys.length && (b.enemiesInKeys(keys, u, prof).length || this._allyTargetIn(keys))) return true;
}
}
return false;
}
/**
* A registered ally target (Battle.setAllyTarget: 白铁's 铁钳号·原型机, a summon of the enemy camp our attacks select) on
* `keys` (tile keys or a Set): a non-heal skill's automatic start counts it like an enemy — the owner's rule of
* 2026-10-08 (a community report and their recall of the official mode: 干员攻击范围/技能范围内有白铁的装置时技能也自动开、自动
* 消耗,装置对干员的仇恨类似敌方单位; PRTS 铁钳号·原型机 备注 "该召唤物阵营为敌方"). It stays out of enemiesInKeys /
* canTargetEnemy (every other selector keeps to enemies); a heal skill never counts it (禁疗: not a patient).
*/
_allyTargetIn(keys) {
if (this.healSkill || !keys) return false;
const b = this.battle;
return !!(b._allyTargets && b._allyTargets.size) && b.allyTargetsInKeys(keys, this.unit).length > 0;
}
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
@@ -405,27 +420,31 @@ export class SkillRuntime {
// SEARCH: an enemy inside the initial attack range, every tick (librators / phalanxes and 安洁莉娜 do not attack
// while the skill is off, so DEFAULT's "about to attack" never comes)
if (this.rule === 'SEARCH') return this._defaultCondition();
// (CUSTOM_RANGE / SKILL_RANGE / GDGLOW_SKILL_2 / the DEFAULT condition: a registered ally target — 白铁's 铁钳号·原型机 —
// counts like an enemy for a non-heal skill, `_allyTargetIn`)
if (this.rule === 'CUSTOM_RANGE') {
if (!this.triggerGrid) return this._defaultCondition();
return b.enemiesInKeys(this._triggerKeys(), u, { canHitFly: true }).length > 0;
const keys = this._triggerKeys();
return b.enemiesInKeys(keys, u, { canHitFly: true }).length > 0 || this._allyTargetIn(this._trigSet);
}
if (this.rule === 'SKILL_RANGE') {
if (this.triggerAllies) return this._allyTriggerSatisfied();
if (!this.triggerGrid) return this._defaultCondition();
return b.anyEnemyInKeys(this._triggerKeys());
return b.anyEnemyInKeys(this._triggerKeys()) || this._allyTargetIn(this._trigSet);
}
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;
return b.enemies.some((e) => canTargetEnemy(u, e, TRIGGER_PROFILE));
return b.enemies.some((e) => canTargetEnemy(u, e, TRIGGER_PROFILE)) || this._allyTargetIn(ALL_TILES);
}
return false;
}
/**
* 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).
* grid + permanent rangeExtend) — a registered ally target there too (`_allyTargetIn`: 铁钳号·原型机; not for heal
* skills) — 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(range = null) {
@@ -435,7 +454,7 @@ export class SkillRuntime {
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;
if (b.enemiesInKeys(keys, u, u.profile).length > 0 || this._allyTargetIn(keys)) return true;
}
// the enemies a unit blocks are always its targets (Battle.blockedTargets), in range or not — PRTS 卫戍协议/帮助
// "敌人被近战干员自身阻挡" satisfies the target condition of the basic strategy (a ranged blocker too: user playtest #6)