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.
This commit is contained in:
sganggs committed 2026-10-05 17:06:45 +08:00
1 parent 8ab6d021da
commit eb04f5dfa4
16 files changed
+1864 -1778

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
// 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;
}