Files
ark-stronghold-protocol/server/sim/targeting.js
T
sganggs 15291c2293 Playtest #5 fixes: hovering enemies, target order, blocking hand-over, 联防 knocked-out, elements, huge-boss hit area, maps, gear icon, phone HUD, 至简 price
- #1 近地悬浮 enemies count as air units (Unit.isFlying): ground-only attacks, splash, terrain and traps skip them;
  PRTS 不可对空 skill effects made ground-only; 掠海漂移体 / 吉兆飞鳞 drop modes; the bot plans them as air
- #2 an operator knocked out before 联防 is fielded and force-exited (PRTS 强制退场): it lies on its tile with the
  redeploy ring and redeploys (full timer [ASSUMED]); partners, observers and the server fallback included
- #3 element audit of every operator, item and enemy: 海嗣 numbers are official; bursts are 无来源, 元素脆弱 only
  raises 元素伤害, 元素抗性 applied, 盟约·辅助干员 fills all three gauges, 菲莱 / 哈洛德 / 纯烬 / enemy kits fixed
- #4 blocking by the official contact radius (0.7071): another operator in contact takes over at once when the
  blocker dies, withdraws or is stunned; melee blockers attack what they block
- #5 enemy target order per PRTS 索敌: blocker, special priority, taunt, last deployed; initial deploy 从上到下>从左到右,
  operators before summons; special priorities break ties by taunt then deploy order
- #6 act2 m01's normal-field blower machines (row 13) are drawn in the normal / 联防 views
- #7 Final Assault prep legality reads the player's half of the boss field (fence tiles deployable); all 11 stages
  audited against the official level files
- #8 regular settings gear; #9 the prep camera keeps the bench and temp rows clear of the shop bar on phones
- #10 huge bosses have the official 4.95 × 2.95 hit area (sim/body.js) and are 自缚 / unblockable
- #11 至简 costs 1: 购买价格为N's bb.price is a discount off the tier price, applied before other price effects
- DESIGN §19 and the normative §3 / §5 / §6 / §7 lines, SIM / META / DATA / PLAYING updated; 2595 unit tests and
  every browser suite green; dev recordings regenerated
2026-09-30 19:12:36 +08:00

152 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// server/sim/targeting.js — range tests, target filters and priorities (DESIGN §3, §5.5).
//
// Grid ranges: `[dRow, dCol]` offsets relative to facing RIGHT, rotated by the unit's direction (sim/dir.js: RIGHT
// (dr,dc), UP (dc,−dr), LEFT (−dr,−dc), DOWN (−dc,dr); row 0 = bottom). An enemy is in range when the tile containing
// it (round(y), round(x)) is one of the absolute range tiles — a huge enemy (巨型单位) when any tile its hit rectangle
// occupies is (body.js; user playtest #5 item 10). `rangeExtend` (ability_range_forward_extend) adds N tiles
// past the furthest cell of every row, along +dCol BEFORE rotating (DESIGN §3).
// Operator priority: (1) enemies it blocks (melee units only: meleeUnit), (2) profile priority (fly/lowDef/…),
// (3) higher enemy taunt, (4) least remaining path distance to the goal, (5) earliest spawned. Air units
// (Unit.isFlying: FLY, 近地悬浮, 浮空) need a profile that can hit them (`canHitFly`, never `groundOnly`). Enemy
// priority: sortAllyTargets.
import { COLS, ROWS } from './constants.js';
import { normDir, rotateOffset } from './dir.js';
import { bodyDist } from './body.js';
/**
* Build the absolute tile-key list of a grid for a unit standing at (r,c) facing `dir` ('UP'|'RIGHT'|'DOWN'|'LEFT';
* a legacy facing sign ±1 is read as RIGHT / LEFT).
*/
export function absoluteRangeKeys(grid, r, c, dir, extend = 0) {
const keys = [];
const seen = new Set();
const d = normDir(dir);
const add = (dr, dc) => {
const [ar, ac] = rotateOffset(dr, dc, d);
const rr = r + ar, cc = c + ac;
if (rr < 0 || rr >= ROWS || cc < 0 || cc >= COLS) return;
const k = rr * COLS + cc;
if (!seen.has(k)) { seen.add(k); keys.push(k); }
};
// kits may hand over junk (a number, a string, [[1,'a']]): ignore anything that is not an integer [dr, dc] pair
// instead of throwing inside the attack loop every tick
if (!Array.isArray(grid) || !Number.isInteger(r) || !Number.isInteger(c)) return keys;
const cells = grid.filter((p) => Array.isArray(p) && Number.isInteger(p[0]) && Number.isInteger(p[1]));
for (const [dr, dc] of cells) add(dr, dc);
if (extend > 0 && Number.isFinite(extend)) {
const maxByRow = new Map();
for (const [dr, dc] of cells) maxByRow.set(dr, Math.max(maxByRow.get(dr) ?? -Infinity, dc));
for (const [dr, mx] of maxByRow) for (let k = 1; k <= Math.min(extend, COLS); k++) add(dr, mx + k);
}
return keys;
}
/** Tile key of a unit's current position. */
export function tileKeyOf(u) {
const r = Math.round(u.y), c = Math.round(u.x);
if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return -1;
return r * COLS + c;
}
/**
* A melee unit (近战位: data `position` MELEE — melee-tile operators and summons, 重装要塞 / 领主 / 哨戒铁卫 included):
* it may always select, and selects first, the enemies it blocks — PRTS 选择器 "可以选择且优先选择阻挡单位" (the
* BlockedOrAdvancedSelector of units whose range is smaller than their block radius), 索敌的概念 "我方索敌优先级:
* 阻挡(近战限定)". A ranged operator standing on a melee tile (卫戍协议 "所有行动内远程干员可部署在近战位") blocks, but
* targets by its range alone: "远程位干员通常而言不会优先攻击自己阻挡的目标;甚至如果这个目标被阻挡在该干员的身后,该干员
* 可能因为敌人未处于自身攻击范围而无法攻击到这个敌人" (索敌的概念). Used by Battle.blockedTargets and sortEnemyTargets.
*/
export function meleeUnit(u) {
return String(u?.def?.position ?? 'MELEE').toUpperCase() === 'MELEE';
}
/** 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 (e.isFlying && !(profile && profile.canHitFly)) return false;
if (profile && profile.groundOnly && e.isFlying) return false;
return true;
}
/**
* Can enemy `e` target ally `a`? `ranged` = the attack is a ranged (non-blocked) one. A stealthed ally (隐匿 / 迷彩, the
* 排气格栅 tile) is a target only for the enemy it blocks — PRTS 作战机制 §隐匿 "我方干员并不会因为阻挡而解除隐匿" and 索敌的
* 概念 "敌人会在自身被干员阻挡情况下强行无视对方可选性发动攻击" (the term text "不阻挡时…" is the short form; 异常效果:
* "隐匿与'阻挡时解除'没有直接关系"). Devices are never targets (阻隔工事: obstacles nobody can select; “双眼皮”: 迷彩 and
* off the enemy paths — PRTS).
*/
export function canTargetAlly(e, a, ranged) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') return false;
const f = a.s.flags;
if (f.untargetable || f.sleep) return false;
if (ranged && f.stealth && e.blockedBy !== a) return false;
return true;
}
const PRIORITY_FNS = {
fly: (e) => (e.isFlying ? 0 : 1),
lowDef: (e) => e.s.def,
highDef: (e) => -e.s.def,
ranged: (e) => (e.base.rangeRadius > 0 && e.def?.applyWay !== 'MELEE' ? 0 : 1),
lowestHp: (e) => e.hp,
highestHp: (e) => -e.hp,
lowestHpRatio: (e) => e.hpRatio,
highestAtk: (e) => -e.s.atk,
boss: (e) => (e.isBoss ? 0 : 1),
notBurst: (e) => (e.s.flags.burstLock ? 1 : 0),
ground: (e) => (e.isFlying ? 1 : 0),
};
/**
* Sort candidate enemies for an attacker (in place) and return them.
* @param {object} attacker ally unit
* @param {object[]} cands enemies
* @param {string|null} priority profile/skill priority key
*/
export function sortEnemyTargets(battle, attacker, cands, priority) {
if (cands.length <= 1) return cands;
const pf = priority ? (PRIORITY_FNS[priority] || (priority === 'nearest' || priority === 'farthest' ? null : null)) : null;
const ax = attacker.x, ay = attacker.y;
const melee = meleeUnit(attacker);
const keyed = cands.map((e) => ({
e,
b: melee && e.blockedBy === attacker ? 0 : 1,
p: pf ? pf(e) : priority === 'nearest' ? bodyDist(e, ax, ay) : priority === 'farthest' ? -bodyDist(e, ax, ay) : 0,
t: -(e.s.taunt || 0),
d: battle.remainingDistance(e),
s: e.spawnSeq,
}));
keyed.sort((a, b) => a.b - b.b || a.p - b.p || a.t - b.t || a.d - b.d || a.s - b.s);
for (let i = 0; i < keyed.length; i++) cands[i] = keyed[i].e;
return cands;
}
/**
* Enemy target selection among allies (PRTS 作战机制 索敌, 敌方: "阻挡→特殊优先级→仇恨值(更容易被攻击→…→最后部署的目标→
* 不容易被攻击)→最早出现"): its blocker first → highest taunt level → latest deployed (`aggroSeq`: the deploy order —
* Battle.start deploys the operators first and ranks the start-of-battle summons after them). Special priorities
* (优先攻击…) are the enemies' own content: `e.profile.canTarget` filters, key sorts broken by `aggroCmp`.
*/
export function sortAllyTargets(enemy, cands) {
if (cands.length <= 1) return cands;
cands.sort((a, b) => {
const ba = enemy.blockedBy === a ? 0 : 1, bb = enemy.blockedBy === b ? 0 : 1;
return ba - bb || aggroCmp(a, b);
});
return cands;
}
/**
* 仇恨值 order of two allies for an enemy (negative ⇒ `a` first): higher taunt level, then the latest deployed
* (`aggroSeq`). The tie-break after an enemy's special priority (优先攻击防御力最高的… — PRTS 索敌: 特殊优先级 → 仇恨值).
*/
export function aggroCmp(a, b) {
const ta = a.s.taunt || 0, tb = b.s.taunt || 0;
if (ta !== tb) return tb - ta;
return (b.aggroSeq || b.deploySeq) - (a.aggroSeq || a.deploySeq);
}