You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
283 lines
11 KiB
283 lines
11 KiB
import { CourtBall } from "./CourtBall"
|
|
|
|
import {
|
|
ReactElement,
|
|
RefObject,
|
|
useCallback,
|
|
useLayoutEffect,
|
|
useState,
|
|
} from "react"
|
|
import CourtPlayer from "./CourtPlayer"
|
|
|
|
import { Player } from "../../model/tactic/Player"
|
|
import { Action, ActionKind } from "../../model/tactic/Action"
|
|
import ArrowAction from "../actions/ArrowAction"
|
|
import { middlePos, ratioWithinBase } from "../../geo/Pos"
|
|
import BallAction from "../actions/BallAction"
|
|
import {BALL_ID} from "../../model/tactic/Ball"
|
|
import { contains, overlaps } from "../../geo/Box"
|
|
|
|
import { CourtAction } from "../../views/editor/CourtAction"
|
|
import { TacticComponent } from "../../model/tactic/Tactic"
|
|
|
|
export interface BasketCourtProps {
|
|
components: TacticComponent[]
|
|
actions: Action[]
|
|
|
|
renderAction: (a: Action, key: number) => ReactElement
|
|
setActions: (f: (a: Action[]) => Action[]) => void
|
|
|
|
onPlayerRemove: (p: Player) => void
|
|
onPlayerChange: (p: Player) => void
|
|
|
|
onBallRemove: () => void
|
|
onBallMoved: (ball: DOMRect) => void
|
|
|
|
courtImage: ReactElement
|
|
courtRef: RefObject<HTMLDivElement>
|
|
}
|
|
|
|
export function BasketCourt({
|
|
components,
|
|
actions,
|
|
renderAction,
|
|
setActions,
|
|
onPlayerRemove,
|
|
onPlayerChange,
|
|
|
|
onBallMoved,
|
|
onBallRemove,
|
|
|
|
courtImage,
|
|
courtRef,
|
|
}: BasketCourtProps) {
|
|
function placeArrow(origin: Player, arrowHead: DOMRect) {
|
|
const originRef = document.getElementById(origin.id)!
|
|
const courtBounds = courtRef.current!.getBoundingClientRect()
|
|
const start = ratioWithinBase(
|
|
middlePos(originRef.getBoundingClientRect()),
|
|
courtBounds,
|
|
)
|
|
|
|
for (const component of components) {
|
|
if (component.id == origin.id) {
|
|
continue
|
|
}
|
|
|
|
const playerBounds = document
|
|
.getElementById(component.id)!
|
|
.getBoundingClientRect()
|
|
|
|
if (overlaps(playerBounds, arrowHead)) {
|
|
const targetPos = document
|
|
.getElementById(component.id)!
|
|
.getBoundingClientRect()
|
|
|
|
const end = ratioWithinBase(middlePos(targetPos), courtBounds)
|
|
|
|
const action: Action = {
|
|
fromId: originRef.id,
|
|
toId: component.id,
|
|
type:
|
|
component.type == "player"
|
|
? origin.hasBall
|
|
? ActionKind.SHOOT
|
|
: ActionKind.SCREEN
|
|
: ActionKind.MOVE,
|
|
moveFrom: start,
|
|
segments: [{ next: end }],
|
|
}
|
|
setActions((actions) => [...actions, action])
|
|
return
|
|
}
|
|
}
|
|
|
|
const action: Action = {
|
|
fromId: originRef.id,
|
|
type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE,
|
|
moveFrom: ratioWithinBase(
|
|
middlePos(originRef.getBoundingClientRect()),
|
|
courtBounds,
|
|
),
|
|
segments: [
|
|
{ next: ratioWithinBase(middlePos(arrowHead), courtBounds) },
|
|
],
|
|
}
|
|
setActions((actions) => [...actions, action])
|
|
}
|
|
|
|
const [previewAction, setPreviewAction] = useState<Action | null>(null)
|
|
|
|
const updateActionsRelatedTo = useCallback((comp: TacticComponent) => {
|
|
const newPos = ratioWithinBase(
|
|
middlePos(
|
|
document.getElementById(comp.id)!.getBoundingClientRect(),
|
|
),
|
|
courtRef.current!.getBoundingClientRect(),
|
|
)
|
|
setActions((actions) =>
|
|
actions.map((a) => {
|
|
if (a.fromId == comp.id) {
|
|
return { ...a, moveFrom: newPos }
|
|
}
|
|
|
|
if (a.toId == comp.id) {
|
|
const segments = a.segments.toSpliced(
|
|
a.segments.length - 1,
|
|
1,
|
|
{
|
|
...a.segments[a.segments.length - 1],
|
|
next: newPos,
|
|
},
|
|
)
|
|
return { ...a, segments }
|
|
}
|
|
|
|
return a
|
|
}),
|
|
)
|
|
}, [])
|
|
|
|
const [internActions, setInternActions] = useState<Action[]>([])
|
|
|
|
useLayoutEffect(() => setInternActions(actions), [actions])
|
|
|
|
return (
|
|
<div
|
|
className="court-container"
|
|
ref={courtRef}
|
|
style={{ position: "relative" }}>
|
|
{courtImage}
|
|
|
|
{components.map((component) => {
|
|
if (component.type == "player") {
|
|
const player = component
|
|
return (
|
|
<CourtPlayer
|
|
key={player.id}
|
|
player={player}
|
|
onDrag={() => updateActionsRelatedTo(player)}
|
|
onChange={onPlayerChange}
|
|
onRemove={() => onPlayerRemove(player)}
|
|
courtRef={courtRef}
|
|
availableActions={(pieceRef) => [
|
|
<ArrowAction
|
|
key={1}
|
|
onHeadMoved={(headPos) => {
|
|
const baseBounds =
|
|
courtRef.current!.getBoundingClientRect()
|
|
|
|
const arrowHeadPos = middlePos(headPos)
|
|
|
|
const target = components.find(
|
|
(c) =>
|
|
c.id != player.id &&
|
|
contains(
|
|
document
|
|
.getElementById(c.id)!
|
|
.getBoundingClientRect(),
|
|
arrowHeadPos,
|
|
),
|
|
)
|
|
|
|
const type =
|
|
target?.type == "player"
|
|
? player.hasBall
|
|
? target
|
|
? ActionKind.SHOOT
|
|
: ActionKind.DRIBBLE
|
|
: target
|
|
? ActionKind.SCREEN
|
|
: ActionKind.MOVE
|
|
: ActionKind.MOVE
|
|
|
|
setPreviewAction((action) => ({
|
|
...action!,
|
|
segments: [
|
|
{
|
|
next: ratioWithinBase(
|
|
arrowHeadPos,
|
|
baseBounds,
|
|
),
|
|
},
|
|
],
|
|
type,
|
|
}))
|
|
}}
|
|
onHeadPicked={(headPos) => {
|
|
;(
|
|
document.activeElement as HTMLElement
|
|
).blur()
|
|
const baseBounds =
|
|
courtRef.current!.getBoundingClientRect()
|
|
|
|
setPreviewAction({
|
|
type: player.hasBall
|
|
? ActionKind.DRIBBLE
|
|
: ActionKind.MOVE,
|
|
fromId: player.id,
|
|
toId: undefined,
|
|
moveFrom: ratioWithinBase(
|
|
middlePos(
|
|
pieceRef.getBoundingClientRect(),
|
|
),
|
|
baseBounds,
|
|
),
|
|
segments: [
|
|
{
|
|
next: ratioWithinBase(
|
|
middlePos(headPos),
|
|
baseBounds,
|
|
),
|
|
},
|
|
],
|
|
})
|
|
}}
|
|
onHeadDropped={(headRect) => {
|
|
placeArrow(player, headRect)
|
|
setPreviewAction(null)
|
|
}}
|
|
/>,
|
|
player.hasBall && (
|
|
<BallAction
|
|
key={2}
|
|
onDrop={(ref) =>
|
|
onBallMoved(
|
|
ref.getBoundingClientRect(),
|
|
)
|
|
}
|
|
/>
|
|
),
|
|
]}
|
|
/>
|
|
)
|
|
}
|
|
if (component.type == BALL_ID) {
|
|
return (
|
|
<CourtBall
|
|
onPosValidated={onBallMoved}
|
|
onMoves={() => updateActionsRelatedTo(component)}
|
|
ball={component}
|
|
onRemove={onBallRemove}
|
|
key="ball"
|
|
/>
|
|
)
|
|
}
|
|
throw new Error("unknown tactic component " + component)
|
|
})}
|
|
|
|
{internActions.map((action, idx) => renderAction(action, idx))}
|
|
|
|
{previewAction && (
|
|
<CourtAction
|
|
courtRef={courtRef}
|
|
action={previewAction}
|
|
//do nothing on change, not really possible as it's a preview arrow
|
|
onActionDeleted={() => {}}
|
|
onActionChanges={() => {}}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|