Files
ark-stronghold-protocol/public/js/ui/gameLogic/loadout.js
T
IceCodeNew 0ca2a9c21a feedback7 R22-LATE: an open detail card draws its field unit's current attack range — own board pieces in prep, any ally operator / summon in battle or on a scouted board; golden: unchanged
GitHub PR #281 by @IceCodeNew (this commit is substantially the PR, ported onto 0.2.2-dev; the field-detail range
idea after @BBleae's fork commits c6437f1 / b451bad, credited here instead of the PR's Co-authored-by trailers). The
unofficial skill-range preview the PR first had is gone (the owner's reply of 2026-10-07: only official-like
behaviour); what remains is the range the unit fights with now.

screens/game/range.js inspectRange resolves the open card's unit: an own board piece in prep (its loadout or
stand-in record, its facing — this replaces the prep selection's range effect); in battle or on a scouted board an
ally op / token on the field, at the snapshot's tile, with the live stats' range when there is one; never an enemy,
a dead or undeployed unit, or a scouted bench unit (UnitInfo `area` hand / temp — the tag the server sends since the
#385 fix; renderInfo now keeps it). shared/protocol.js unitStatsEntry gains an optional `dir` (the unit's facing now:
the snapshot tuples carry none and UnitInfo.dir is the facing at send time). tokenVariantFor moves to
gameLogic/loadout.js. Drag, the wheel and the pen clear the overlay.

Added on top of the PR: a test from the server's own scout m.field (board operator ranged, hand / temp none), docs
(PLAYING.md, design/client.md, design/network.md), comments.
Tests: test/ui/inspect-range.test.js (6), test/ui/playtest6_summons.test.js, test/match/unitStats.test.js;
test/ui/inspect-range.e2e.test.js (SP_E2E + CHROME_PATH: run here, 1/1). Golden unchanged (display; m.unitStats only).
2026-10-08 21:27:33 +08:00

81 lines
4.9 KiB
JavaScript

// ui/gameLogic/loadout.js — skill / module loadout shown on cards. Re-exported from ../gameLogic.js.
import { MODULE_NONE, loadoutOptions, resolveLoadout } from '../../../../shared/protocol.js';
import { loadoutRecord, resolveRecordLoadout } from '../../../../shared/loadoutRecord.js';
import { isObj } from './shared.js';
import { t } from '../../../../shared/i18n.js';
/** The summon owner's variant: exact chess id, normal sibling, then the first available variant. */
export function tokenVariantFor(token, ownerId = null) {
const vs = token?.variants || {};
if (typeof ownerId === 'string') {
const v = vs[ownerId] || vs[ownerId.replace(/_b$/, '_a')];
if (v) return v;
}
return Object.values(vs)[0] || null;
}
// ---- operator loadout (DESIGN §16) ----------------------------------------------------------------------------------
/**
* The skill / module a chess fights with under the player's loadout (m.private.loadout, DESIGN §16) — what the shop
* card and the detail panel show. Works with data that already lists every selectable skill (`skills[]`, elite
* `modules[]`) and with older data (only the default `skill` / `module`).
* @param {any} chess the chess record (normal or elite)
* @param {any} loadout m.private.loadout `{ [baseChessId]: { skill, module } }` (entries equal to the defaults may be
* missing); null → defaults
* @param {(id: string) => any} [getChess]
* @returns {{ skill: any, skillIndex: number|null, defaultSkill: boolean, module: { id: string, name: string, typeName: string, none: boolean }|null,
* defaultModule: boolean, changed: boolean, choices: number } | null}
*/
export function chessLoadout(chess, loadout, getChess = () => null) {
if (!isObj(chess)) return null;
const lo = isObj(loadout) && !Array.isArray(loadout) ? loadout : null;
let r = { skillIndex: null, moduleId: null };
try { r = resolveLoadout(lo, chess, getChess); } catch { /* defaults */ }
const skills = Array.isArray(chess.skills) && chess.skills.length ? chess.skills.filter(isObj) : (isObj(chess.skill) ? [chess.skill] : []);
const skill = skills.find((s) => s.index === r.skillIndex) || (isObj(chess.skill) ? chess.skill : null) || skills[0] || null;
const base = chess.isGolden ? (getChess(chess.baseId) || chess) : chess;
let opt = { defaultSkill: null, defaultModule: null, skills: [] };
try { opt = loadoutOptions(base, chess.isGolden ? chess : null); } catch { /* defaults */ }
const defIdx = opt.defaultSkill ?? chess.skill?.index ?? null;
const defaultSkill = !skill || skill.index == null || defIdx == null || skill.index === defIdx;
let module = null;
let defaultModule = true;
if (chess.isGolden) {
const id = r.moduleId ?? (chess.module?.active ? chess.module.id : MODULE_NONE);
if (id === MODULE_NONE) module = { id: MODULE_NONE, name: t('未装备模组'), typeName: '', none: true };
else {
const rec = (Array.isArray(chess.modules) ? chess.modules : []).find((m) => isObj(m) && m.uniEquipId === id)
|| (isObj(chess.module) && chess.module.id === id ? { uniEquipId: id, name: chess.module.name, typeName: chess.module.type } : null);
module = { id, name: rec?.name || id, typeName: rec?.typeName || rec?.type || '', none: false };
}
defaultModule = opt.defaultModule == null || id === opt.defaultModule;
}
// the record the unit fights with (stats / 特性 / talents of the chosen module or none — the battle's own composition,
// shared/loadoutRecord.js): the detail card must show what the sim runs
let record = chess;
try { record = loadoutRecord(chess, resolveRecordLoadout(chess, { skillIndex: r.skillIndex, moduleId: r.moduleId })) || chess; } catch { /* the record as is */ }
return { skill, skillIndex: skill?.index ?? null, defaultSkill, module, defaultModule, changed: !defaultSkill || !defaultModule,
choices: Math.max(skills.length, opt.skills?.length || 0), record };
}
/**
* The loadout (m.private.loadout shape) a battle / scouting unit fights with, from the unit itself — a teammate's
* operator shows ITS owner's skill / module (DESIGN §16), not the viewer's nor the defaults: UnitInfo `skillIndex`
* (sim units, prep scouting m.field units) and `moduleId` (elite only), keyed by the chess's base id for chessLoadout.
* Null when the unit carries neither (the defaults).
* @param {any} chess the unit's chess record (normal or elite)
* @param {any} unit UnitInfo
* @returns {Record<string, { skill?: number, module?: string }> | null}
*/
export function unitLoadout(chess, unit) {
if (!isObj(chess) || !isObj(unit)) return null;
const baseId = chess.baseId || chess.chessId;
if (typeof baseId !== 'string' || !baseId) return null;
const e = {};
if (Number.isInteger(unit.skillIndex) && unit.skillIndex >= 0) e.skill = unit.skillIndex;
if (chess.isGolden && typeof unit.moduleId === 'string' && unit.moduleId) e.module = unit.moduleId;
return Object.keys(e).length ? { [baseId]: e } : null;
}