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 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<HTMLElement>
onPositionValidated: (newPos: Pos) => void
onRemove: () => void
courtRef: RefObject<HTMLElement>
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<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
* */
export default function CourtPlayer({
export function EditableCourtPlayer({
playerInfo,
className,
courtRef,
onPositionValidated,
onRemove,
courtRef,
availableActions,
}: CourtPlayerProps) {
const usesBall = playerInfo.ballState != BallState.NONE
const { x, y } = playerInfo.pos
}: EditableCourtPlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null)
const { x, y } = playerInfo.pos
return (
<Draggable
handle=".player-piece"
nodeRef={pieceRef}
//The piece is positioned using top/bottom style attributes instead
position={NULL_POS}
onStop={useCallback(() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect()
@ -53,9 +70,44 @@ export default function CourtPlayer({
)
onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}>
{courtPlayerPiece({
playerInfo,
className,
pieceRef,
availableActions: () => availableActions(pieceRef.current!),
onKeyUp: useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
},
[onRemove],
),
})}
</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
id={playerInfo.id}
ref={pieceRef}
id={playerInfo.id}
className={"player " + (className ?? "")}
style={{
position: "absolute",
@ -65,15 +117,15 @@ export default function CourtPlayer({
<div
tabIndex={0}
className="player-content"
onKeyUp={useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
},
[onRemove],
)}>
onKeyUp={onKeyUp}
>
{
availableActions && (
<div className="player-actions">
{availableActions(pieceRef.current!)}
{availableActions()}
</div>
)
}
<PlayerPiece
team={playerInfo.team}
text={playerInfo.role}
@ -81,6 +133,5 @@ export default function CourtPlayer({
/>
</div>
</div>
</Draggable>
)
}

@ -67,7 +67,6 @@ export function getPrecomputedPosition(
return computedPositions.get(phantom.id)
}
export function computePhantomPositioning(
phantom: PlayerPhantom,
content: StepContent,

@ -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
}
}
@ -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({
childContent = updateComponent(
{
...childComponent,
pos: parentComponent.pos,
}, childContent)
},
childContent,
)
}
}

@ -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 }
/**

@ -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

@ -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<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 = parseInt(idStr!)
const navigation = useNavigate()
@ -338,7 +315,6 @@ function UserModeEditor() {
[tacticId, stepId, stepsTree],
)
const [stepContent, setStepContent, saveState] =
useContentState<ComputedStepContent>(
{
@ -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) && (
<BallAction
key={2}
@ -688,10 +672,18 @@ function EditorPage({
}
} else {
info = component
if (component.frozen) {
return <CourtPlayer
playerInfo={info}
className={"player"}
availableActions={() => renderAvailablePlayerActions(info, component)}
/>
}
}
return (
<CourtPlayer
<EditableCourtPlayer
key={component.id}
className={isPhantom ? "phantom" : "player"}
playerInfo={info}
@ -1206,7 +1198,6 @@ function useContentState<S>(
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
}
Loading…
Cancel
Save