feedback2 WG: an enemy's 隐匿 returns 3 s after a block ends (0 s / 1 s where its PRTS page says so); a 深池逐火 knocked out while blocked leaves a revealed ember; 山海众头目's strong hit comes with its 隐匿 (#43 follow-up)
This commit is contained in:
1 parent
1aed315579
commit
b32c510069
7 files changed
+120
-27
No files matched your search
+29
-8
@@ -24,14 +24,14 @@
|
||||
// Robustness: every content callback and every step phase is wrapped; errors are logged once per key and the
|
||||
// battle continues. After MAX_INTERNAL_ERRORS the battle force-ends as a timeout.
|
||||
|
||||
import { TICK, ROWS, COLS, BLOCK_RADIUS_SQ, DP_DEFAULTS, DOWN_STATE, FORCED_EXIT, MAX_BATTLE_TIME, MAX_INTERNAL_ERRORS, COLD_FREEZE_DURATION, OBSTACLE_DEVICES, EVENT_BUFFER_CAP, BOSS_ROW_OFFSET, MAX_HOOK_DEPTH, MAX_ALIVE_ENEMIES, LEVITATE_HALF_WEIGHT, RESIST_DEFAULT, RESIST_PALSY_DECAY, PUSH_TILES, PUSH_TILES_EFFECT, PULL_WEAK_SHARE, PULL_CRAWL, PULL_ORIGIN, PULL_STOP_RADIUS, PUSH_DIRECTIONAL_MIN_DIST, AUTO_OP_COOLDOWN } from './constants.js';
|
||||
import { TICK, ROWS, COLS, BLOCK_RADIUS_SQ, DP_DEFAULTS, DOWN_STATE, FORCED_EXIT, MAX_BATTLE_TIME, MAX_INTERNAL_ERRORS, COLD_FREEZE_DURATION, OBSTACLE_DEVICES, EVENT_BUFFER_CAP, BOSS_ROW_OFFSET, MAX_HOOK_DEPTH, MAX_ALIVE_ENEMIES, LEVITATE_HALF_WEIGHT, RESIST_DEFAULT, RESIST_PALSY_DECAY, PUSH_TILES, PUSH_TILES_EFFECT, PULL_WEAK_SHARE, PULL_CRAWL, PULL_ORIGIN, PULL_STOP_RADIUS, PUSH_DIRECTIONAL_MIN_DIST, AUTO_OP_COOLDOWN, STEALTH_RESTORE } from './constants.js';
|
||||
import { GEO, layerGainRoom } from '../../shared/constants.js';
|
||||
import { createRng } from './rng.js';
|
||||
import { Grid } from './grid.js';
|
||||
import { Unit } from './units.js';
|
||||
import { makeBuff, STATUS, RESIST_STATUSES } from './buffs.js';
|
||||
import { dealDamage as pipeDamage, heal as pipeHeal, applyHpLoss, makeDamageInfo, reduceElement, palsyBuff, elementView, leaderHitCancelled } from './damage.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy, extendedGrid, evadesGround } from './targeting.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy, extendedGrid, evadesGround, enemyStealthed, stealthOffKey } from './targeting.js';
|
||||
import { bodyKeys, bodyInKeys, bodyInRadius } from './body.js';
|
||||
import { normDir, mirrorDir, localOrder, localBefore } from './dir.js';
|
||||
import { ProjectileSystem } from './projectiles.js';
|
||||
@@ -1169,13 +1169,33 @@ export class Battle {
|
||||
const i = bl.blocking.indexOf(e);
|
||||
if (i >= 0) bl.blocking.splice(i, 1);
|
||||
e.blockedBy = null;
|
||||
this._stealthSwitch(e);
|
||||
}
|
||||
|
||||
/** Release every enemy blocked by ally `u` (death, retreat, block count drop, substitution…). */
|
||||
releaseBlocked(u) {
|
||||
if (!u || !u.blocking || !u.blocking.length) return;
|
||||
for (const e of u.blocking) if (e.blockedBy === u) e.blockedBy = null;
|
||||
const was = u.blocking;
|
||||
u.blocking = [];
|
||||
for (const e of was) if (e.blockedBy === u) { e.blockedBy = null; this._stealthSwitch(e); }
|
||||
}
|
||||
|
||||
/**
|
||||
* A block on enemy `e` just ended (every release goes through here: `_unblock`, `releaseBlocked`, ai.js
|
||||
* enforceBlockCapacity). Each 隐匿 source it holds stays switched off for its restore time — PRTS 作战机制 §隐匿 "对于
|
||||
* 绝大部分可隐匿的敌人而言,在被我方单位阻挡后会解除隐匿,不被阻挡的3秒后重新进入隐匿" (STEALTH_RESTORE), or the source's
|
||||
* own "(解除阻挡N秒后恢复)" (buff `data.stealthRestore`, content/enemies.js: 0 s / 1 s on some enemy pages) — as a
|
||||
* `stealthOff` buff per source; meanwhile it is targetable, operator splash reaches it and it is drawn solid
|
||||
* (targeting.js enemyStealthed). A new block inside the window lifts it again and its end restarts the window. Our
|
||||
* operators' 隐匿 / 迷彩 are never lifted by blocking (only enemies get here).
|
||||
*/
|
||||
_stealthSwitch(e) {
|
||||
if (!e || e.side !== 'enemy' || !e.alive || !e.s.flags.stealth) return;
|
||||
for (const b of e.buffs.slice()) {
|
||||
if (!b.flags || !b.flags.stealth) continue;
|
||||
const t = Number.isFinite(b.data?.stealthRestore) ? b.data.stealthRestore : STEALTH_RESTORE;
|
||||
if (t > 0) this.addBuff(e, { key: stealthOffKey(b.key), duration: t, flags: { stealthOff: true } });
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================================================
|
||||
@@ -1609,17 +1629,18 @@ export class Battle {
|
||||
/**
|
||||
* The enemies within `r` (as enemiesInRadius) an ally-side area effect can select — PRTS 作战机制 §AOE伤害判定 "AOE的判定是
|
||||
* 对攻击范围内的每个可以被选中的敌人进行判定", 隐匿 "隐匿状态下的单位一般无法被敌方的索敌机制和Buff选择器选中为目标": no
|
||||
* untargetable enemy and no 隐匿 one unless revealed or blocked (tile selectors — enemiesInKeys / canTargetEnemy — already
|
||||
* skip them). Flying and asleep enemies stay the caller's choice. Enemy-side effects on other enemies (auras, heals) and
|
||||
* physical collisions keep enemiesInRadius. Player report #8 after 0.1.0 (the 逐火 余烬): until 0.1.1 profession splash
|
||||
* and skill circles still reached an unblocked 隐匿 enemy.
|
||||
* untargetable enemy and no 隐匿 one unless revealed, blocked or not hidden again yet after a block (targeting.js
|
||||
* enemyStealthed; tile selectors — enemiesInKeys / canTargetEnemy — already skip them). Flying and asleep enemies stay
|
||||
* the caller's choice. Enemy-side effects on other enemies (auras, heals) and physical collisions keep enemiesInRadius.
|
||||
* Player report #8 after 0.1.0 (the 逐火 余烬): until 0.1.1 profession splash and skill circles still reached an
|
||||
* unblocked 隐匿 enemy.
|
||||
*/
|
||||
foesInRadius(x, y, r, centre = false) {
|
||||
const out = this.enemiesInRadius(x, y, r, centre);
|
||||
let n = 0;
|
||||
for (const e of out) {
|
||||
const f = e.s.flags;
|
||||
if (f.untargetable || (f.stealth && !f.reveal && !e.blockedBy)) continue;
|
||||
if (f.untargetable || (f.stealth && enemyStealthed(e))) continue;
|
||||
out[n++] = e;
|
||||
}
|
||||
out.length = n;
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export function enforceBlockCapacity(b, u) {
|
||||
for (const e of u.blocking) used += e.blockWeight ?? 1;
|
||||
while (used > cap && u.blocking.length) {
|
||||
const e = u.blocking.pop();
|
||||
if (e.blockedBy === u) e.blockedBy = null;
|
||||
if (e.blockedBy === u) { e.blockedBy = null; b._stealthSwitch(e); } // a released 隐匿 enemy hides again later
|
||||
used -= e.blockWeight ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,13 @@ export const ALLY_COLLIDER_RADIUS = 0.25;
|
||||
*/
|
||||
export const BLOCK_RADIUS = Object.freeze({ ground: 0.70709997, fly: 0.8944, device: 0.4472 });
|
||||
export const BLOCK_RADIUS_SQ = Object.freeze({ ground: 0.49999037, fly: 0.79995137, device: 0.4472 * 0.4472 });
|
||||
/**
|
||||
* An enemy's 隐匿 after a block ends (s): PRTS 作战机制 §隐匿 "对于绝大部分可隐匿的敌人而言,在被我方单位阻挡后会解除隐匿,不被
|
||||
* 阻挡的3秒后重新进入隐匿" / §隐匿与Buff的关系 "阻挡状态解除后3s开关重新被开启而恢复隐匿". An enemy page's "(解除阻挡N秒后
|
||||
* 恢复)" overrides it per 隐匿 source (buff `data.stealthRestore`: content/enemies.js). Battle._stealthSwitch; our
|
||||
* operators' 隐匿 / 迷彩 are never lifted by blocking ("我方干员并不会因为阻挡而解除隐匿").
|
||||
*/
|
||||
export const STEALTH_RESTORE = 3;
|
||||
/** Default projectile speed in tiles/s for ranged operators/enemies. */
|
||||
export const PROJECTILE_SPEED = 12;
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
// INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed). An operator's radius area
|
||||
// damage skips an unblocked one too (Battle.foesInRadius: profession splash around a struck target,
|
||||
// skill circles — PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 it
|
||||
// still hit it); tile selectors (enemiesInKeys) always skipped it.
|
||||
// still hit it); tile selectors (enemiesInKeys) always skipped it. After a block it hides again only
|
||||
// STEALTH_RESTORE (3) s later — or after the "(解除阻挡N秒后恢复)" of its PRTS page (STEALTH_RESTORE_BY_KEY,
|
||||
// 清明's veil 0 s) — Battle._stealthSwitch / targeting.js enemyStealthed (until 0.1.2: at once).
|
||||
// REFLECTION — 折射 (ba.refraction "生效时,法术抗性+70"): RES +refracting.magic_resistance while NOT silenced
|
||||
// (the ability line is SILENCE-flagged: silencing turns it off); 镜膜 also gets max HP +100 % while on.
|
||||
// SPECIAL — mostly stats; prisoners, 穿刺手, 暴虐兵长, 镜卫, 动力装甲 … below.
|
||||
@@ -78,7 +80,7 @@
|
||||
// Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss.
|
||||
|
||||
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS } from '../constants.js';
|
||||
import { canTargetAlly, sortAllyTargets, aggroCmp, evadesGround } from '../targeting.js';
|
||||
import { canTargetAlly, sortAllyTargets, aggroCmp, evadesGround, enemyStealthed } from '../targeting.js';
|
||||
import { mitigate, periodicDamage } from '../damage.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -107,6 +109,23 @@ const TANK_ZONE_RADIUS = 1.7;
|
||||
const POLLUTION_INTERVAL = 1;
|
||||
/** "数个目标" of 假想敌:骨刺 while stealthed [ASSUMED]. */
|
||||
const ACBUNN_TARGETS = 3;
|
||||
/**
|
||||
* 隐匿 that comes back sooner after a block than the general 3 s (PRTS 作战机制 §隐匿 "对于绝大部分可隐匿的敌人而言…不被阻挡的
|
||||
* 3秒后重新进入隐匿"; engine STEALTH_RESTORE): the enemy pages' 天赋 "隐匿(解除阻挡N秒后恢复)" — PRTS text search
|
||||
* "解除阻挡0秒后恢复" (21 enemies; these 6 are in this mode) and "解除阻挡1秒后恢复" (the two 家族灭迹人), checked
|
||||
* 2026-10-03. Every other stealthy enemy here has a plain "隐匿" (3 s), the 深池逐火 embers included. 清明 / 堂皇's veil
|
||||
* ("获得隐匿(解除阻挡0秒后恢复)") is kitInvisShield's.
|
||||
*/
|
||||
const STEALTH_RESTORE_BY_KEY = Object.freeze({
|
||||
enemy_10031_cnvsld: 0, // 业余竞演者
|
||||
enemy_10034_cnvsax: 0, // 节日爵士乐手
|
||||
enemy_9008_acbunn: 0, // 假想敌:骨刺
|
||||
enemy_2034_sythef: 0, // 流泪小子
|
||||
enemy_2034_sythef_2: 0, // 流泪小子 (its stronger copy)
|
||||
enemy_1389_winbab_2: 0, // 访问团强攻冠军
|
||||
enemy_1283_sgkill: 1, // 家族灭迹人
|
||||
enemy_1283_sgkill_2: 1, // 家族暗影灭迹人
|
||||
});
|
||||
/** 假想敌:再生 shield aura radius (PRTS 假想敌:再生 天赋 "进入此形态时,使半径1.8范围内的其他敌方单位(无视其可选性)获得5层…护盾"). */
|
||||
const ACPUPP_AURA_RADIUS = 1.8;
|
||||
/** 重弩突袭者 直击 reach along a row/column [ASSUMED]. */
|
||||
@@ -572,8 +591,16 @@ const onTerrain = (b, u, terrain) => { const [r, c] = tileOf(u); return b.grid.t
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// archetypes
|
||||
|
||||
/** 隐匿: permanent stealth. */
|
||||
const stealth = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:stealth', flags: { stealth: true }, persist: true }); } });
|
||||
/**
|
||||
* 隐匿: permanent stealth. Blocked it is lifted; it hides again STEALTH_RESTORE s after the block ends, or after its own
|
||||
* "(解除阻挡N秒后恢复)" (STEALTH_RESTORE_BY_KEY → buff `data.stealthRestore`; Battle._stealthSwitch).
|
||||
*/
|
||||
const stealth = () => ({
|
||||
spawn(b, e, a, ab) {
|
||||
const n = STEALTH_RESTORE_BY_KEY[ab.key];
|
||||
b.addBuff(e, { key: 'ab:stealth', flags: { stealth: true }, persist: true, data: n != null ? { stealthRestore: n } : {} });
|
||||
},
|
||||
});
|
||||
/** 无法被阻挡. */
|
||||
const unblockable = () => ({ spawn(b, e) { b.addBuff(e, { key: 'ab:unblockable', flags: { unblockable: true }, persist: true }); } });
|
||||
/**
|
||||
@@ -883,9 +910,10 @@ function artsBarrier(amount, { key = 'ab:artsBarrier', whileUp = null } = {}) {
|
||||
* (PRTS 深池逐火战士 天赋 "被击倒后重生,持续1s,随后变为怨恨的余烬,1s内不移动且持有无敌+无法阻挡+失衡免疫"; PRTS 特殊机制 §重生) —
|
||||
* then the husk until `delay` s after that: `hits` HP of 特殊生命值机制 (every damage instance removes 1 — PRTS 特殊机制
|
||||
* §特殊生命值机制; engine flag hitCount), no attack (缴械), walking its route on — 隐匿 (`stealthy`: targetable only while
|
||||
* blocked, targeting.js canTargetEnemy — the 余烬 must be blocked to be beaten; drawn solid while blocked)
|
||||
* and / or unblockable (`unblock`: 再生's 傀儡 "不可被阻挡"). Killing the husk is the real death; a husk still standing
|
||||
* after `delay` s stands up in its first form with full HP, and every later knock-out starts it again ("一次又一次地站起").
|
||||
* blocked and for 3 s after a block ends, the block the knock-out itself releases included — targeting.js
|
||||
* enemyStealthed; drawn solid meanwhile) and / or unblockable (`unblock`: 再生's 傀儡 "不可被阻挡"). Killing the husk is
|
||||
* the real death; a husk still standing after `delay` s stands up in its first form with full HP, and every later
|
||||
* knock-out starts it again ("一次又一次地站起").
|
||||
* `onHusk(b, e)` runs as the husk begins, after the 重生 ("进入此形态时": 再生's shields).
|
||||
* User report after 0.1.0 (#8): the v2.5 ember stood still, stealthed AND unblockable — nobody could ever target it.
|
||||
* fx (setForm): 'ember' {id, hits, dur, form: 'husk'} at the knock-out, 'revive' {id, form: 'revived'} when it stands
|
||||
@@ -923,6 +951,13 @@ function husk({ hits, delay, stealthy = true, unblock = false, onHusk = null, ke
|
||||
hitCount(b, e, true);
|
||||
b.addBuff(e, { key, visible: true, persist: true, flags: { disarm: true, ...(stealthy ? { stealth: true } : {}), ...(unblock ? { unblockable: true } : {}) } });
|
||||
b.addBuff(e, { key: `${key}:reborn`, duration: HUSK_REBIRTH, flags: { invulnerable: true, untargetable: true, unblockable: true, noMove: true, noDisplace: true } });
|
||||
// the 重生's 无法阻挡 ends a block at once (as applyStatus does for a status): a warrior knocked out while blocked
|
||||
// leaves an ember whose 隐匿 is switched off until STEALTH_RESTORE (3) s after that block ended
|
||||
// (Battle._stealthSwitch) — ~2 s after the 1 s 重生, which stays 无敌 + untargetable — so ranged operators and
|
||||
// operator splash can finish it although its blocker took the next warrior meanwhile (players after 0.1.1: "the
|
||||
// stealth monster revives forever"). [ASSUMED] the order: the 重生's cleanse first, then the ember's 隐匿 with the
|
||||
// block-end switch (PRTS documents neither the order of 重生 vs that switch nor whether it survives the 重生)
|
||||
b._unblock(e);
|
||||
if (e.route) e.route.pts = null;
|
||||
setForm(b, e, 'husk', 'ember', { hits, dur: HUSK_REBIRTH + delay });
|
||||
b.after(HUSK_REBIRTH, () => { if (e.alive && a.state === 'husk') { rebirthCooldowns(e); if (onHusk) onHusk(b, e); } }, { owner: e });
|
||||
@@ -1063,7 +1098,8 @@ const kitStun3 = (ab) => [nthAttackStatus(nthOf(ab.sk.stuncombat), 'stun', (ab.s
|
||||
const kitSelfFear = (ab) => [selfFear(ab)];
|
||||
/** 深池逐火战士 / 精锐战士 / 护卫: knock-out ⇒ 1 s 重生 ⇒ a walking, 隐匿, disarmed 余烬 / 火灰 of prop_max_hp hits for `interval` s
|
||||
* (PRTS 深池逐火战士 天赋: "基础最大生命值临时变为5…具有特殊生命值机制,不进行攻击,获得隐匿、缴械,10s后若未被击倒则变回战士形态并恢复所有
|
||||
* 生命"); blocking it lifts the 隐匿, so its blocker (and every operator in range) can beat it. */
|
||||
* 生命"); blocking it lifts the 隐匿, so its blocker (and every operator in range) can beat it — also during the 3 s
|
||||
* after a block ends, the warrior's own block that the knock-out releases included (Battle._stealthSwitch). */
|
||||
const kitEmber = (ab) => [husk({ hits: T(ab, 'Revive[Trigger].prop_max_hp'), delay: T(ab, 'Revive[Trigger].interval') })];
|
||||
const kitPolluted = (ab) => [{
|
||||
death(c, b, e) {
|
||||
@@ -1311,7 +1347,8 @@ function kitInvisShield(ab, e) {
|
||||
return [...kitDeathSpawn()(ab), skill(s, (b, e2) => {
|
||||
const dur = s.bb.duration ?? T(ab, 'InvisibleShield.duration') ?? 0;
|
||||
b.fx('telegraph', { x: e2.x, y: e2.y, r, kind: 'invisShield', id: e2.id });
|
||||
for (const o of b.enemiesInRadius(e2.x, e2.y, r)) if (o !== e2 && dur > 0) b.addBuff(o, { key: 'ab:veiled', duration: dur, flags: { stealth: true }, visible: true });
|
||||
// "获得隐匿(解除阻挡0秒后恢复)" (PRTS 清明 / 堂皇 天赋): the veil's 隐匿 is back as soon as a block ends
|
||||
for (const o of b.enemiesInRadius(e2.x, e2.y, r)) if (o !== e2 && dur > 0) b.addBuff(o, { key: 'ab:veiled', duration: dur, flags: { stealth: true }, visible: true, data: { stealthRestore: 0 } });
|
||||
})];
|
||||
}
|
||||
|
||||
@@ -1331,10 +1368,18 @@ function kitCrossbow(ab) {
|
||||
}, { sil: true, cond: (b, e) => aligned(b, e).length > 0 })];
|
||||
}
|
||||
|
||||
/**
|
||||
* 山海众头目 / 山海众秘使 (PRTS 天赋 "隐匿,该隐匿每次生效后自身获得强击标记(不可叠加)"; 技能 破隐一击 "仅持有强击标记且被阻挡时可
|
||||
* 触发:对阻挡目标造成攻击力200%的物理普通伤害,技能开始时消耗强击标记"): the mark comes with its 隐匿 — at the spawn and every
|
||||
* time it hides again (3 s after a block, Battle._stealthSwitch; or once a 反隐 ends) — and its next attack (a melee
|
||||
* one: blocked) spends it at InvisibleCombat.atk_scale. A new block inside the 3 s gives no new mark (until 0.1.2 every
|
||||
* block did).
|
||||
*/
|
||||
function kitShadowKiller(ab) {
|
||||
const scale = (ab.sk.InvisibleCombat && ab.sk.InvisibleCombat.bb.atk_scale) || 1;
|
||||
return [stealth(), {
|
||||
blocked(c, b, e, a) { a.power = true; }, // 隐匿效果失效 (blocked) → next attack ×scale
|
||||
spawn(b, e, a) { a.power = true; a.on = true; },
|
||||
tick(b, e, a) { const on = enemyStealthed(e); if (on && !a.on) a.power = true; a.on = on; },
|
||||
hitOut(c, b, e, a) { if (a.power && c.dmg.isAttack) c.dmg.amount *= scale; },
|
||||
attack(c, b, e, a) { a.power = false; },
|
||||
}];
|
||||
@@ -1343,7 +1388,7 @@ function kitShadowKiller(ab) {
|
||||
function kitJazz(ab, e) {
|
||||
const s = ab.sk.fire;
|
||||
const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2.5;
|
||||
const revealed = (u) => !!(u.blockedBy || u.s.flags.reveal);
|
||||
const revealed = (u) => !enemyStealthed(u); // blocked, 反隐 (its 隐匿 returns 0 s after a block)
|
||||
return [stealth(), {
|
||||
spawn(b, e2) { e2.profile.noAttack = true; },
|
||||
tick(b, e2) { e2.profile.noAttack = !revealed(e2); }, // 平时不攻击,失去隐匿时反击
|
||||
@@ -1496,7 +1541,7 @@ function kitParasite(ab) {
|
||||
function kitBoneSpike() {
|
||||
return [stealth(), {
|
||||
before(c, b, e) {
|
||||
if (e.blockedBy || e.s.flags.reveal) return; // 隐匿状态下同时攻击数个目标
|
||||
if (!enemyStealthed(e)) return; // 隐匿状态下同时攻击数个目标 (back 0 s after a block)
|
||||
const l = byPriority(e, targetsNear(b, e, e.base.rangeRadius));
|
||||
if (l.length) c.targets = l.slice(0, ACBUNN_TARGETS);
|
||||
},
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
// dir = 'UP'|'RIGHT'|'DOWN'|'LEFT' (allies: the deploy direction, sim/dir.js); facing = its horizontal sign (±1).
|
||||
// items = an ally operator's equipped item ids (absent without any).
|
||||
// flags bits & anim codes come from shared/constants.js (UF / ANIM); an enemy's stealth bit = its 隐匿 is on (not while it
|
||||
// is blocked or revealed), an ally's = 隐匿 / 迷彩 whatever it blocks.
|
||||
// is blocked or revealed, nor in the seconds after a block before it hides again — targeting.js enemyStealthed), an
|
||||
// ally's = 隐匿 / 迷彩 whatever it blocks.
|
||||
|
||||
import { UF, ANIM } from '../../shared/constants.js';
|
||||
import { DIE_ANIM_TIME, ATTACK_ANIM_TIME, DEPLOY_ANIM_TIME } from './constants.js';
|
||||
import { enemyStealthed } from './targeting.js';
|
||||
|
||||
const r2 = (v) => Math.round(v * 100) / 100;
|
||||
const r1 = (v) => Math.round(v * 10) / 10;
|
||||
@@ -60,9 +62,10 @@ export function flagsOf(u) {
|
||||
if (f.stun && !f.freeze && !f.sleep) bits |= UF.STUNNED;
|
||||
if (f.freeze) bits |= UF.FROZEN;
|
||||
// 隐匿 or 迷彩 (buffs.js camou), shown the see-through way. An ally keeps it while blocking (targeting.js
|
||||
// canTargetAlly); an enemy's 隐匿 is off while it is blocked or revealed (PRTS 作战机制 §隐匿 "在被阻挡时开关会被关掉从而
|
||||
// 失去隐匿"; targeting.js canTargetEnemy): a blocked 逐火 余烬 is drawn solid while the team beats it
|
||||
if (u.side === 'enemy' ? (f.stealth && !f.reveal && !u.blockedBy) || f.camou : f.stealth || f.camou) bits |= UF.STEALTH;
|
||||
// canTargetAlly); an enemy's 隐匿 is off while it is blocked or revealed and until it hides again after a block (PRTS
|
||||
// 作战机制 §隐匿 "在被阻挡时开关会被关掉从而失去隐匿,阻挡状态解除后3s开关重新被开启"; targeting.js enemyStealthed): a
|
||||
// blocked 逐火 余烬 is drawn solid while the team beats it, and for 3 s after it slips away
|
||||
if (u.side === 'enemy' ? (f.stealth && enemyStealthed(u)) || f.camou : f.stealth || f.camou) bits |= UF.STEALTH;
|
||||
if (u.skill && u.skill.active && u.skill.kind !== 'passive') bits |= UF.SKILL;
|
||||
if (u.s.shield > 0 || u.buffs.some((b) => b.shieldHits > 0)) bits |= UF.SHIELD;
|
||||
if (f.invulnerable) bits |= UF.INVULN;
|
||||
|
||||
+18
-1
@@ -55,12 +55,29 @@ export function tileKeyOf(u) {
|
||||
return r * COLS + c;
|
||||
}
|
||||
|
||||
/** Key of the buff that keeps an enemy's 隐匿 source `sourceKey` switched off after a block (Battle._stealthSwitch). */
|
||||
export const stealthOffKey = (sourceKey) => `stealthOff:${sourceKey}`;
|
||||
|
||||
/**
|
||||
* Is enemy `e`'s 隐匿 in effect — no ally selection picks it, operator splash skips it (Battle.foesInRadius), the b.snap
|
||||
* stealth bit is set? Not while it is revealed (反隐, flag `reveal`), not while blocked, and not while every 隐匿 source
|
||||
* it holds is still switched off after its last block (flag `stealthOff`: PRTS 作战机制 §隐匿 "在被我方单位阻挡后会解除隐匿,
|
||||
* 不被阻挡的3秒后重新进入隐匿" — constants.js STEALTH_RESTORE, or the source's own "(解除阻挡N秒后恢复)").
|
||||
*/
|
||||
export function enemyStealthed(e) {
|
||||
const f = e.s.flags;
|
||||
if (!f.stealth || f.reveal || e.blockedBy) return false;
|
||||
if (!f.stealthOff) return true;
|
||||
for (const b of e.buffs) if (b.flags && b.flags.stealth && !e.findBuff(stealthOffKey(b.key))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Can `attacker` (ally) target enemy `e` at all (ignoring range)? */
|
||||
export function canTargetEnemy(attacker, e, profile) {
|
||||
if (!e.alive || e.hidden || !e.deployed) return false;
|
||||
const f = e.s.flags;
|
||||
if (f.untargetable || (f.sleep && !(profile && profile.hitSleep))) return false;
|
||||
if (f.stealth && !f.reveal && !e.blockedBy) return false;
|
||||
if (f.stealth && enemyStealthed(e)) return false;
|
||||
if (e.isFlying && !(profile && profile.canHitFly)) return false;
|
||||
if (profile && profile.groundOnly && e.isFlying) return false;
|
||||
return true;
|
||||
|
||||
@@ -572,7 +572,7 @@ describe('a blocked 隐匿 enemy is drawn solid (b.snap stealth bit: PRTS 作战
|
||||
const tuple = () => h.b.snapshot().units.find((t) => t[0] === e.id);
|
||||
assert.ok(tuple()[7] & UF.STEALTH, 'in b.snap too');
|
||||
assert.ok(h.runUntil(() => !!e.blockedBy, 10), 'walks into the wall');
|
||||
assert.ok(e.s.flags.stealth, 'the sim keeps the 隐匿 flag (it returns once the block ends)');
|
||||
assert.ok(e.s.flags.stealth, 'the sim keeps the 隐匿 flag (it returns 3 s after the block ends, §22.8)');
|
||||
assert.equal(flagsOf(e) & UF.STEALTH, 0, 'blocked: drawn solid');
|
||||
assert.equal(tuple()[7] & UF.STEALTH, 0);
|
||||
assert.ok(flagsOf(e) & UF.BLOCKED);
|
||||
|
||||
Reference in new issue
Block a user