feat(control): трекер инициативы в отдельном окне
Очередь NPC/игроков с DnD, курсором на сцене, состояниями и запасом; сброс при смене сцены. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,6 +14,12 @@ import { usePlayerImageUrl } from './usePlayerImageUrl';
|
||||
|
||||
import { PlayerTokenView } from './PlayerTokenView';
|
||||
import { useLiveDragBroadcast, useLiveResizeBroadcast } from './useLiveDragBroadcast';
|
||||
import {
|
||||
TokenInitiativeDecor,
|
||||
initiativeConditionsForPlayer,
|
||||
initiativeHighlightForPlayer,
|
||||
} from '../initiative/TokenInitiativeDecor';
|
||||
import { useInitiativeState } from '../initiative/useInitiativeState';
|
||||
import styles from './SceneNpcTokensOverlay.module.css';
|
||||
|
||||
type Viewport = { x: number; y: number; w: number; h: number };
|
||||
@@ -31,6 +37,8 @@ function PlayerSprite({
|
||||
onResize,
|
||||
resizeTitle,
|
||||
snapNorm,
|
||||
initiativeActive = false,
|
||||
initiativeConditions = [],
|
||||
}: {
|
||||
player: AppPlayer;
|
||||
nx: number;
|
||||
@@ -44,6 +52,8 @@ function PlayerSprite({
|
||||
onResize?: (playerId: string, sizeN: number) => void;
|
||||
resizeTitle?: string;
|
||||
snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
|
||||
initiativeActive?: boolean;
|
||||
initiativeConditions?: readonly import('../../../shared/types/initiative').InitiativeConditionId[];
|
||||
}) {
|
||||
const imageUrl = usePlayerImageUrl(player.id);
|
||||
const [localPos, setLocalPos] = useState<{ nx: number; ny: number } | null>(null);
|
||||
@@ -100,8 +110,15 @@ function PlayerSprite({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[styles.token, editable ? styles.editable : ''].filter(Boolean).join(' ')}
|
||||
className={[
|
||||
styles.token,
|
||||
editable ? styles.editable : '',
|
||||
initiativeActive ? styles.initiativeActive : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
data-testid={`session-player-token-${player.id}`}
|
||||
data-initiative-active={initiativeActive ? '1' : '0'}
|
||||
style={{
|
||||
left: viewport.x + pos.nx * viewport.w,
|
||||
top: viewport.y + pos.ny * viewport.h,
|
||||
@@ -155,6 +172,7 @@ function PlayerSprite({
|
||||
imageScale={player.imageScale}
|
||||
sizePx={sizePx}
|
||||
/>
|
||||
<TokenInitiativeDecor conditions={initiativeConditions} />
|
||||
{canResize ? (
|
||||
<div
|
||||
className={styles.resizeHandle}
|
||||
@@ -218,6 +236,7 @@ export function ScenePlayerTokensOverlay({
|
||||
resizeTitle?: string;
|
||||
snapNorm?: (nx: number, ny: number) => { nx: number; ny: number };
|
||||
}) {
|
||||
const [initiative] = useInitiativeState();
|
||||
if (!viewport || !session?.visible) return null;
|
||||
const byId = new Map(library.map((p) => [String(p.id), p]));
|
||||
const gridFit = sceneGridTokenFitFactor(grid);
|
||||
@@ -238,6 +257,8 @@ export function ScenePlayerTokensOverlay({
|
||||
editable={editable}
|
||||
displayScale={displayScale}
|
||||
gridFit={gridFit}
|
||||
initiativeActive={initiativeHighlightForPlayer(initiative, playerId)}
|
||||
initiativeConditions={initiativeConditionsForPlayer(initiative, playerId)}
|
||||
{...(onMove ? { onMove } : {})}
|
||||
{...(onResize ? { onResize } : {})}
|
||||
{...(resizeTitle ? { resizeTitle } : {})}
|
||||
|
||||
Reference in New Issue
Block a user