feedback5 R3: Battle.js → server/sim/battle/*.js, a pure move — the 131 methods, verbatim with their comments, go to 14 per-concern method containers (players, lifecycle, hooks, spawns, deploy, blocking, status, combat, queries, summons, tiles, displacement, economy, events; fin / clone in util.js) that Battle.js installs on Battle.prototype in a fixed order with class-method descriptors; Battle.js keeps the header, the constructor and the class, and re-exports pushTiles. docs-consistency reads the method modules (battleText) and pins layerGainRoom in battle/economy.js
This commit is contained in:
1 parent
6b489b1776
commit
13224f9483
17 files changed
+2488
-2328
No files matched your search
@@ -0,0 +1,247 @@
|
||||
// server/sim/battle/queries.js — Battle methods: queries: the per-tick enemy tile index, enemies / allies by tile keys,
|
||||
// grid, radius and selectability, players, and the range rebuild of an ally (current, base and extra range keys).
|
||||
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
||||
|
||||
import { ROWS, COLS } from '../constants.js';
|
||||
import { absoluteRangeKeys, canTargetEnemy, extendedGrid, enemyStealthed } from '../targeting.js';
|
||||
import { bodyKeys, bodyInKeys, bodyInRadius } from '../body.js';
|
||||
|
||||
export class BattleQueries {
|
||||
/**
|
||||
* Tile buckets of the living enemies: a regular enemy on the tile of its position, a huge one (body.js) on every tile
|
||||
* it occupies.
|
||||
*/
|
||||
_buildEnemyIndex() {
|
||||
for (const k of this._ebUsed) this._eb[k].length = 0;
|
||||
this._ebUsed.length = 0;
|
||||
for (const e of this.enemies) {
|
||||
if (!e.alive || e.hidden) continue;
|
||||
if (e.hitArea) {
|
||||
for (const k of bodyKeys(e)) { const b = this._eb[k]; if (!b.length) this._ebUsed.push(k); b.push(e); }
|
||||
continue;
|
||||
}
|
||||
const r = Math.round(e.y), c = Math.round(e.x);
|
||||
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) continue;
|
||||
const k = r * COLS + c;
|
||||
const b = this._eb[k];
|
||||
if (!b.length) this._ebUsed.push(k);
|
||||
b.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Targetable enemies whose body is on any of `keys` (absolute tile keys; a huge enemy is listed once). */
|
||||
enemiesInKeys(keys, attacker, profile) {
|
||||
const out = [];
|
||||
if (!keys) return out;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const b = this._eb[keys[i]];
|
||||
if (!b || !b.length) continue;
|
||||
for (const e of b) if (e.alive && (!e.hitArea || !out.includes(e)) && canTargetEnemy(attacker, e, profile)) out.push(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A living enemy whose body is on any of `keys` — targetable or not (stealthed, untargetable, asleep, flying): the
|
||||
* 技能范围 trigger "技能范围内存在敌人(无视其不可选中)" (skills.js SKILL_RANGE).
|
||||
*/
|
||||
anyEnemyInKeys(keys) {
|
||||
if (!keys) return false;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const b = this._eb[keys[i]];
|
||||
if (b && b.length) for (const e of b) if (e.alive && e.deployed) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Allies (not devices) within tiles `keys`, sorted by HP ratio (lowest first) that need healing. */
|
||||
injuredAlliesInKeys(keys, healer, includeElement = false) {
|
||||
const out = [];
|
||||
if (!keys) return out;
|
||||
const set = keys instanceof Set ? keys : (healer && healer.rangeKeys === keys && healer.rangeKeySet ? healer.rangeKeySet : new Set(keys));
|
||||
for (const a of this.allyUnits) {
|
||||
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') continue;
|
||||
if (!set.has(a.tileR * COLS + a.tileC)) continue;
|
||||
if (a !== healer && (a.s.flags.noHeal || (a.profile && a.profile.noHeal))) continue;
|
||||
const injured = a.hp < a.s.maxHp - 1e-6;
|
||||
const elem = includeElement && (a.elem.burn + a.elem.neural + a.elem.necrosis + a.elem.apoptosis + a.elem.erosion) > 0;
|
||||
if (injured || elem) out.push(a);
|
||||
}
|
||||
out.sort((a, b) => a.hpRatio - b.hpRatio || a.deploySeq - b.deploySeq);
|
||||
return out;
|
||||
}
|
||||
|
||||
lowestHpAllyInRange(unit) {
|
||||
const l = this.injuredAlliesInKeys(unit.rangeKeys, unit);
|
||||
return l[0] ?? null;
|
||||
}
|
||||
|
||||
/** Units of the opposite side (or `side`) whose tile is inside `grid` offsets relative to `unit`. */
|
||||
unitsInGrid(unit, grid, { side = null, extend = 0 } = {}) {
|
||||
const keys = absoluteRangeKeys(grid || unit.rangeGrid || [[0, 0]], Math.round(unit.y), Math.round(unit.x), unit.dir, extend);
|
||||
const want = side ?? (unit.side === 'ally' ? 'enemy' : 'ally');
|
||||
const set = new Set(keys);
|
||||
const out = [];
|
||||
const list = want === 'enemy' ? this.enemies : this.allyUnits;
|
||||
for (const x of list) {
|
||||
if (!x.alive || !x.deployed || x.hidden) continue;
|
||||
if (bodyInKeys(x, set)) out.push(x);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Allies inside `unit`'s current range (for auras) — not the 孤立 ones ("无法被同阵营选中": 炎佑), `unit` aside. */
|
||||
alliesInGrid(unit) {
|
||||
const set = unit.rangeKeySet || new Set(unit.rangeKeys || []);
|
||||
return this.allyUnits.filter((a) => a.alive && a.deployed && !a.hidden && a.kind !== 'device' && set.has(a.tileR * COLS + a.tileC) && this.allySelectable(a, unit));
|
||||
}
|
||||
|
||||
/**
|
||||
* May an ability of ally `by` select ally `a`? Never a 孤立 unit (炎佑, 从不混淆的方向; flag `isolated`) other than `by`
|
||||
* itself — PRTS 选择器: selectors decide "天赋是否能给予某个干员Buff" and every heal / buff / aura target, and their 可选判定
|
||||
* rejects a 孤立 target of a friendly selector ("若掩码中孤立为1,且选择器的阵营与目标为友好关系,则不可选中"; user playtest #6
|
||||
* item 18). Enemies still target it (a hostile selector). 禁疗 only keeps heals off (heal pipeline, injuredAlliesInKeys).
|
||||
*/
|
||||
allySelectable(a, by = null) {
|
||||
return !!a && (a === by || !a.s.flags.isolated);
|
||||
}
|
||||
|
||||
/** Deployed allies (no devices) an ability of ally `by` may select: allies(ownerId) without the 孤立 ones. */
|
||||
alliesFor(by, ownerId = null) {
|
||||
return this.allies(ownerId).filter((a) => this.allySelectable(a, by));
|
||||
}
|
||||
|
||||
/**
|
||||
* Living enemies whose body (body.js: a huge enemy's hit rectangle, else its position) is within `r` of (x, y).
|
||||
* `centre` = a 中点判定 radius — splash around a struck / marked target (PRTS 作战机制: "中点判定…案例:阻挡,酒神1天赋的1.3
|
||||
* 溅射半径"): every enemy counts by its position (判定中心), a huge one too.
|
||||
*/
|
||||
enemiesInRadius(x, y, r, centre = false) {
|
||||
const out = [];
|
||||
const r2 = r * r + 1e-9;
|
||||
for (const e of this.enemies) {
|
||||
if (!e.alive || e.hidden) continue;
|
||||
if (e.hitArea && !centre) { if (bodyInRadius(e, x, y, r)) out.push(e); continue; }
|
||||
const dx = e.x - x, dy = e.y - y;
|
||||
if (dx * dx + dy * dy <= r2) out.push(e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, 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 && enemyStealthed(e))) continue;
|
||||
out[n++] = e;
|
||||
}
|
||||
out.length = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every deployed ally within `r` of (x, y) — no selection rule: an enemy's area effect selects among them with
|
||||
* targeting.js areaSelectable (content/enemies.js areaAllies: no 隐匿 ally, the blocker included, GitHub #97; DESIGN §22.12).
|
||||
*/
|
||||
alliesInRadius(x, y, r, ownerId = null, { includeDevices = false } = {}) {
|
||||
const out = [];
|
||||
const r2 = r * r + 1e-9;
|
||||
for (const a of this.allyUnits) {
|
||||
if (!a.alive || !a.deployed || a.hidden) continue;
|
||||
if (!includeDevices && a.kind === 'device') continue;
|
||||
if (ownerId != null && a.ownerId !== ownerId) continue;
|
||||
const dx = a.x - x, dy = a.y - y;
|
||||
if (dx * dx + dy * dy <= r2) out.push(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Alive deployed allies (ops + tokens), optionally of one player. */
|
||||
allies(ownerId = null) {
|
||||
return this.allyUnits.filter((a) => a.alive && a.deployed && a.kind !== 'device' && (ownerId == null || a.ownerId === ownerId));
|
||||
}
|
||||
|
||||
aliveEnemies() { return this.enemies.filter((e) => e.alive); }
|
||||
|
||||
unitAt(r, c) { const u = this._occ[r * COLS + c]; return u && u.alive ? u : null; }
|
||||
|
||||
unitById(id) { return this.units.find((u) => u.id === id) ?? null; }
|
||||
|
||||
tileInfo(r, c) { return this.grid.tile(r, c); }
|
||||
|
||||
getPlayer(playerId) { return this.players.find((p) => p.playerId === playerId) ?? null; }
|
||||
|
||||
_pp(playerId) { return playerId == null ? null : this._perPlayer[playerId] ?? null; }
|
||||
|
||||
// =============================================================================================================
|
||||
// ranges
|
||||
|
||||
/**
|
||||
* Recompute a unit's absolute range tile keys: `rangeKeys` / `rangeKeySet` = current range (skill range override +
|
||||
* rangeExtend — the unit's own 攻击距离 unless the running skill's range ignores it, `targeting.noRangeExtend`: 信仰搅拌机
|
||||
* S3, PRTS 备注 "此技能的攻击范围不受'攻击距离'属性影响" — plus the skill's own `targeting.rangeExtend`) ∪
|
||||
* `unit.extraRangeKeys` (content extra targets — setExtraRange); `baseRangeKeys` = the INITIAL range of the DEFAULT
|
||||
* skill trigger: the unit's own grid + its permanent rangeExtend (`s.baseRangeExtend`: persistent, never-expiring
|
||||
* buffs — talents, modules, bonds), no skill range, no temporary extend, no extra keys; `liveRangeGrid` = the
|
||||
* relative grid (facing RIGHT) of the 攻击范围 the detail card shows (shared/protocol.js unitStatsEntry `range`;
|
||||
* community report E1 after 0.1.0: the card kept the base grid while 烛煌 S3 attacked with 4-11): the grid behind
|
||||
* `rangeKeys` without the extra keys (targeting.js extendedGrid; the grid itself when nothing extends it), or the
|
||||
* unit's own range while a skill's grid only selects targets (`targeting.showOwnRange`: 荒芜拉普兰德 S1, no official
|
||||
* range change). Rebuilt on deploy / relocate / skill range switches and whenever either extend changes (rangeChanged).
|
||||
*/
|
||||
_refreshRange(u) {
|
||||
const tg = u.skill && u.skill.active ? u.skill.spec.targeting : null;
|
||||
const grid = (tg && tg.rangeGrid) || u.rangeGrid || [[0, 0]];
|
||||
const ext = (tg && tg.noRangeExtend ? 0 : u.s.rangeExtend) + ((tg && tg.rangeExtend) || 0);
|
||||
u._rangeExtend = u.s.rangeExtend;
|
||||
u._baseExtend = u.s.baseRangeExtend;
|
||||
const keys = absoluteRangeKeys(grid, u.tileR, u.tileC, u.dir, ext);
|
||||
const set = new Set(keys);
|
||||
const extra = u.extraRangeKeys;
|
||||
if (extra) for (const k of extra) if (!set.has(k)) { set.add(k); keys.push(k); }
|
||||
u.rangeKeys = keys;
|
||||
u.rangeKeySet = set;
|
||||
const own = !!(tg && tg.showOwnRange);
|
||||
const lg = own ? u.rangeGrid || [[0, 0]] : grid, le = own ? u.s.rangeExtend : ext;
|
||||
u.liveRangeGrid = le > 0 ? extendedGrid(lg, le) : lg;
|
||||
if (u.kind !== 'device') u.baseRangeKeys = absoluteRangeKeys(u.rangeGrid || [[0, 0]], u.tileR, u.tileC, u.dir, u.s.baseRangeExtend);
|
||||
if (u.skill) u.skill._trigKeys = null; // CUSTOM_RANGE trigger grid is relative to the tile
|
||||
}
|
||||
|
||||
/** True when a buff changed the unit's rangeExtend (total or permanent part) since its range was last built. */
|
||||
rangeChanged(u) {
|
||||
return u._rangeExtend !== undefined && (u._rangeExtend !== u.s.rangeExtend || u._baseExtend !== u.s.baseRangeExtend);
|
||||
}
|
||||
|
||||
/** Public: rebuild a unit's range after content changed `unit.rangeGrid` (流形 copies) or its tile keys. */
|
||||
refreshRange(unit) {
|
||||
if (!unit || unit.side !== 'ally' || !unit.alive || !unit.deployed) return false;
|
||||
this._refreshRange(unit);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra targetable tiles merged into the unit's range (蕾缪安 wanted targets, 维娜 S3, …): absolute tile keys
|
||||
* (row × COLS + col), kept across every later range rebuild until content sets them again (null / [] clears).
|
||||
* Not part of `baseRangeKeys` (the DEFAULT trigger range) — see SkillRuntime.addTriggerRange for that.
|
||||
*/
|
||||
setExtraRange(unit, keys) {
|
||||
if (!unit || unit.side !== 'ally') return false;
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const k of keys || []) if (Number.isInteger(k) && k >= 0 && k < ROWS * COLS && !seen.has(k)) { seen.add(k); out.push(k); }
|
||||
unit.extraRangeKeys = out.length ? out : null;
|
||||
if (unit.alive && unit.deployed) this._refreshRange(unit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user