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).
60 lines
4.7 KiB
JavaScript
60 lines
4.7 KiB
JavaScript
// public/js/render/app/info.js — battle-unit info handed to a view, and which deaths draw particles.
|
|
|
|
/**
|
|
* 'die' reason of an operator that enters the battle already knocked out — a 联防 helper's operator down at the end of
|
|
* its own combat, deployed and forced out at once (server/sim/constants.js FORCED_EXIT, user playtest #5 item 2): its
|
|
* view goes straight to the held knocked-down pose (UnitView.die(true)) without the death burst; b.snap `down` keeps it.
|
|
*/
|
|
export const FORCED_EXIT = 'forcedExit';
|
|
|
|
/**
|
|
* Generic death particles for a battle unit's 'die' event: never for stage devices, nor for a summon used up by its
|
|
* own effect (user playtest #2 item 4: 香槟炸弹's blast — sim fx `{ consumed: true, id }` before its 'die' — is its end,
|
|
* not a knock-out), nor for an operator entering the battle knocked out (FORCED_EXIT).
|
|
*/
|
|
export const showsDeathFx = (info, consumed = false, reason = null) => !consumed && reason !== FORCED_EXIT && info?.kind !== 'device';
|
|
|
|
/**
|
|
* The views' info of a battle unit from its UnitInfo (m.field / fieldMeta `units`, a 'spawn' event; snapshot.js
|
|
* unitInfo), sanitised; null for a malformed entry. `form` — the unit's current model form (an enemy's, content/enemies.js
|
|
* setForm: 转译基底·α's forms, a 逐火 余烬, a leader after its 重生, 掠海漂移体's crawl; a 傀儡师 fighting as its 替身, sim
|
|
* professions.js) — makes a view built mid-battle (a teammate's
|
|
* field watched later, 联防 observers, a reconnect, server-run watchers: no fx of the change is replayed) start on that
|
|
* clip set (render/units.js FORMS); it used to be dropped here, so such views drew the first form (player report #5).
|
|
*/
|
|
export function renderInfo(u) {
|
|
if (!u || typeof u !== 'object' || (typeof u.id !== 'number' && typeof u.id !== 'string')) return null;
|
|
return {
|
|
id: u.id, uid: u.uid ?? null, kind: u.kind || 'enemy', side: u.side === 'ally' ? 'ally' : 'enemy', ownerId: u.ownerId ?? null,
|
|
defId: u.defId ?? null, name: u.name ?? '', tier: u.tier ?? 1, golden: !!u.golden, spine: u.spine ?? u.defId ?? null,
|
|
avatar: u.avatar ?? u.defId ?? null, x: Number(u.x) || 0, y: Number(u.y) || 0, facing: u.facing === -1 ? -1 : 1,
|
|
maxHp: Number(u.maxHp) || 1, boss: !!u.boss, motion: u.motion,
|
|
// deploy direction of allies (UnitInfo.dir, DESIGN §3): the model (Back for UP, mirrored for LEFT) and the
|
|
// ground wedge follow it; absent = unknown (legacy frames) → derived from `facing`, no wedge
|
|
dir: typeof u.dir === 'string' ? u.dir : undefined,
|
|
// the unit's current model form (UnitInfo.form: an enemy's mode, a 傀儡师's 替身): the view starts in it (UnitView reads info.form)
|
|
form: typeof u.form === 'string' ? u.form : undefined,
|
|
// DESIGN §16 loadout of an ally (UnitInfo.skillIndex / moduleId): the Spine actor plays that skill's clip, and a
|
|
// tap hands them to the detail card (a teammate's unit shows its owner's skill / module)
|
|
skillIndex: Number.isInteger(u.skillIndex) ? u.skillIndex : undefined,
|
|
// an ally whose skill is an ammo magazine (UnitInfo.ammoSkill): its rounds are the cells under the HP bar, so no sustained
|
|
// skill aura is drawn for it (render/fx/rings.js _aura)
|
|
ammoSkill: u.ammoSkill === true ? true : undefined,
|
|
moduleId: typeof u.moduleId === 'string' ? u.moduleId : undefined,
|
|
// Scouted hand and temp units have details but no field attack range.
|
|
area: typeof u.area === 'string' ? u.area : undefined,
|
|
// the ally's equipped item ids (UnitInfo.items, DESIGN §16 / §21.11): the detail card needs them for a teammate's
|
|
// unit (resolveDetail `unitItems` → the read-only 装备 section and the 变形同构体 pairing chips); the owner's own
|
|
// unit takes its items from the piece instead, so only other players' boards ever read this field
|
|
items: Array.isArray(u.items) ? u.items.filter((x) => typeof x === 'string') : undefined,
|
|
// 0.2.0 补位: the replaced operator's charId of a chess fighting as its stand-in (UnitInfo.standInFor; spine / avatar /
|
|
// name are already the stand-in's) — the detail card and the view's data lookups (attack interval, splash FX) follow it
|
|
standInFor: typeof u.standInFor === 'string' && u.standInFor ? u.standInFor : undefined,
|
|
// 0.2.0 自选编队: the pick of a DIY slot's unit (UnitInfo.diy { charId, skillIndex, uniEquipId }; spine / avatar / name
|
|
// are already the operator's) — the detail card composes the operator from it, the data lookups follow it
|
|
diy: u.diy && typeof u.diy === 'object' && typeof u.diy.charId === 'string'
|
|
? { charId: u.diy.charId, skillIndex: Number.isInteger(u.diy.skillIndex) ? u.diy.skillIndex : null, uniEquipId: typeof u.diy.uniEquipId === 'string' ? u.diy.uniEquipId : null }
|
|
: undefined,
|
|
};
|
|
}
|