From f5b7b6141198aa8530b9867edc4e6d69a877bcae Mon Sep 17 00:00:00 2001 From: maxime Date: Mon, 4 Mar 2024 12:19:34 +0100 Subject: [PATCH] add read only court players --- src/components/editor/CourtPlayer.tsx | 137 ++++++++++++++++++-------- src/editor/PlayerDomains.ts | 11 +-- src/editor/TacticContentDomains.ts | 87 +++++++--------- src/geo/Pos.ts | 1 - src/model/tactic/Player.ts | 4 +- src/pages/Editor.tsx | 88 +++++++---------- 6 files changed, 177 insertions(+), 151 deletions(-) diff --git a/src/components/editor/CourtPlayer.tsx b/src/components/editor/CourtPlayer.tsx index 6b8e8dd..d85e51f 100644 --- a/src/components/editor/CourtPlayer.tsx +++ b/src/components/editor/CourtPlayer.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, RefObject, useCallback, useRef } from "react" +import React, { KeyboardEventHandler, ReactNode, RefObject, useCallback, useRef } from "react" import "../../style/player.css" import Draggable from "react-draggable" import { PlayerPiece } from "./PlayerPiece" @@ -8,38 +8,55 @@ import { NULL_POS, Pos, ratioWithinBase } from "../../geo/Pos" export interface CourtPlayerProps { playerInfo: PlayerInfo className?: string + availableActions: (ro: HTMLElement) => ReactNode[] +} +export interface EditableCourtPlayerProps extends CourtPlayerProps { + courtRef: RefObject onPositionValidated: (newPos: Pos) => void onRemove: () => void - courtRef: RefObject - availableActions: (ro: HTMLElement) => ReactNode[] } const MOVE_AREA_SENSIBILITY = 0.001 export const PLAYER_RADIUS_PIXELS = 20 +export function CourtPlayer({ + playerInfo, + className, + availableActions, + }: CourtPlayerProps) { + + const pieceRef = useRef(null) + + return courtPlayerPiece({ + playerInfo, + pieceRef, + className, + availableActions: () => availableActions(pieceRef.current!), + }) +} + /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({ - playerInfo, - className, - - onPositionValidated, - onRemove, - courtRef, - availableActions, -}: CourtPlayerProps) { - const usesBall = playerInfo.ballState != BallState.NONE - const { x, y } = playerInfo.pos +export function EditableCourtPlayer({ + playerInfo, + className, + courtRef, + + onPositionValidated, + onRemove, + availableActions, + }: EditableCourtPlayerProps) { const pieceRef = useRef(null) + const { x, y } = playerInfo.pos + return ( { const pieceBounds = pieceRef.current!.getBoundingClientRect() @@ -53,34 +70,68 @@ export default function CourtPlayer({ ) onPositionValidated(pos) }, [courtRef, onPositionValidated, x, y])}> -
-
) => { - if (e.key == "Delete") onRemove() - }, - [onRemove], - )}> -
- {availableActions(pieceRef.current!)} -
- -
-
+ + {courtPlayerPiece({ + playerInfo, + className, + pieceRef, + availableActions: () => availableActions(pieceRef.current!), + onKeyUp: useCallback( + (e: React.KeyboardEvent) => { + if (e.key == "Delete") onRemove() + }, + [onRemove], + ), + })}
) } + +interface CourtPlayerPieceProps extends CourtPlayerProps { + pieceRef?: RefObject + availableActions?: () => ReactNode[] + onKeyUp?: KeyboardEventHandler +} + +function courtPlayerPiece({ + playerInfo, + className, + pieceRef, + onKeyUp, + availableActions, + }: CourtPlayerPieceProps) { + const usesBall = playerInfo.ballState != BallState.NONE + const { x, y } = playerInfo.pos + + + return ( +
+
+ { + availableActions && ( +
+ {availableActions()} +
+ ) + } + +
+
+ ) +} \ No newline at end of file diff --git a/src/editor/PlayerDomains.ts b/src/editor/PlayerDomains.ts index fd5c914..19ca23e 100644 --- a/src/editor/PlayerDomains.ts +++ b/src/editor/PlayerDomains.ts @@ -67,7 +67,6 @@ export function getPrecomputedPosition( return computedPositions.get(phantom.id) } - export function computePhantomPositioning( phantom: PlayerPhantom, content: StepContent, @@ -129,11 +128,11 @@ export function computePhantomPositioning( pivotPoint = playerBeforePhantom.type === "phantom" ? computePhantomPositioning( - playerBeforePhantom, - content, - computedPositions, - area, - ) + playerBeforePhantom, + content, + computedPositions, + area, + ) : playerBeforePhantom.pos } } diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 8eeacd9..15191a8 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -1,33 +1,12 @@ import { equals, Pos, ratioWithinBase } from "../geo/Pos" -import { - BallState, - Player, - PlayerInfo, - PlayerLike, - PlayerPhantom, - PlayerTeam, -} from "../model/tactic/Player" -import { - Ball, - BALL_ID, - BALL_TYPE, - CourtObject, -} from "../model/tactic/CourtObjects" -import { - ComponentId, - StepContent, - TacticComponent, -} from "../model/tactic/Tactic" +import { BallState, Player, PlayerInfo, PlayerLike, PlayerPhantom, PlayerTeam } from "../model/tactic/Player" +import { Ball, BALL_ID, BALL_TYPE, CourtObject } from "../model/tactic/CourtObjects" +import { ComponentId, StepContent, TacticComponent } from "../model/tactic/Tactic" import { overlaps } from "../geo/Box" import { RackedCourtObject, RackedPlayer } from "./RackedItems" -import { - getComponent, - getOrigin, - getPrecomputedPosition, - tryGetComponent, -} from "./PlayerDomains" +import { getComponent, getOrigin, getPrecomputedPosition, tryGetComponent } from "./PlayerDomains" import { ActionKind } from "../model/tactic/Action.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" @@ -47,6 +26,7 @@ export function placePlayerAt( ballState: BallState.NONE, path: null, actions: [], + frozen: false } } @@ -197,9 +177,9 @@ export function moveComponent( phantomIdx == 0 ? origin : getComponent( - originPathItems[phantomIdx - 1], - content.components, - ) + originPathItems[phantomIdx - 1], + content.components, + ) // detach the action from the screen target and transform it to a regular move action to the phantom. content = updateComponent( { @@ -207,18 +187,18 @@ export function moveComponent( actions: playerBeforePhantom.actions.map((a) => a.target === referent ? { - ...a, - segments: a.segments.toSpliced( - a.segments.length - 2, - 1, - { - ...a.segments[a.segments.length - 1], - next: component.id, - }, - ), - target: component.id, - type: ActionKind.MOVE, - } + ...a, + segments: a.segments.toSpliced( + a.segments.length - 2, + 1, + { + ...a.segments[a.segments.length - 1], + next: component.id, + }, + ), + target: component.id, + type: ActionKind.MOVE, + } : a, ), }, @@ -231,9 +211,9 @@ export function moveComponent( ...component, pos: isPhantom ? { - type: "fixed", - ...newPos, - } + type: "fixed", + ...newPos, + } : newPos, }, content, @@ -312,7 +292,7 @@ export function computeTerminalState( content.components.filter((c) => c.type !== "phantom") as ( | Player | CourtObject - )[] + )[] const componentsTargetedState = nonPhantomComponents.map((comp) => comp.type === "player" @@ -365,6 +345,7 @@ function getPlayerTerminalState( ballState: stateAfter(player.ballState), actions: [], pos, + frozen: true, } } const lastPhantomId = phantoms[phantoms.length - 1] @@ -384,6 +365,7 @@ function getPlayerTerminalState( ballState: stateAfter(lastPhantom.ballState), id: player.id, pos, + frozen: true } } @@ -410,7 +392,11 @@ export function drainTerminalStateOnChildContent( } // ensure that the component is a player - if (parentComponent.type !== "player" || childComponent.type !== "player") continue + if ( + parentComponent.type !== "player" || + childComponent.type !== "player" + ) + continue const newContentResult = spreadNewStateFromOriginStateChange( childComponent, @@ -424,10 +410,13 @@ export function drainTerminalStateOnChildContent( // also update the position of the player if it has been moved if (!equals(childComponent.pos, parentComponent.pos)) { gotUpdated = true - childContent = updateComponent({ - ...childComponent, - pos: parentComponent.pos, - }, childContent) + childContent = updateComponent( + { + ...childComponent, + pos: parentComponent.pos, + }, + childContent, + ) } } diff --git a/src/geo/Pos.ts b/src/geo/Pos.ts index d3d7337..0e591b3 100644 --- a/src/geo/Pos.ts +++ b/src/geo/Pos.ts @@ -7,7 +7,6 @@ export function equals(a: Pos, b: Pos): boolean { return a.x === b.x && a.y === b.y } - export const NULL_POS: Pos = { x: 0, y: 0 } /** diff --git a/src/model/tactic/Player.ts b/src/model/tactic/Player.ts index 842bbbe..5f1d3df 100644 --- a/src/model/tactic/Player.ts +++ b/src/model/tactic/Player.ts @@ -48,6 +48,8 @@ export interface Player extends Component<"player", Pos>, PlayerInfo { readonly ballState: BallState readonly path: MovementPath | null + + readonly frozen: boolean } export interface MovementPath { @@ -71,8 +73,6 @@ export type PhantomPositioning = | FixedPhantomPositioning | FollowsPhantomPositioning - - /** * A player phantom is a kind of component that represents the future state of a player * according to the court's step information diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 2fd6627..0c1b792 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -19,20 +19,10 @@ import { BallPiece } from "../components/editor/BallPiece" import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" -import { - ComponentId, - CourtType, - StepContent, - StepInfoNode, - TacticComponent, - TacticInfo, -} from "../model/tactic/Tactic" +import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent, TacticInfo } from "../model/tactic/Tactic" import { fetchAPI, fetchAPIGet } from "../Fetcher" -import SavingState, { - SaveState, - SaveStates, -} from "../components/editor/SavingState" +import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState" import { BALL_TYPE } from "../model/tactic/CourtObjects" import { CourtAction } from "../components/editor/CourtAction" @@ -53,16 +43,10 @@ import { updateComponent, } from "../editor/TacticContentDomains" -import { - BallState, - Player, - PlayerInfo, - PlayerLike, - PlayerTeam, -} from "../model/tactic/Player" +import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" -import CourtPlayer from "../components/editor/CourtPlayer" +import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx" import { createAction, getActionKind, @@ -74,21 +58,11 @@ import ArrowAction from "../components/actions/ArrowAction" import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" import { Action, ActionKind } from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" -import { - computePhantomPositioning, - getOrigin, - removePlayer, -} from "../editor/PlayerDomains" +import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains" import { CourtBall } from "../components/editor/CourtBall" import { useNavigate, useParams } from "react-router-dom" import StepsTree from "../components/editor/StepsTree" -import { - addStepNode, - getAvailableId, - getParent, - getStepNode, - removeStepNode, -} from "../editor/StepsDomain" +import { addStepNode, getAvailableId, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -282,7 +256,10 @@ function GuestModeEditor() { function UserModeEditor() { const [tactic, setTactic] = useState(null) - const [stepsTree, setStepsTree] = useState({ id: ROOT_STEP_ID, children: [] }) + const [stepsTree, setStepsTree] = useState({ + id: ROOT_STEP_ID, + children: [], + }) const { tacticId: idStr } = useParams() const tacticId = parseInt(idStr!) const navigation = useNavigate() @@ -338,7 +315,6 @@ function UserModeEditor() { [tacticId, stepId, stepsTree], ) - const [stepContent, setStepContent, saveState] = useContentState( { @@ -349,7 +325,6 @@ function UserModeEditor() { useMemo(() => debounceAsync(saveContent, 250), [saveContent]), ) - useEffect(() => { async function initialize() { const infoResponsePromise = fetchAPIGet(`tactics/${tacticId}`) @@ -380,19 +355,22 @@ function UserModeEditor() { setStepContent({ content, relativePositions: new Map() }, false) } - if (tactic === null) - initialize() + if (tactic === null) initialize() }, [tactic, tacticId, idStr, navigation, setStepContent]) const onNameChange = useCallback( (name: string) => - fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then((r) => r.ok), + fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then( + (r) => r.ok, + ), [tacticId], ) const selectStep = useCallback( async (step: number) => { - const response = await fetchAPIGet(`tactics/${tacticId}/steps/${step}`) + const response = await fetchAPIGet( + `tactics/${tacticId}/steps/${step}`, + ) if (!response.ok) return setStepId(step) setStepContent( @@ -423,7 +401,11 @@ function UserModeEditor() { const onRemoveStep = useCallback( async (step: StepInfoNode) => { - const response = await fetchAPI(`tactics/${tacticId}/steps/${step.id}`, {}, "DELETE") + const response = await fetchAPI( + `tactics/${tacticId}/steps/${step.id}`, + {}, + "DELETE", + ) setStepsTree(removeStepNode(stepsTree, step)!) return response.ok }, @@ -519,9 +501,9 @@ function EditorPage({ : newState const courtBounds = courtRef.current?.getBoundingClientRect() - const relativePositions: ComputedRelativePositions = courtBounds ? computeRelativePositions(courtBounds, state) : new Map() - - console.log("in set: ", relativePositions) + const relativePositions: ComputedRelativePositions = courtBounds + ? computeRelativePositions(courtBounds, state) + : new Map() return { content: state, @@ -617,6 +599,7 @@ function EditorPage({ const renderAvailablePlayerActions = useCallback( (info: PlayerInfo, player: PlayerLike) => { let canPlaceArrows: boolean + let isFrozen: boolean = false if (player.type == "player") { canPlaceArrows = @@ -624,6 +607,7 @@ function EditorPage({ player.actions.findIndex( (p) => p.type != ActionKind.SHOOT, ) == -1 + isFrozen = player.frozen } else { const origin = getOrigin(player, content.components) const path = origin.path! @@ -654,7 +638,7 @@ function EditorPage({ setContent={setContent} /> ), - (info.ballState === BallState.HOLDS_ORIGIN || + !isFrozen && (info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && ( renderAvailablePlayerActions(info, component)} + /> + } } return ( - ( return [content, setContentSynced, savingState] } - function computeRelativePositions(courtBounds: DOMRect, content: StepContent) { const relativePositionsCache: ComputedRelativePositions = new Map() @@ -1220,8 +1211,5 @@ function computeRelativePositions(courtBounds: DOMRect, content: StepContent) { ) } - console.log("computed bounds: ", relativePositionsCache) - - return relativePositionsCache -} \ No newline at end of file +}