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).
This commit is contained in:
IceCodeNew authored and sganggs committed 2026-10-08 21:27:33 +08:00
1 parent a1bc37ed90
commit 0ca2a9c21a
12 files changed
+274 -33

No files matched your search

+22 -11
View File
@@ -117,6 +117,7 @@ import { useDocClass, FullscreenButton } from '../ui/device.js';
import { HUD_HZ_MS, MERGE_HL, SEL_RANGE, cx } from './game/marks.js';
import { keepEarly, audioEarly } from './game/early.js';
import { MatchEnded, PausedOverlay } from './game/overlays.js';
import { inspectRange } from './game/range.js';
import { t, tParts } from '../../../shared/i18n.js';
/** Router for the in-match screens. */
@@ -477,6 +478,9 @@ function MatchScreen() {
}
if (earlySnap) {
view.pushSnapshot(earlySnap);
if (Array.isArray(earlySnap.units)) {
snapUnitsRef.current = new Map(earlySnap.units.filter(Array.isArray).map((t) => [t[0], t]));
}
hudRef.current = snapHud(earlySnap);
setHud(hudRef.current);
}
@@ -1045,10 +1049,6 @@ function MatchScreen() {
}, []);
// ---- direction step (research 09 §1.2) and the selected piece's underframe ------------------------------------
// DESIGN §16: previews show the range the unit fights with under the player's loadout (an elite's module grid); a
// chess the player does not own the range of its stand-in (0.2.0 补位, gameLogic deployedRecord)
const lookups = useMemo(() => ({ getChess: ownChess, getToken: gd.token, getItem: gd.item,
chessRecord: (rec) => deployedRecord(rec, live.current.priv, ownChess, gd.backups) }), [gd.ready]);
const heldRef = useRef(new Map()); // uid → { row, col, t } committed placements awaiting m.private
const releaseHold = useCallback((uid) => {
heldRef.current.delete(uid);
@@ -1100,16 +1100,10 @@ function MatchScreen() {
useEffect(() => {
if (facing && (!editable || !placeCtx.pieces.get(facing.uid))) cancelFacing();
}, [editable, placeCtx, facing]);
// the selected piece: gone / not editable → deselect; on the board its range tiles show (rotated to its facing)
// the selected piece: gone / not editable → deselect (its range tiles: the detail card's, cardRange below)
const selEntry = sel ? placeCtx.pieces.get(sel.uid) || null : null;
live.current.showPrep = showPrep;
useEffect(() => { if (sel && (!selEntry || !editable || !showPrep)) setSel(null); }, [sel, selEntry, editable, showPrep]);
const selRangeKey = selEntry && selEntry.area === 'board' ? `${selEntry.piece.uid}:${selEntry.row},${selEntry.col}:${pieceDir(selEntry.piece)}` : '';
useEffect(() => {
if (!view || !selRangeKey) return undefined;
showRange(view, previewGrid(lookups, selEntry.piece), selEntry.row, selEntry.col, pieceDir(selEntry.piece), SEL_RANGE);
return () => showRange(view, null, 0, 0, null, SEL_RANGE);
}, [view, selRangeKey]);
// the selected piece's underframe on screen: the detail card docks on the side away from it (user playtest #2
// item 8 — at some aspect ratios a bench unit's 出售 sat under the left card); the underframe is drawn above every
// panel anyway (css z-index), this keeps it visible too
@@ -1350,6 +1344,23 @@ function MatchScreen() {
}
return priv?.loadout ?? null; // own pieces, shop / reward / bond-member cards
})();
// the open detail card's unit draws its attack range on the field (screens/game/range.js inspectRange, GitHub PR #281):
// an own board piece in prep (its loadout / stand-in record, its facing); in battle or on a scouted board an ally
// operator / summon on the field — the range it fights with now (the live stats' `range` and `dir`, the snapshot's
// tile) — never an enemy, a dead unit or a bench unit (UnitInfo `area` hand / temp); drag, the wheel and the pen
// draw their own
const cardRange = !drag && !facing && !pen ? inspectRange({
target: detailTarget, detail: resolved, pieces: placeCtx.pieces, showPrep, field,
snapshot: snapUnitsRef.current.get(resolved?.unitId),
live: typeof liveStats === 'function' ? liveStats() : liveStats,
loadout: detailLoadout, getChess: gd.chess, backups: gd.backups,
}) : null;
const cardRangeKey = cardRange ? JSON.stringify(cardRange) : '';
useEffect(() => {
if (!view) return undefined;
showRange(view, cardRange?.grid, cardRange?.row, cardRange?.col, cardRange?.dir, SEL_RANGE);
return () => showRange(view, null, 0, 0, null, SEL_RANGE);
}, [view, cardRangeKey, showPrep, field]);
// the card's bond chips — and the popup a chip opens: a battle / scouted unit's OWNER's bonds (yours, or that
// teammate's — m.public + live layers), your own piece's yours, a popup member card the popup's player; shop / reward
// cards read the strip's (ui/watchBonds.js detailBondOwner)