Files
ark-stronghold-protocol/public/js/ui/gameLogic/enemies.js
T
sganggs eb04f5dfa4 feedback5 R5: split the in-match UI logic into modules
public/js/ui/gameLogic.js becomes a re-export barrel. The bodies move verbatim into public/js/ui/gameLogic/*.js (shared, phases, camera, watch, bonds, shop, placement, draft, enemies, format, shortcuts, settings, result, loadout, panel). The barrel re-exports the same 96 names in the same order. isObj and int gain export on shared.js so sibling modules can import them; they stay off the barrel. No importer changes.
2026-10-05 17:06:45 +08:00

97 lines
5.1 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.
// ui/gameLogic/enemies.js — enemy groups, factions, the preview pen. Re-exported from ../gameLogic.js.
import { layoutPen } from '../../render/pen.js';
import { isObj } from './shared.js';
// ---- enemies ------------------------------------------------------------------------------------------------
const TAG_ORDER = { boss: 0, bounty: 1, escort: 2 };
const RANK_ORDER = { BOSS: 0, ELITE: 1, NORMAL: 2 };
/**
* Next-round enemy preview rows: merged by (enemyKey, tag), sorted boss → bounty → elite → normal → count.
* @param {Array<{enemyKey:string, count?:number, tag?:string}>} list
* @param {(key:string)=>any} [getEnemy]
*/
export function groupEnemies(list, getEnemy = () => null) {
const map = new Map();
for (const e of Array.isArray(list) ? list : []) {
if (!isObj(e) || typeof e.enemyKey !== 'string') continue;
const tag = typeof e.tag === 'string' ? e.tag : null;
const k = `${e.enemyKey}|${tag}`;
const cur = map.get(k) || { enemyKey: e.enemyKey, tag, count: 0 };
cur.count += Number.isFinite(e.count) && e.count > 0 ? e.count : 1;
map.set(k, cur);
}
return [...map.values()].map((row) => {
const en = getEnemy(row.enemyKey);
return { ...row, name: en?.name || row.enemyKey, rank: en?.rank || 'NORMAL', acTypes: Array.isArray(en?.acTypes) ? en.acTypes : (en?.acType ? [en.acType] : []), fly: en?.stats?.motion === 'FLY' || !!en?.isFlyEnemy };
}).sort((a, b) => (TAG_ORDER[a.tag] ?? 9) - (TAG_ORDER[b.tag] ?? 9)
|| (RANK_ORDER[a.rank] ?? 9) - (RANK_ORDER[b.rank] ?? 9)
|| b.count - a.count || (a.enemyKey < b.enemyKey ? -1 : 1));
}
// ---- enemy preview pen (research 09 §2, research 08 §4.2) ---------------------------------------------------
/** Pen rect and zones: rows 14–18 × cols 7–13, row 16 unused; lower-gate zone rows 14–15, upper-gate zone rows 17–18. */
export const PEN = Object.freeze({ r0: 14, r1: 18, c0: 7, c1: 13, emptyRow: 16, cap: 50, anchors: Object.freeze({ lower: [15, 7], upper: [18, 7] }) });
/** Zone tiles in row-major order (low row first, col 7→13), without the zone's anchor (tile_start) — 13 per zone. */
export function penZoneTiles(gate) {
const rows = gate === 'upper' ? [17, 18] : [14, 15];
const [ar, ac] = PEN.anchors[gate === 'upper' ? 'upper' : 'lower'];
const out = [];
for (const r of rows) for (let c = PEN.c0; c <= PEN.c1; c++) if (!(r === ar && c === ac)) out.push([r, c]);
return out;
}
/**
* Placement of the preview models in the pen for the DOM fallback view — the SAME layout as the render engine
* (render/pen.js layoutPen: research 08 §4.2, client AutoChessEnemyPreviewManager — spawn-time order; > 50 enemies
* thinned per entry to max(1, round(count·50/total)), elites / leaders always all of theirs; each gate's models in its
* zone, the k-th model of a zone at zoneTiles[round(k/totalShowCnt·len) − jitter]; ≤ 3 per tile; seeded, so a re-sent
* m.private never reshuffles it). Both views show one teammate's pen identically (review regression: the fallback used
* its own variant — global k, no per-tile cap — and placed the same enemies elsewhere). `slot` numbers the models
* sharing a tile.
* @param {Array<{ enemyKey: string, count?: number, gate?: string, t?: number, fly?: boolean, elite?: boolean, boss?: boolean }>} entries m.private.nextEnemies
* @param {{ cap?: number, stage?: any }} [opts] cap = MAX_PREVIEW_CNT (50); stage = the stage (its pen rect / tiles)
* @returns {Array<{ enemyKey: string, gate: 'upper'|'lower', row: number, col: number, slot: number, fly: boolean, elite: boolean, boss: boolean, k: number }>}
*/
export function penPlacement(entries, opts = {}) {
const max = Number.isInteger(opts.cap) && opts.cap > 0 ? opts.cap : PEN.cap;
const { figures } = layoutPen(Array.isArray(entries) ? entries : [], { stage: opts.stage || null, max });
const perTile = new Map();
return figures.map((f, k) => {
const key = `${f.row},${f.col}`;
const slot = perTile.get(key) || 0;
perTile.set(key, slot + 1);
return { enemyKey: f.enemyKey, gate: f.zone === 'upper' ? 'upper' : 'lower', row: f.row, col: f.col, slot, fly: !!f.fly, elite: !!f.elite, boss: !!f.boss, k };
});
}
/**
* The enemy a view `pieceClick` points at in the preview pen, or null: `{ enemyKey }` or a `preview` enemy unit
* (`{ preview: true, unit: { side: 'enemy', defId } }`). Battle units (live fights) are not pen enemies.
* @param {any} e
*/
export function previewEnemyKey(e) {
if (!isObj(e)) return null;
if (typeof e.enemyKey === 'string' && e.enemyKey) return e.enemyKey;
const u = isObj(e.unit) ? e.unit : null;
if (!u || !(e.preview || u.preview)) return null;
if (u.side !== 'enemy' && u.kind !== 'enemy') return null;
const k = typeof u.enemyKey === 'string' && u.enemyKey ? u.enemyKey : u.defId;
return typeof k === 'string' && k ? k : null;
}
/** Normalise m.public.factions (type strings or objects) to type ids. */
export function factionTypes(factions) {
const out = [];
for (const f of Array.isArray(factions) ? factions : []) {
const t = typeof f === 'string' ? f : isObj(f) ? (f.type || f.id || f.key) : null;
if (typeof t === 'string' && t && !out.includes(t)) out.push(t);
}
return out;
}