add read only court players

pull/114/head
maxime 1 year ago
parent 72273e3f3e
commit f5b7b61411

@ -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 "../../style/player.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
@ -8,38 +8,55 @@ import { NULL_POS, Pos, ratioWithinBase } from "../../geo/Pos"
export interface CourtPlayerProps { export interface CourtPlayerProps {
playerInfo: PlayerInfo playerInfo: PlayerInfo
className?: string className?: string
availableActions: (ro: HTMLElement) => ReactNode[]
}
export interface EditableCourtPlayerProps extends CourtPlayerProps {
courtRef: RefObject<HTMLElement>
onPositionValidated: (newPos: Pos) => void onPositionValidated: (newPos: Pos) => void
onRemove: () => void onRemove: () => void
courtRef: RefObject<HTMLElement>
availableActions: (ro: HTMLElement) => ReactNode[]
} }
const MOVE_AREA_SENSIBILITY = 0.001 const MOVE_AREA_SENSIBILITY = 0.001
export const PLAYER_RADIUS_PIXELS = 20 export const PLAYER_RADIUS_PIXELS = 20
export function CourtPlayer({
playerInfo,
className,
availableActions,
}: CourtPlayerProps) {
const pieceRef = useRef<HTMLDivElement>(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 * A player that is placed on the court, which can be selected, and moved in the associated bounds
* */ * */
export default function CourtPlayer({ export function EditableCourtPlayer({
playerInfo, playerInfo,
className, className,
courtRef,
onPositionValidated,
onRemove, onPositionValidated,
courtRef, onRemove,
availableActions, availableActions,
}: CourtPlayerProps) { }: EditableCourtPlayerProps) {
const usesBall = playerInfo.ballState != BallState.NONE
const { x, y } = playerInfo.pos
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
const { x, y } = playerInfo.pos
return ( return (
<Draggable <Draggable
handle=".player-piece" handle=".player-piece"
nodeRef={pieceRef} nodeRef={pieceRef}
//The piece is positioned using top/bottom style attributes instead
position={NULL_POS} position={NULL_POS}
onStop={useCallback(() => { onStop={useCallback(() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect() const pieceBounds = pieceRef.current!.getBoundingClientRect()
@ -53,34 +70,68 @@ export default function CourtPlayer({
) )
onPositionValidated(pos) onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}> }, [courtRef, onPositionValidated, x, y])}>
<div
id={playerInfo.id} {courtPlayerPiece({
ref={pieceRef} playerInfo,
className={"player " + (className ?? "")} className,
style={{ pieceRef,
position: "absolute", availableActions: () => availableActions(pieceRef.current!),
left: `${x * 100}%`, onKeyUp: useCallback(
top: `${y * 100}%`, (e: React.KeyboardEvent<HTMLDivElement>) => {
}}> if (e.key == "Delete") onRemove()
<div },
tabIndex={0} [onRemove],
className="player-content" ),
onKeyUp={useCallback( })}
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
},
[onRemove],
)}>
<div className="player-actions">
{availableActions(pieceRef.current!)}
</div>
<PlayerPiece
team={playerInfo.team}
text={playerInfo.role}
hasBall={usesBall}
/>
</div>
</div>
</Draggable> </Draggable>
) )
} }
interface CourtPlayerPieceProps extends CourtPlayerProps {
pieceRef?: RefObject<HTMLDivElement>
availableActions?: () => ReactNode[]
onKeyUp?: KeyboardEventHandler<HTMLDivElement>
}
function courtPlayerPiece({
playerInfo,
className,
pieceRef,
onKeyUp,
availableActions,
}: CourtPlayerPieceProps) {
const usesBall = playerInfo.ballState != BallState.NONE
const { x, y } = playerInfo.pos
return (
<div
ref={pieceRef}
id={playerInfo.id}
className={"player " + (className ?? "")}
style={{
position: "absolute",
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<div
tabIndex={0}
className="player-content"
onKeyUp={onKeyUp}
>
{
availableActions && (
<div className="player-actions">
{availableActions()}
</div>
)
}
<PlayerPiece
team={playerInfo.team}
text={playerInfo.role}
hasBall={usesBall}
/>
</div>
</div>
)
}

@ -67,7 +67,6 @@ export function getPrecomputedPosition(
return computedPositions.get(phantom.id) return computedPositions.get(phantom.id)
} }
export function computePhantomPositioning( export function computePhantomPositioning(
phantom: PlayerPhantom, phantom: PlayerPhantom,
content: StepContent, content: StepContent,
@ -129,11 +128,11 @@ export function computePhantomPositioning(
pivotPoint = pivotPoint =
playerBeforePhantom.type === "phantom" playerBeforePhantom.type === "phantom"
? computePhantomPositioning( ? computePhantomPositioning(
playerBeforePhantom, playerBeforePhantom,
content, content,
computedPositions, computedPositions,
area, area,
) )
: playerBeforePhantom.pos : playerBeforePhantom.pos
} }
} }

@ -1,33 +1,12 @@
import { equals, Pos, ratioWithinBase } from "../geo/Pos" import { equals, Pos, ratioWithinBase } from "../geo/Pos"
import { import { BallState, Player, PlayerInfo, PlayerLike, PlayerPhantom, PlayerTeam } from "../model/tactic/Player"
BallState, import { Ball, BALL_ID, BALL_TYPE, CourtObject } from "../model/tactic/CourtObjects"
Player, import { ComponentId, StepContent, TacticComponent } from "../model/tactic/Tactic"
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 { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems" import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { import { getComponent, getOrigin, getPrecomputedPosition, tryGetComponent } from "./PlayerDomains"
getComponent,
getOrigin,
getPrecomputedPosition,
tryGetComponent,
} from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts" import { ActionKind } from "../model/tactic/Action.ts"
import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts"
@ -47,6 +26,7 @@ export function placePlayerAt(
ballState: BallState.NONE, ballState: BallState.NONE,
path: null, path: null,
actions: [], actions: [],
frozen: false
} }
} }
@ -197,9 +177,9 @@ export function moveComponent(
phantomIdx == 0 phantomIdx == 0
? origin ? origin
: getComponent( : getComponent(
originPathItems[phantomIdx - 1], originPathItems[phantomIdx - 1],
content.components, content.components,
) )
// detach the action from the screen target and transform it to a regular move action to the phantom. // detach the action from the screen target and transform it to a regular move action to the phantom.
content = updateComponent( content = updateComponent(
{ {
@ -207,18 +187,18 @@ export function moveComponent(
actions: playerBeforePhantom.actions.map((a) => actions: playerBeforePhantom.actions.map((a) =>
a.target === referent a.target === referent
? { ? {
...a, ...a,
segments: a.segments.toSpliced( segments: a.segments.toSpliced(
a.segments.length - 2, a.segments.length - 2,
1, 1,
{ {
...a.segments[a.segments.length - 1], ...a.segments[a.segments.length - 1],
next: component.id, next: component.id,
}, },
), ),
target: component.id, target: component.id,
type: ActionKind.MOVE, type: ActionKind.MOVE,
} }
: a, : a,
), ),
}, },
@ -231,9 +211,9 @@ export function moveComponent(
...component, ...component,
pos: isPhantom pos: isPhantom
? { ? {
type: "fixed", type: "fixed",
...newPos, ...newPos,
} }
: newPos, : newPos,
}, },
content, content,
@ -312,7 +292,7 @@ export function computeTerminalState(
content.components.filter((c) => c.type !== "phantom") as ( content.components.filter((c) => c.type !== "phantom") as (
| Player | Player
| CourtObject | CourtObject
)[] )[]
const componentsTargetedState = nonPhantomComponents.map((comp) => const componentsTargetedState = nonPhantomComponents.map((comp) =>
comp.type === "player" comp.type === "player"
@ -365,6 +345,7 @@ function getPlayerTerminalState(
ballState: stateAfter(player.ballState), ballState: stateAfter(player.ballState),
actions: [], actions: [],
pos, pos,
frozen: true,
} }
} }
const lastPhantomId = phantoms[phantoms.length - 1] const lastPhantomId = phantoms[phantoms.length - 1]
@ -384,6 +365,7 @@ function getPlayerTerminalState(
ballState: stateAfter(lastPhantom.ballState), ballState: stateAfter(lastPhantom.ballState),
id: player.id, id: player.id,
pos, pos,
frozen: true
} }
} }
@ -410,7 +392,11 @@ export function drainTerminalStateOnChildContent(
} }
// ensure that the component is a player // 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( const newContentResult = spreadNewStateFromOriginStateChange(
childComponent, childComponent,
@ -424,10 +410,13 @@ export function drainTerminalStateOnChildContent(
// also update the position of the player if it has been moved // also update the position of the player if it has been moved
if (!equals(childComponent.pos, parentComponent.pos)) { if (!equals(childComponent.pos, parentComponent.pos)) {
gotUpdated = true gotUpdated = true
childContent = updateComponent({ childContent = updateComponent(
...childComponent, {
pos: parentComponent.pos, ...childComponent,
}, childContent) pos: parentComponent.pos,
},
childContent,
)
} }
} }

@ -7,7 +7,6 @@ export function equals(a: Pos, b: Pos): boolean {
return a.x === b.x && a.y === b.y return a.x === b.x && a.y === b.y
} }
export const NULL_POS: Pos = { x: 0, y: 0 } export const NULL_POS: Pos = { x: 0, y: 0 }
/** /**

@ -48,6 +48,8 @@ export interface Player extends Component<"player", Pos>, PlayerInfo {
readonly ballState: BallState readonly ballState: BallState
readonly path: MovementPath | null readonly path: MovementPath | null
readonly frozen: boolean
} }
export interface MovementPath { export interface MovementPath {
@ -71,8 +73,6 @@ export type PhantomPositioning =
| FixedPhantomPositioning | FixedPhantomPositioning
| FollowsPhantomPositioning | FollowsPhantomPositioning
/** /**
* A player phantom is a kind of component that represents the future state of a player * A player phantom is a kind of component that represents the future state of a player
* according to the court's step information * according to the court's step information

@ -19,20 +19,10 @@ import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack" import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece" import { PlayerPiece } from "../components/editor/PlayerPiece"
import { import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent, TacticInfo } from "../model/tactic/Tactic"
ComponentId,
CourtType,
StepContent,
StepInfoNode,
TacticComponent,
TacticInfo,
} from "../model/tactic/Tactic"
import { fetchAPI, fetchAPIGet } from "../Fetcher" import { fetchAPI, fetchAPIGet } from "../Fetcher"
import SavingState, { import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState"
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects" import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "../components/editor/CourtAction" import { CourtAction } from "../components/editor/CourtAction"
@ -53,16 +43,10 @@ import {
updateComponent, updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
import { import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player"
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer" import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx"
import { import {
createAction, createAction,
getActionKind, getActionKind,
@ -74,21 +58,11 @@ import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action" import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction" import BallAction from "../components/actions/BallAction"
import { import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains"
computePhantomPositioning,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall" import { CourtBall } from "../components/editor/CourtBall"
import { useNavigate, useParams } from "react-router-dom" import { useNavigate, useParams } from "react-router-dom"
import StepsTree from "../components/editor/StepsTree" import StepsTree from "../components/editor/StepsTree"
import { import { addStepNode, getAvailableId, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain"
addStepNode,
getAvailableId,
getParent,
getStepNode,
removeStepNode,
} from "../editor/StepsDomain"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -282,7 +256,10 @@ function GuestModeEditor() {
function UserModeEditor() { function UserModeEditor() {
const [tactic, setTactic] = useState<TacticDto | null>(null) const [tactic, setTactic] = useState<TacticDto | null>(null)
const [stepsTree, setStepsTree] = useState<StepInfoNode>({ id: ROOT_STEP_ID, children: [] }) const [stepsTree, setStepsTree] = useState<StepInfoNode>({
id: ROOT_STEP_ID,
children: [],
})
const { tacticId: idStr } = useParams() const { tacticId: idStr } = useParams()
const tacticId = parseInt(idStr!) const tacticId = parseInt(idStr!)
const navigation = useNavigate() const navigation = useNavigate()
@ -338,7 +315,6 @@ function UserModeEditor() {
[tacticId, stepId, stepsTree], [tacticId, stepId, stepsTree],
) )
const [stepContent, setStepContent, saveState] = const [stepContent, setStepContent, saveState] =
useContentState<ComputedStepContent>( useContentState<ComputedStepContent>(
{ {
@ -349,7 +325,6 @@ function UserModeEditor() {
useMemo(() => debounceAsync(saveContent, 250), [saveContent]), useMemo(() => debounceAsync(saveContent, 250), [saveContent]),
) )
useEffect(() => { useEffect(() => {
async function initialize() { async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${tacticId}`) const infoResponsePromise = fetchAPIGet(`tactics/${tacticId}`)
@ -380,19 +355,22 @@ function UserModeEditor() {
setStepContent({ content, relativePositions: new Map() }, false) setStepContent({ content, relativePositions: new Map() }, false)
} }
if (tactic === null) if (tactic === null) initialize()
initialize()
}, [tactic, tacticId, idStr, navigation, setStepContent]) }, [tactic, tacticId, idStr, navigation, setStepContent])
const onNameChange = useCallback( const onNameChange = useCallback(
(name: string) => (name: string) =>
fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then((r) => r.ok), fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then(
(r) => r.ok,
),
[tacticId], [tacticId],
) )
const selectStep = useCallback( const selectStep = useCallback(
async (step: number) => { async (step: number) => {
const response = await fetchAPIGet(`tactics/${tacticId}/steps/${step}`) const response = await fetchAPIGet(
`tactics/${tacticId}/steps/${step}`,
)
if (!response.ok) return if (!response.ok) return
setStepId(step) setStepId(step)
setStepContent( setStepContent(
@ -423,7 +401,11 @@ function UserModeEditor() {
const onRemoveStep = useCallback( const onRemoveStep = useCallback(
async (step: StepInfoNode) => { 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)!) setStepsTree(removeStepNode(stepsTree, step)!)
return response.ok return response.ok
}, },
@ -519,9 +501,9 @@ function EditorPage({
: newState : newState
const courtBounds = courtRef.current?.getBoundingClientRect() const courtBounds = courtRef.current?.getBoundingClientRect()
const relativePositions: ComputedRelativePositions = courtBounds ? computeRelativePositions(courtBounds, state) : new Map() const relativePositions: ComputedRelativePositions = courtBounds
? computeRelativePositions(courtBounds, state)
console.log("in set: ", relativePositions) : new Map()
return { return {
content: state, content: state,
@ -617,6 +599,7 @@ function EditorPage({
const renderAvailablePlayerActions = useCallback( const renderAvailablePlayerActions = useCallback(
(info: PlayerInfo, player: PlayerLike) => { (info: PlayerInfo, player: PlayerLike) => {
let canPlaceArrows: boolean let canPlaceArrows: boolean
let isFrozen: boolean = false
if (player.type == "player") { if (player.type == "player") {
canPlaceArrows = canPlaceArrows =
@ -624,6 +607,7 @@ function EditorPage({
player.actions.findIndex( player.actions.findIndex(
(p) => p.type != ActionKind.SHOOT, (p) => p.type != ActionKind.SHOOT,
) == -1 ) == -1
isFrozen = player.frozen
} else { } else {
const origin = getOrigin(player, content.components) const origin = getOrigin(player, content.components)
const path = origin.path! const path = origin.path!
@ -654,7 +638,7 @@ function EditorPage({
setContent={setContent} setContent={setContent}
/> />
), ),
(info.ballState === BallState.HOLDS_ORIGIN || !isFrozen && (info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && ( info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction <BallAction
key={2} key={2}
@ -688,10 +672,18 @@ function EditorPage({
} }
} else { } else {
info = component info = component
if (component.frozen) {
return <CourtPlayer
playerInfo={info}
className={"player"}
availableActions={() => renderAvailablePlayerActions(info, component)}
/>
}
} }
return ( return (
<CourtPlayer <EditableCourtPlayer
key={component.id} key={component.id}
className={isPhantom ? "phantom" : "player"} className={isPhantom ? "phantom" : "player"}
playerInfo={info} playerInfo={info}
@ -1206,7 +1198,6 @@ function useContentState<S>(
return [content, setContentSynced, savingState] return [content, setContentSynced, savingState]
} }
function computeRelativePositions(courtBounds: DOMRect, content: StepContent) { function computeRelativePositions(courtBounds: DOMRect, content: StepContent) {
const relativePositionsCache: ComputedRelativePositions = new Map() const relativePositionsCache: ComputedRelativePositions = new Map()
@ -1220,8 +1211,5 @@ function computeRelativePositions(courtBounds: DOMRect, content: StepContent) {
) )
} }
console.log("computed bounds: ", relativePositionsCache)
return relativePositionsCache return relativePositionsCache
} }

Loading…
Cancel
Save