+
+
+
{selectedToken.name}
+
{selectedToken.side}
+
+ {currentActor && currentActor.toLowerCase() === selectedToken.name.toLowerCase() && (
+
Turn
+ )}
+
+
+
X {selectedToken.x}
+
Y {selectedToken.y}
+
{selectedZone ? selectedZone.name : 'Outside named zone'}
+
+ {objectives.carrier === selectedToken.name && (
+
Carrying gene-core: reduced movement, no run or charge.
+ )}
+
+ {selectedToken.side === 'player' && (isGM || selectedToken.name === authedPlayer) && (
+
+
Move Action
+
+ {['half', 'full', 'charge', 'run'].map((mode) => {
+ const budget = movementBudget(playerMovementByName[selectedToken.name] || normalizeMovement(), mode, objectives.carrier === selectedToken.name);
+ const lockedToOtherMode = Boolean(selectedMoveMode) && selectedMoveMode !== mode;
+ const spent = selectedMoveMode === mode ? selectedMoveSpent : 0;
+ const remaining = Math.max(0, budget - spent);
+ const active = armedMoveMode === mode || selectedMoveMode === mode;
+ return (
+ setArmedMoveMode(armedMoveMode === mode ? '' : mode)}
+ disabled={budget <= 0 || lockedToOtherMode || remaining <= 0}
+ className={`rounded px-3 py-2 text-xs font-semibold ${active ? 'bg-sky-700 text-sky-100' : 'bg-slate-800 text-slate-200'} disabled:opacity-40`}
+ >
+ {mode} {budget > 0 ? `(${remaining}/${budget})` : '(blocked)'}
+
+ );
+ })}
+
+
+ Click one adjacent checker at a time. Movement is spent across the whole round for the chosen mode, and walls/doors/terrain are checked on every single step.
+
+ {selectedMoveMode && (
+
+ Current turn: {selectedMoveMode} movement, {Math.max(0, movementBudget(playerMovementByName[selectedToken.name] || normalizeMovement(), selectedMoveMode, objectives.carrier === selectedToken.name) - selectedMoveSpent)} checker(s) left.
+
+ )}
+