stabilize phantoms, spread changes between players and phantoms if an action changes
continuous-integration/drone/push Build is failing Details

maxime 1 year ago
parent a38f15771a
commit d95e84c413

@ -13,6 +13,8 @@ module.exports = {
'plugin:react/jsx-runtime', 'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended' 'plugin:react-hooks/recommended'
], ],
rules: {
},
settings: { settings: {
react: { react: {
version: 'detect' version: 'detect'

@ -4,13 +4,13 @@ import "../style/title_input.css"
export interface TitleInputOptions { export interface TitleInputOptions {
style: CSSProperties style: CSSProperties
default_value: string default_value: string
on_validated: (a: string) => void onValidated: (a: string) => void
} }
export default function TitleInput({ export default function TitleInput({
style, style,
default_value, default_value,
on_validated, onValidated,
}: TitleInputOptions) { }: TitleInputOptions) {
const [value, setValue] = useState(default_value) const [value, setValue] = useState(default_value)
const ref = useRef<HTMLInputElement>(null) const ref = useRef<HTMLInputElement>(null)
@ -23,7 +23,7 @@ export default function TitleInput({
type="text" type="text"
value={value} value={value}
onChange={(event) => setValue(event.target.value)} onChange={(event) => setValue(event.target.value)}
onBlur={(_) => on_validated(value)} onBlur={(_) => onValidated(value)}
onKeyUp={(event) => { onKeyUp={(event) => {
if (event.key == "Enter") ref.current?.blur() if (event.key == "Enter") ref.current?.blur()
}} }}

@ -44,18 +44,18 @@ export default function ArrowAction({
) )
} }
export function ScreenHead() { export function ScreenHead({color}: {color: string}) {
return ( return (
<div <div
style={{ backgroundColor: "black", height: "5px", width: "25px" }} style={{ backgroundColor: color, height: "5px", width: "25px" }}
/> />
) )
} }
export function MoveToHead() { export function MoveToHead({color}: {color: string}) {
return ( return (
<svg viewBox={"0 0 50 50"} width={20} height={20}> <svg viewBox={"0 0 50 50"} width={20} height={20}>
<polygon points={"50 0, 0 0, 25 40"} fill="#000" /> <polygon points={"50 0, 0 0, 25 40"} fill={color} />
</svg> </svg>
) )
} }

@ -1,5 +1,6 @@
import { import {
CSSProperties, CSSProperties,
MouseEvent as ReactMouseEvent,
ReactElement, ReactElement,
RefObject, RefObject,
useCallback, useCallback,
@ -7,21 +8,21 @@ import {
useLayoutEffect, useLayoutEffect,
useRef, useRef,
useState, useState,
MouseEvent as ReactMouseEvent,
} from "react" } from "react"
import { import {
add, add,
angle, angle,
middle,
distance, distance,
middle,
middlePos, middlePos,
minus, minus,
mul, mul,
norm,
NULL_POS,
Pos, Pos,
posWithinBase, posWithinBase,
ratioWithinBase, ratioWithinBase,
relativeTo, relativeTo,
norm,
} from "../../geo/Pos" } from "../../geo/Pos"
import "../../style/bendable_arrows.css" import "../../style/bendable_arrows.css"
@ -46,12 +47,14 @@ export interface BendableArrowProps {
export interface ArrowStyle { export interface ArrowStyle {
width?: number width?: number
dashArray?: string dashArray?: string
color: string,
head?: () => ReactElement head?: () => ReactElement
tail?: () => ReactElement tail?: () => ReactElement
} }
const ArrowStyleDefaults: ArrowStyle = { const ArrowStyleDefaults: ArrowStyle = {
width: 3, width: 3,
color: "black"
} }
export interface Segment { export interface Segment {
@ -134,7 +137,7 @@ export default function BendableArrow({
} }
}) })
}, },
[segments, startPos], [startPos],
) )
// Cache the segments so that when the user is changing the segments (it moves an ArrowPoint), // Cache the segments so that when the user is changing the segments (it moves an ArrowPoint),
@ -147,7 +150,7 @@ export default function BendableArrow({
// If the (original) segments changes, overwrite the current ones. // If the (original) segments changes, overwrite the current ones.
useLayoutEffect(() => { useLayoutEffect(() => {
setInternalSegments(computeInternalSegments(segments)) setInternalSegments(computeInternalSegments(segments))
}, [computeInternalSegments]) }, [computeInternalSegments, segments])
const [isSelected, setIsSelected] = useState(false) const [isSelected, setIsSelected] = useState(false)
@ -245,6 +248,8 @@ export default function BendableArrow({
* Updates the states based on given parameters, which causes the arrow to re-render. * Updates the states based on given parameters, which causes the arrow to re-render.
*/ */
const update = useCallback(() => { const update = useCallback(() => {
const parentBase = area.current!.getBoundingClientRect() const parentBase = area.current!.getBoundingClientRect()
const segment = internalSegments[0] ?? null const segment = internalSegments[0] ?? null
@ -309,7 +314,7 @@ export default function BendableArrow({
}, },
] ]
: internalSegments : internalSegments
).map(({ start, controlPoint, end }, idx) => { ).map(({start, controlPoint, end}) => {
const svgPosRelativeToBase = {x: left, y: top} const svgPosRelativeToBase = {x: left, y: top}
const nextRelative = relativeTo( const nextRelative = relativeTo(
@ -355,14 +360,14 @@ export default function BendableArrow({
? add(start, previousSegmentCpAndCurrentPosVector) ? add(start, previousSegmentCpAndCurrentPosVector)
: cp : cp
if (wavy) {
return wavyBezier(start, smoothCp, cp, end, 10, 10)
}
if (forceStraight) { if (forceStraight) {
return `L${end.x} ${end.y}` return `L${end.x} ${end.y}`
} }
if (wavy) {
return wavyBezier(start, smoothCp, cp, end, 10, 10)
}
return `C${smoothCp.x} ${smoothCp.y}, ${cp.x} ${cp.y}, ${end.x} ${end.y}` return `C${smoothCp.x} ${smoothCp.y}, ${cp.x} ${cp.y}, ${end.x} ${end.y}`
}) })
.join(" ") .join(" ")
@ -370,14 +375,7 @@ export default function BendableArrow({
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
pathRef.current!.setAttribute("d", d) pathRef.current!.setAttribute("d", d)
Object.assign(svgRef.current!.style, svgStyle) Object.assign(svgRef.current!.style, svgStyle)
}, [ }, [area, internalSegments, startPos, forceStraight, startRadius, endRadius, wavy])
startPos,
internalSegments,
forceStraight,
startRadius,
endRadius,
style,
])
// Will update the arrow when the props change // Will update the arrow when the props change
useEffect(update, [update]) useEffect(update, [update])
@ -396,7 +394,7 @@ export default function BendableArrow({
} }
return () => observer.disconnect() return () => observer.disconnect()
}, [startPos, segments]) }, [startPos, segments, update])
// Adds a selection handler // Adds a selection handler
// Also force an update when the window is resized // Also force an update when the window is resized
@ -515,7 +513,7 @@ export default function BendableArrow({
<path <path
className="arrow-path" className="arrow-path"
ref={pathRef} ref={pathRef}
stroke={"#000"} stroke={style?.color ?? ArrowStyleDefaults.color}
strokeWidth={styleWidth} strokeWidth={styleWidth}
strokeDasharray={ strokeDasharray={
style?.dashArray ?? ArrowStyleDefaults.dashArray style?.dashArray ?? ArrowStyleDefaults.dashArray
@ -556,8 +554,8 @@ function getPosWithinBase(target: Pos | string, area: DOMRect): Pos {
return posWithinBase(target, area) return posWithinBase(target, area)
} }
const targetPos = document.getElementById(target)!.getBoundingClientRect() const targetPos = document.getElementById(target)?.getBoundingClientRect()
return relativeTo(middlePos(targetPos), area) return targetPos ? relativeTo(middlePos(targetPos), area) : NULL_POS
} }
function getRatioWithinBase(target: Pos | string, area: DOMRect): Pos { function getRatioWithinBase(target: Pos | string, area: DOMRect): Pos {
@ -565,8 +563,8 @@ function getRatioWithinBase(target: Pos | string, area: DOMRect): Pos {
return target return target
} }
const targetPos = document.getElementById(target)!.getBoundingClientRect() const targetPos = document.getElementById(target)?.getBoundingClientRect()
return ratioWithinBase(middlePos(targetPos), area) return targetPos ? ratioWithinBase(middlePos(targetPos), area) : NULL_POS
} }
interface ControlPointProps { interface ControlPointProps {

@ -24,6 +24,7 @@ export interface BasketCourtProps {
export interface ActionPreview extends Action { export interface ActionPreview extends Action {
origin: ComponentId origin: ComponentId
isInvalid: boolean
} }
export function BasketCourt({ export function BasketCourt({
@ -36,6 +37,7 @@ export function BasketCourt({
courtImage, courtImage,
courtRef, courtRef,
}: BasketCourtProps) { }: BasketCourtProps) {
return ( return (
<div <div
className="court-container" className="court-container"
@ -51,6 +53,7 @@ export function BasketCourt({
courtRef={courtRef} courtRef={courtRef}
action={previewAction} action={previewAction}
origin={previewAction.origin} origin={previewAction.origin}
isInvalid={previewAction.isInvalid}
//do nothing on interacted, not really possible as it's a preview arrow //do nothing on interacted, not really possible as it's a preview arrow
onActionDeleted={() => {}} onActionDeleted={() => {}}
onActionChanges={() => {}} onActionChanges={() => {}}

@ -1,4 +1,4 @@
import { ReactNode, RefObject, useRef } from "react" import React, {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"
@ -38,13 +38,15 @@ export default function CourtPlayer({
nodeRef={pieceRef} nodeRef={pieceRef}
//The piece is positioned using top/bottom style attributes instead //The piece is positioned using top/bottom style attributes instead
position={NULL_POS} position={NULL_POS}
onStop={() => { onStop={useCallback(() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect() const pieceBounds = pieceRef.current!.getBoundingClientRect()
const parentBounds = courtRef.current!.getBoundingClientRect() const parentBounds = courtRef.current!.getBoundingClientRect()
const pos = ratioWithinBase(pieceBounds, parentBounds) const pos = ratioWithinBase(pieceBounds, parentBounds)
if (pos.x !== x || pos.y != y)
onPositionValidated(pos) onPositionValidated(pos)
}}> }, [courtRef, onPositionValidated, x, y])}>
<div <div
id={playerInfo.id} id={playerInfo.id}
ref={pieceRef} ref={pieceRef}
@ -57,9 +59,9 @@ export default function CourtPlayer({
<div <div
tabIndex={0} tabIndex={0}
className="player-content" className="player-content"
onKeyUp={(e) => { onKeyUp={useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}}> }, [onRemove])}>
<div className="player-actions"> <div className="player-actions">
{availableActions(pieceRef.current!)} {availableActions(pieceRef.current!)}
</div> </div>

@ -1,61 +1,139 @@
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" import {BallState, Player, PlayerPhantom} from "../model/tactic/Player"
import { middlePos, ratioWithinBase } from "../geo/Pos" import {ratioWithinBase} from "../geo/Pos"
import { import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import {overlaps} from "../geo/Box" import {overlaps} from "../geo/Box"
import { Action, ActionKind } from "../model/tactic/Action" import {Action, ActionKind, moves} from "../model/tactic/Action"
import {removeBall, updateComponent} from "./TacticContentDomains" import {removeBall, updateComponent} from "./TacticContentDomains"
import { getOrigin } from "./PlayerDomains" import {areInSamePath, changePlayerBallState, getOrigin, isNextInPath, removePlayer} from "./PlayerDomains"
import {BALL_TYPE} from "../model/tactic/CourtObjects";
// export function refreshAllActions(
// actions: Action[], export function getActionKind(
// components: TacticComponent[], target: TacticComponent | null,
// ) { ballState: BallState,
// return actions.map((action) => ({
// ...action,
// type: getActionKindFrom(action.fromId, action.toId, components),
// }))
// }
export function getActionKindFrom(
originId: ComponentId,
targetId: ComponentId | null,
components: TacticComponent[],
): ActionKind { ): ActionKind {
const origin = components.find((p) => p.id == originId)! switch (ballState) {
const target = components.find((p) => p.id == targetId) case BallState.HOLDS_ORIGIN:
case BallState.HOLDS_BY_PASS:
return target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
case BallState.PASSED_ORIGIN:
case BallState.PASSED:
case BallState.NONE:
return target && target.type != BALL_TYPE
? ActionKind.SCREEN
: ActionKind.MOVE
}
}
let ballState = BallState.NONE export function getActionKindBetween(origin: Player | PlayerPhantom, target: TacticComponent | null, state: BallState): ActionKind {
//remove the target if the target is a phantom that is within the origin's path
if (target != null && target.type == 'phantom' && areInSamePath(origin, target)) {
target = null;
}
if (origin.type == "player" || origin.type == "phantom") { return getActionKind(target, state)
ballState = origin.ballState
} }
let hasTarget = target export function isActionValid(origin: TacticComponent, target: TacticComponent | null, components: TacticComponent[]): boolean {
? target.type != "phantom" || target.originPlayerId != origin.id /// action is valid if the origin is neither a phantom nor a player
: false if (origin.type != "phantom" && origin.type != "player") {
return true
}
return getActionKind(hasTarget, ballState) // action is invalid if the origin already moves (unless the origin holds a ball which will lead to a ball pass)
if (origin.actions.find(a => moves(a.type)) && origin.ballState != BallState.HOLDS_BY_PASS) {
return false
}
//Action is valid if the target is null
if (target == null) {
return true
} }
export function getActionKind( // action is invalid if it targets its own origin
hasTarget: boolean, if (origin.id === target.id) {
ballState: BallState, return false
): ActionKind { }
switch (ballState) {
case BallState.HOLDS: // action is invalid if the target already moves and is not indirectly bound with origin
return hasTarget ? ActionKind.SHOOT : ActionKind.DRIBBLE if (target.actions.find(a => moves(a.type)) && (hasBoundWith(target, origin, components) || hasBoundWith(origin, target, components))) {
case BallState.SHOOTED: return false
return ActionKind.MOVE }
case BallState.NONE:
return hasTarget ? ActionKind.SCREEN : ActionKind.MOVE // Action is invalid if there is already an action between origin and target.
if (origin.actions.find(a => a.target === target?.id) || target?.actions.find(a => a.target === origin.id)) {
return false
}
// Action is invalid if there is already an anterior action within the target's path
if (target.type == "phantom" || target.type == "player") {
// cant have an action with current path
if (areInSamePath(origin, target))
return false;
if (alreadyHasAnAnteriorActionWith(origin, target, components)) {
return false
} }
} }
export function placeArrow( return true
}
function hasBoundWith(origin: TacticComponent, target: TacticComponent, components: TacticComponent[]): boolean {
const toVisit = [origin.id]
const visited: string[] = []
let itemId: string | undefined
while ((itemId = toVisit.pop())) {
if (visited.indexOf(itemId) !== -1)
continue
visited.push(itemId)
const item = components.find(c => c.id === itemId)!
const itemBounds = item.actions.flatMap(a => typeof a.target == "string" ? [a.target] : [])
if (itemBounds.indexOf(target.id) !== -1) {
return true
}
toVisit.push(...itemBounds)
}
return false
}
function alreadyHasAnAnteriorActionWith(origin: Player | PlayerPhantom, target: Player | PlayerPhantom, components: TacticComponent[]): boolean {
const targetOrigin = target.type === "phantom" ? getOrigin(target, components) : target
const targetOriginPath = [targetOrigin.id, ...(targetOrigin.path?.items ?? [])]
const originOrigin = origin.type === "phantom" ? getOrigin(origin, components) : origin
const originOriginPath = [originOrigin.id, ...(originOrigin.path?.items ?? [])]
const targetIdx = targetOriginPath.indexOf(target.id)
for (let i = targetIdx; i < targetOriginPath.length; i++) {
const phantom = components.find(c => c.id === targetOriginPath[i])! as (Player | PlayerPhantom)
if (phantom.actions.find(a => typeof a.target === "string" && (originOriginPath.indexOf(a.target) !== -1))) {
return true;
}
}
const originIdx = originOriginPath.indexOf(origin.id)
for (let i = 0; i <= originIdx; i++) {
const phantom = components.find(c => c.id === originOriginPath[i])! as (Player | PlayerPhantom)
if (phantom.actions.find(a => typeof a.target === "string" && targetOriginPath.indexOf(a.target) > targetIdx)) {
return true;
}
}
return false;
}
export function createAction(
origin: Player | PlayerPhantom, origin: Player | PlayerPhantom,
courtBounds: DOMRect, courtBounds: DOMRect,
arrowHead: DOMRect, arrowHead: DOMRect,
@ -64,9 +142,8 @@ export function placeArrow(
/** /**
* Creates a new phantom component. * Creates a new phantom component.
* Be aware that this function will reassign the `content` parameter. * Be aware that this function will reassign the `content` parameter.
* @param receivesBall
*/ */
function createPhantom(receivesBall: boolean): ComponentId { function createPhantom(originState: BallState): ComponentId {
const {x, y} = ratioWithinBase(arrowHead, courtBounds) const {x, y} = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number let itemIndex: number
@ -99,20 +176,27 @@ export function placeArrow(
content, content,
) )
const ballState = receivesBall let phantomState: BallState
? BallState.HOLDS switch (originState) {
: origin.ballState == BallState.HOLDS case BallState.HOLDS_ORIGIN:
? BallState.HOLDS phantomState = BallState.HOLDS_BY_PASS
: BallState.NONE break
case BallState.PASSED:
case BallState.PASSED_ORIGIN:
phantomState = BallState.NONE
break
default:
phantomState = originState
}
const phantom: PlayerPhantom = { const phantom: PlayerPhantom = {
actions: [],
type: "phantom", type: "phantom",
id: phantomId, id: phantomId,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
originPlayerId: originPlayer.id, originPlayerId: originPlayer.id,
ballState, ballState: phantomState,
actions: [],
} }
content = { content = {
...content, ...content,
@ -140,14 +224,14 @@ export function placeArrow(
const action: Action = { const action: Action = {
target: toId, target: toId,
type: getActionKind(true, origin.ballState), type: getActionKind(component, origin.ballState),
segments: [{ next: component.id }], segments: [{next: toId}],
} }
return { return {
newContent: updateComponent( newContent: updateComponent(
{ {
...origin, ...content.components.find((c) => c.id == origin.id)!,
actions: [...origin.actions, action], actions: [...origin.actions, action],
}, },
content, content,
@ -157,11 +241,11 @@ export function placeArrow(
} }
} }
const phantomId = createPhantom(origin.ballState == BallState.HOLDS) const phantomId = createPhantom(origin.ballState)
const action: Action = { const action: Action = {
target: phantomId, target: phantomId,
type: getActionKind(false, origin.ballState), type: getActionKind(null, origin.ballState),
segments: [{next: phantomId}], segments: [{next: phantomId}],
} }
return { return {
@ -180,7 +264,7 @@ export function removeAllActionsTargeting(
componentId: ComponentId, componentId: ComponentId,
content: TacticContent, content: TacticContent,
): TacticContent { ): TacticContent {
let components = [] const components = []
for (let i = 0; i < content.components.length; i++) { for (let i = 0; i < content.components.length; i++) {
const component = content.components[i] const component = content.components[i]
components.push({ components.push({
@ -194,3 +278,119 @@ export function removeAllActionsTargeting(
components, components,
} }
} }
export function removeAction(origin: TacticComponent, action: Action, actionIdx: number, content: TacticContent): TacticContent {
origin = {
...origin,
actions: origin.actions.toSpliced(actionIdx, 1),
}
content = updateComponent(
origin,
content,
)
if (action.target == null) return content
const target = content.components.find(
(c) => action.target == c.id,
)!
// if the removed action is a shoot, set the origin as holding the ball
if (action.type == ActionKind.SHOOT && (origin.type === "player" || origin.type === "phantom")) {
if (origin.ballState === BallState.PASSED)
content = changePlayerBallState(origin, BallState.HOLDS_BY_PASS, content)
else if (origin.ballState === BallState.PASSED_ORIGIN)
content = changePlayerBallState(origin, BallState.HOLDS_ORIGIN, content)
if (target.type === "player" || target.type === "phantom")
content = changePlayerBallState(target, BallState.NONE, content)
}
if (target.type === "phantom") {
let path = null
if (origin.type === "player") {
path = origin.path
} else if (origin.type === "phantom") {
path = getOrigin(origin, content.components).path
}
if (
path != null &&
path.items.find((c) => c == target.id)
) {
content = removePlayer(target, content)
}
}
return content
}
/**
* Spreads the changes to others actions and components, directly or indirectly bound to the origin, implied by the change of the origin's actual state with
* the given newState.
* @param origin
* @param newState
* @param content
*/
export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent {
if (origin.ballState === newState) {
return content
}
origin = {
...origin,
ballState: newState
}
content = updateComponent(origin, content)
for (let i = 0; i < origin.actions.length; i++) {
const action = origin.actions[i]
if (typeof action.target !== "string") {
continue;
}
const actionTarget = content.components.find(c => action.target === c.id)! as Player | PlayerPhantom;
let targetState: BallState = actionTarget.ballState
let deleteAction = false
if (isNextInPath(origin, actionTarget, content.components)) {
/// If the target is the next phantom from the origin, its state is propagated.
targetState = (newState === BallState.PASSED || newState === BallState.PASSED_ORIGIN) ? BallState.NONE : newState
} else if (newState === BallState.NONE && action.type === ActionKind.SHOOT) {
/// if the new state removes the ball from the player, remove all actions that were meant to shoot the ball
deleteAction = true
targetState = BallState.NONE // then remove the ball for the target as well
} else if ((newState === BallState.HOLDS_BY_PASS || newState === BallState.HOLDS_ORIGIN) && action.type === ActionKind.SCREEN) {
targetState = BallState.HOLDS_BY_PASS
}
if (deleteAction) {
content = removeAction(origin, action, i, content)
origin = content.components.find(c => c.id === origin.id)! as Player | PlayerPhantom
i--; // step back
} else {
// do not change the action type if it is a shoot action
const type = action.type == ActionKind.SHOOT
? ActionKind.SHOOT
: getActionKindBetween(origin, actionTarget, newState)
origin = {
...origin,
actions: origin.actions.toSpliced(i, 1, {
...action,
type
})
}
content = updateComponent(origin, content)
}
content = spreadNewStateFromOriginStateChange(actionTarget, targetState, content)
}
return content
}

@ -1,7 +1,8 @@
import { Player, PlayerPhantom } from "../model/tactic/Player" import {BallState, Player, PlayerPhantom} from "../model/tactic/Player"
import {TacticComponent, TacticContent} from "../model/tactic/Tactic" import {TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {removeComponent, updateComponent} from "./TacticContentDomains" import {removeComponent, updateComponent} from "./TacticContentDomains"
import { removeAllActionsTargeting } from "./ActionsDomains" import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange} from "./ActionsDomains"
import {ActionKind} from "../model/tactic/Action";
export function getOrigin( export function getOrigin(
pathItem: PlayerPhantom, pathItem: PlayerPhantom,
@ -11,6 +12,36 @@ export function getOrigin(
return components.find((c) => c.id == pathItem.originPlayerId)! as Player return components.find((c) => c.id == pathItem.originPlayerId)! as Player
} }
export function areInSamePath(
a: Player | PlayerPhantom,
b: Player | PlayerPhantom,
) {
if (a.type === "phantom" && b.type === "phantom") {
return a.originPlayerId === b.originPlayerId
}
if (a.type === "phantom") {
return b.id === a.originPlayerId
}
if (b.type === "phantom") {
return a.id === b.originPlayerId
}
return false
}
/**
* @param origin
* @param other
* @param components
* @returns true if the `other` player is the phantom next-to the origin's path.
*/
export function isNextInPath(origin: Player | PlayerPhantom, other: Player | PlayerPhantom, components: TacticComponent[]): boolean {
if (origin.type === "player") {
return origin.path?.items[0] === other.id
}
const originPath = getOrigin(origin, components).path!
return originPath.items!.indexOf(origin.id) === originPath.items!.indexOf(other.id) - 1
}
export function removePlayerPath( export function removePlayerPath(
player: Player, player: Player,
content: TacticContent, content: TacticContent,
@ -21,6 +52,7 @@ export function removePlayerPath(
for (const pathElement of player.path.items) { for (const pathElement of player.path.items) {
content = removeComponent(pathElement, content) content = removeComponent(pathElement, content)
content = removeAllActionsTargeting(pathElement, content)
} }
return updateComponent( return updateComponent(
{ {
@ -43,7 +75,17 @@ export function removePlayer(
} }
content = removePlayerPath(player, content) content = removePlayerPath(player, content)
return removeComponent(player.id, content) content = removeComponent(player.id, content)
for (const action of player.actions) {
if (action.type !== ActionKind.SHOOT) {
continue
}
const actionTarget = content.components.find(c => c.id === action.target)! as (Player | PlayerPhantom)
return spreadNewStateFromOriginStateChange(actionTarget, BallState.NONE, content)
}
return content
} }
export function truncatePlayerPath( export function truncatePlayerPath(
@ -55,16 +97,14 @@ export function truncatePlayerPath(
const path = player.path! const path = player.path!
let truncateStartIdx = -1 const truncateStartIdx = path.items.indexOf(phantom.id)
for (let i = 0; i < path.items.length; i++) { for (let i = truncateStartIdx; i < path.items.length; i++) {
const pathPhantomId = path.items[i] const pathPhantomId = path.items[i]
if (truncateStartIdx != -1 || pathPhantomId == phantom.id) {
if (truncateStartIdx == -1) truncateStartIdx = i
//remove the phantom from the tactic //remove the phantom from the tactic
content = removeComponent(pathPhantomId, content) content = removeComponent(pathPhantomId, content)
} content = removeAllActionsTargeting(pathPhantomId, content)
} }
return updateComponent( return updateComponent(
@ -81,3 +121,7 @@ export function truncatePlayerPath(
content, content,
) )
} }
export function changePlayerBallState(player: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent {
return spreadNewStateFromOriginStateChange(player, newState, content)
}

@ -1,24 +1,10 @@
import {Pos, ratioWithinBase} from "../geo/Pos" import {Pos, ratioWithinBase} from "../geo/Pos"
import { import {BallState, Player, PlayerInfo, PlayerTeam,} from "../model/tactic/Player"
BallState, import {Ball, BALL_ID, BALL_TYPE, CourtObject,} from "../model/tactic/CourtObjects"
Player, import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
PlayerInfo,
PlayerTeam,
} from "../model/tactic/Player"
import {
Ball,
BALL_ID,
BALL_TYPE,
CourtObject,
} from "../model/tactic/CourtObjects"
import {
ComponentId,
TacticComponent,
TacticContent,
} 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 { getOrigin } from "./PlayerDomains" import {changePlayerBallState} from "./PlayerDomains"
export function placePlayerAt( export function placePlayerAt(
refBounds: DOMRect, refBounds: DOMRect,
@ -58,7 +44,7 @@ export function placeObjectAt(
BALL_ID, BALL_ID,
) )
if (playerCollidedIdx != -1) { if (playerCollidedIdx != -1) {
return dropBallOnComponent(playerCollidedIdx, content) return dropBallOnComponent(playerCollidedIdx, content, true)
} }
courtObject = { courtObject = {
@ -83,77 +69,31 @@ export function placeObjectAt(
export function dropBallOnComponent( export function dropBallOnComponent(
targetedComponentIdx: number, targetedComponentIdx: number,
content: TacticContent, content: TacticContent,
setAsOrigin: boolean
): TacticContent { ): TacticContent {
let components = content.components const component = content.components[targetedComponentIdx]
let component = components[targetedComponentIdx]
let origin if ((component.type == 'player' || component.type == 'phantom')) {
let isPhantom: boolean const newState = setAsOrigin
? (component.ballState === BallState.PASSED || component.ballState === BallState.PASSED_ORIGIN) ? BallState.PASSED_ORIGIN : BallState.HOLDS_ORIGIN
: BallState.HOLDS_BY_PASS
if (component.type == "phantom") { content = changePlayerBallState(component, newState, content)
isPhantom = true
origin = getOrigin(component, components)
} else if (component.type == "player") {
isPhantom = false
origin = component
} else {
return content
} }
components = components.toSpliced(targetedComponentIdx, 1, { return removeBall(content)
...component,
ballState: BallState.HOLDS,
})
if (origin.path != null) {
const phantoms = origin.path!.items
const headingPhantoms = isPhantom
? phantoms.slice(phantoms.indexOf(component.id))
: phantoms
components = components.map((c) =>
headingPhantoms.indexOf(c.id) != -1
? {
...c,
hasBall: true,
}
: c,
)
}
const ballObj = components.findIndex((p) => p.type == BALL_TYPE)
// Maybe the ball is not present on the court as an object component
// if so, don't bother removing it from the court.
// This can occur if the user drags and drop the ball from a player that already has the ball
// to another component
if (ballObj != -1) {
components.splice(ballObj, 1)
}
return {
...content,
components,
}
} }
export function removeBall(content: TacticContent): TacticContent { export function removeBall(content: TacticContent): TacticContent {
const ballObj = content.components.findIndex((o) => o.type == "ball") const ballObjIdx = content.components.findIndex((o) => o.type == "ball")
const components = content.components.map((c) => if (ballObjIdx == -1) {
c.type == "player" || c.type == "phantom" return content
? {
...c,
hasBall: false,
}
: c,
)
// if the ball is already not on the court, do nothing
if (ballObj != -1) {
components.splice(ballObj, 1)
} }
return { return {
...content, ...content,
components, components: content.components.toSpliced(ballObjIdx, 1),
} }
} }
@ -161,48 +101,24 @@ export function placeBallAt(
refBounds: DOMRect, refBounds: DOMRect,
courtBounds: DOMRect, courtBounds: DOMRect,
content: TacticContent, content: TacticContent,
): { ): TacticContent {
newContent: TacticContent
removed: boolean
} {
if (!overlaps(courtBounds, refBounds)) { if (!overlaps(courtBounds, refBounds)) {
return { newContent: removeBall(content), removed: true } return removeBall(content)
} }
const playerCollidedIdx = getComponentCollided( const playerCollidedIdx = getComponentCollided(
refBounds, refBounds,
content.components, content.components,
BALL_ID, BALL_ID,
) )
if (playerCollidedIdx != -1) { if (playerCollidedIdx != -1) {
return { return dropBallOnComponent(playerCollidedIdx, content, true)
newContent: dropBallOnComponent(playerCollidedIdx, {
...content,
components: content.components.map((c) =>
c.type == "player" || c.type == "phantom"
? {
...c,
hasBall: false,
}
: c,
),
}),
removed: false,
}
} }
const ballIdx = content.components.findIndex((o) => o.type == "ball") const ballIdx = content.components.findIndex((o) => o.type == "ball")
const {x, y} = ratioWithinBase(refBounds, courtBounds) const {x, y} = ratioWithinBase(refBounds, courtBounds)
const components = content.components.map((c) =>
c.type == "player" || c.type == "phantom"
? {
...c,
hasBall: false,
}
: c,
)
const ball: Ball = { const ball: Ball = {
type: BALL_TYPE, type: BALL_TYPE,
id: BALL_ID, id: BALL_ID,
@ -210,18 +126,18 @@ export function placeBallAt(
bottomRatio: y, bottomRatio: y,
actions: [], actions: [],
} }
let components = content.components
if (ballIdx != -1) { if (ballIdx != -1) {
components.splice(ballIdx, 1, ball) components = components.toSpliced(ballIdx, 1, ball)
} else { } else {
components.push(ball) components = components.concat(ball)
} }
return { return {
newContent: {
...content, ...content,
components, components,
},
removed: false,
} }
} }

@ -12,7 +12,10 @@ export enum ActionKind {
export type Action = { type: ActionKind } & MovementAction export type Action = { type: ActionKind } & MovementAction
export interface MovementAction { export interface MovementAction {
// fromId: ComponentId
target: ComponentId | Pos target: ComponentId | Pos
segments: Segment[] segments: Segment[]
} }
export function moves(kind: ActionKind): boolean {
return kind != ActionKind.SHOOT
}

@ -44,8 +44,10 @@ export interface PlayerInfo {
export enum BallState { export enum BallState {
NONE, NONE,
HOLDS, HOLDS_ORIGIN,
SHOOTED, HOLDS_BY_PASS,
PASSED,
PASSED_ORIGIN,
} }
export interface Player extends Component<"player">, PlayerInfo { export interface Player extends Component<"player">, PlayerInfo {

@ -1,8 +1,10 @@
import { import {
CSSProperties, CSSProperties,
Dispatch, Dispatch,
RefObject,
SetStateAction, SetStateAction,
useCallback, useCallback,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -20,10 +22,7 @@ import { PlayerPiece } from "../components/editor/PlayerPiece"
import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic" import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {fetchAPI} from "../Fetcher" import {fetchAPI} 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 "./editor/CourtAction" import {CourtAction} from "./editor/CourtAction"
@ -40,25 +39,15 @@ import {
removeBall, removeBall,
updateComponent, updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
import { import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player"
BallState, import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems"
Player,
PlayerInfo,
PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer" import CourtPlayer from "../components/editor/CourtPlayer"
import { getActionKind, placeArrow } from "../editor/ActionsDomains" import {createAction, getActionKind, isActionValid, removeAction} from "../editor/ActionsDomains"
import ArrowAction from "../components/actions/ArrowAction" import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, 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 {changePlayerBallState, getOrigin, removePlayer,} from "../editor/PlayerDomains"
getOrigin,
removePlayer,
truncatePlayerPath,
} from "../editor/PlayerDomains"
import {CourtBall} from "../components/editor/CourtBall" import {CourtBall} from "../components/editor/CourtBall"
import {BASE} from "../Constants" import {BASE} from "../Constants"
@ -133,6 +122,7 @@ function EditorView({
onNameChange, onNameChange,
courtType, courtType,
}: EditorViewProps) { }: EditorViewProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -169,8 +159,6 @@ function EditorView({
const courtRef = useRef<HTMLDivElement>(null) const courtRef = useRef<HTMLDivElement>(null)
const actionsReRenderHooks = []
const setComponents = (action: SetStateAction<TacticComponent[]>) => { const setComponents = (action: SetStateAction<TacticComponent[]>) => {
setContent((c) => ({ setContent((c) => ({
...c, ...c,
@ -179,6 +167,12 @@ function EditorView({
})) }))
} }
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
useEffect(() => {
setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}])
}, [setObjects, content]);
const insertRackedPlayer = (player: Player) => { const insertRackedPlayer = (player: Player) => {
let setter let setter
switch (player.team) { switch (player.team) {
@ -188,7 +182,7 @@ function EditorView({
case PlayerTeam.Allies: case PlayerTeam.Allies:
setter = setAllies setter = setAllies
} }
if (player.ballState == BallState.HOLDS) { if (player.ballState == BallState.HOLDS_BY_PASS) {
setObjects([{key: "ball"}]) setObjects([{key: "ball"}])
} }
setter((players) => [ setter((players) => [
@ -201,47 +195,97 @@ function EditorView({
]) ])
} }
const doMoveBall = (newBounds: DOMRect) => { const doRemovePlayer = useCallback((component: Player | PlayerPhantom) => {
setContent((c) => removePlayer(component, c))
if (component.type == "player") insertRackedPlayer(component)
}, [setContent])
const doMoveBall = useCallback((newBounds: DOMRect, from?: Player | PlayerPhantom) => {
setContent((content) => { setContent((content) => {
const { newContent, removed } = placeBallAt( if (from) {
content = changePlayerBallState(from, BallState.NONE, content)
}
content = placeBallAt(
newBounds, newBounds,
courtBounds(), courtBounds(),
content, content,
) )
if (removed) { return content
setObjects((objects) => [...objects, { key: "ball" }])
}
return newContent
}) })
} }, [courtBounds, setContent])
const courtBounds = () => courtRef.current!.getBoundingClientRect() const validatePlayerPosition = useCallback((player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => {
setContent((content) =>
moveComponent(
newPos,
player,
info,
courtBounds(),
content,
const renderPlayer = (component: Player | PlayerPhantom) => { (content) => {
let info: PlayerInfo if (player.type == "player") insertRackedPlayer(player)
return removePlayer(player, content)
},
),
)
}, [courtBounds, setContent])
const renderAvailablePlayerActions = useCallback((info: PlayerInfo, player: Player | PlayerPhantom) => {
let canPlaceArrows: boolean let canPlaceArrows: boolean
const isPhantom = component.type == "phantom"
if (isPhantom) { if (player.type == "player") {
const origin = getOrigin(component, content.components) canPlaceArrows =
player.path == null ||
player.actions.findIndex(
(p) => p.type != ActionKind.SHOOT,
) == -1
} else {
const origin = getOrigin(player, content.components)
const path = origin.path! const path = origin.path!
// phantoms can only place other arrows if they are the head of the path // phantoms can only place other arrows if they are the head of the path
canPlaceArrows = canPlaceArrows =
path.items.indexOf(component.id) == path.items.length - 1 path.items.indexOf(player.id) == path.items.length - 1
if (canPlaceArrows) { if (canPlaceArrows) {
// and if their only action is to shoot the ball // and if their only action is to shoot the ball
const phantomActions = player.actions
// list the actions the phantoms does
const phantomActions = component.actions
canPlaceArrows = canPlaceArrows =
phantomActions.length == 0 || phantomActions.length == 0 ||
phantomActions.findIndex( phantomActions.findIndex(
(c) => c.type != ActionKind.SHOOT, (c) => c.type != ActionKind.SHOOT,
) == -1 ) == -1
} }
}
return [
canPlaceArrows && (
<CourtPlayerArrowAction
key={1}
player={player}
isInvalid={previewAction?.isInvalid ?? false}
setPreviewAction={setPreviewAction}
playerInfo={info}
content={content}
courtRef={courtRef}
setContent={setContent}
/>
),
(info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction key={2} onDrop={(ballBounds) => {
doMoveBall(ballBounds, player)
}}/>
),
]
}, [content, doMoveBall, previewAction?.isInvalid, setContent])
const renderPlayer = useCallback((component: Player | PlayerPhantom) => {
let info: PlayerInfo
const isPhantom = component.type == "phantom"
if (isPhantom) {
const origin = getOrigin(component, content.components)
info = { info = {
id: component.id, id: component.id,
team: origin.team, team: origin.team,
@ -251,14 +295,7 @@ function EditorView({
ballState: component.ballState, ballState: component.ballState,
} }
} else { } else {
// a player
info = component info = component
// can place arrows only if the
canPlaceArrows =
component.path == null ||
component.actions.findIndex(
(p) => p.type != ActionKind.SHOOT,
) == -1
} }
return ( return (
@ -266,165 +303,87 @@ function EditorView({
key={component.id} key={component.id}
className={isPhantom ? "phantom" : "player"} className={isPhantom ? "phantom" : "player"}
playerInfo={info} playerInfo={info}
onPositionValidated={(newPos) => { onPositionValidated={(newPos) => validatePlayerPosition(component, info, newPos)}
setContent((content) => onRemove={() => doRemovePlayer(component)}
moveComponent(
newPos,
component,
info,
courtBounds(),
content,
(content) => {
if (!isPhantom) insertRackedPlayer(component)
return removePlayer(component, content)
},
),
)
}}
onRemove={() => {
setContent((c) => removePlayer(component, c))
if (!isPhantom) insertRackedPlayer(component)
}}
courtRef={courtRef} courtRef={courtRef}
availableActions={() => [ availableActions={() => renderAvailablePlayerActions(info, component)}
canPlaceArrows && ( />
<ArrowAction
key={1}
onHeadMoved={(headPos) => {
const arrowHeadPos = middlePos(headPos)
const targetIdx = getComponentCollided(
headPos,
content.components,
) )
}, [content.components, doRemovePlayer, renderAvailablePlayerActions, validatePlayerPosition])
setPreviewAction((action) => ({ const doDeleteAction = useCallback((
...action!, action: Action,
segments: [ idx: number,
{ origin: TacticComponent,
next: ratioWithinBase( ) => {
arrowHeadPos, setContent((content) => removeAction(origin, action, idx, content))
courtBounds(), }, [setContent])
),
},
],
type: getActionKind(
targetIdx != -1,
info.ballState,
),
}))
}}
onHeadPicked={(headPos) => {
;(document.activeElement as HTMLElement).blur()
setPreviewAction({ const doUpdateAction = useCallback((component: TacticComponent, action: Action, actionIndex: number) => {
origin: component.id, setContent((content) =>
type: getActionKind(false, info.ballState), updateComponent(
target: ratioWithinBase(
headPos,
courtBounds(),
),
segments: [
{ {
next: ratioWithinBase( ...component,
middlePos(headPos), actions:
courtBounds(), component.actions.toSpliced(
actionIndex,
1,
action,
), ),
}, },
],
})
}}
onHeadDropped={(headRect) => {
setContent((content) => {
let { createdAction, newContent } =
placeArrow(
component,
courtBounds(),
headRect,
content, content,
),
) )
}, [setContent])
let originNewBallState = component.ballState const renderComponent = useCallback((component: TacticComponent) => {
if ( if (
createdAction.type == ActionKind.SHOOT component.type == "player" ||
component.type == "phantom"
) { ) {
const targetIdx = return renderPlayer(component)
newContent.components.findIndex(
(c) =>
c.id ==
createdAction.target,
)
newContent = dropBallOnComponent(
targetIdx,
newContent,
)
originNewBallState = BallState.SHOOTED
} }
if (component.type == BALL_TYPE) {
newContent = updateComponent( return (
{ <CourtBall
...(newContent.components.find( key="ball"
(c) => c.id == component.id, ball={component}
)! as Player | PlayerPhantom), onPosValidated={doMoveBall}
ballState: originNewBallState, onRemove={() => {
}, setContent((content) =>
newContent, removeBall(content),
) )
return newContent setObjects((objects) => [
}) ...objects,
setPreviewAction(null) {key: "ball"},
])
}} }}
/> />
),
info.ballState != BallState.NONE && (
<BallAction key={2} onDrop={doMoveBall} />
),
]}
/>
) )
} }
throw new Error(
const doDeleteAction = ( "unknown tactic component " + component,
action: Action,
idx: number,
component: TacticComponent,
) => {
setContent((content) => {
content = updateComponent(
{
...component,
actions: component.actions.toSpliced(idx, 1),
},
content,
) )
}, [renderPlayer, doMoveBall, setContent])
if (action.target == null) return content const renderActions = useCallback((component: TacticComponent) =>
component.actions.map((action, i) => {
const target = content.components.find( return (
(c) => action.target == c.id, <CourtAction
)! key={"action-" + component.id + "-" + i}
action={action}
if (target.type == "phantom") { origin={component.id}
let path = null courtRef={courtRef}
if (component.type == "player") { isInvalid={false}
path = component.path onActionDeleted={() => {
} else if (component.type == "phantom") { doDeleteAction(action, i, component)
path = getOrigin(component, content.components).path }}
} onActionChanges={(action) =>
doUpdateAction(component, action, i)
if (
path == null ||
path.items.find((c) => c == target.id) == null
) {
return content
}
content = removePlayer(target, content)
}
return content
})
} }
/>
)
}), [doDeleteAction, doUpdateAction])
return ( return (
<div id="main-div"> <div id="main-div">
@ -439,52 +398,28 @@ function EditorView({
<TitleInput <TitleInput
style={titleStyle} style={titleStyle}
default_value={name} default_value={name}
on_validated={(new_name) => { onValidated={useCallback((new_name) => {
onNameChange(new_name).then((success) => { onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE) setTitleStyle(success ? {} : ERROR_STYLE)
}) })
}} }, [onNameChange])}
/> />
</div> </div>
<div id="topbar-right"/> <div id="topbar-right"/>
</div> </div>
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
<Rack <PlayerRack id={"allies"} objects={allies} setObjects={setAllies} setComponents={setComponents}
id="allies-rack" courtRef={courtRef}/>
objects={allies}
onChange={setAllies}
canDetach={(div) =>
overlaps(courtBounds(), div.getBoundingClientRect())
}
onElementDetached={(r, e) =>
setComponents((components) => [
...components,
placePlayerAt(
r.getBoundingClientRect(),
courtBounds(),
e,
),
])
}
render={({ team, key }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
)}
/>
<Rack <Rack
id={"objects"} id={"objects"}
objects={objects} objects={objects}
onChange={setObjects} onChange={setObjects}
canDetach={(div) => canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect()) overlaps(courtBounds(), div.getBoundingClientRect())
} , [courtBounds])}
onElementDetached={(r, e) => onElementDetached={useCallback((r, e: RackedCourtObject) =>
setContent((content) => setContent((content) =>
placeObjectAt( placeObjectAt(
r.getBoundingClientRect(), r.getBoundingClientRect(),
@ -493,18 +428,51 @@ function EditorView({
content, content,
), ),
) )
} , [courtBounds, setContent])}
render={renderCourtObject} render={renderCourtObject}
/> />
<PlayerRack id={"opponents"} objects={opponents} setObjects={setOpponents}
setComponents={setComponents} courtRef={courtRef}/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType}/>}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
renderActions={renderActions}
/>
</div>
</div>
</div>
</div>
)
}
interface PlayerRackProps {
id: string
objects: RackedPlayer[]
setObjects: (state: RackedPlayer[]) => void
setComponents: (f: (components: TacticComponent[]) => TacticComponent[]) => void
courtRef: RefObject<HTMLDivElement>
}
function PlayerRack({id, objects, setObjects, courtRef, setComponents}: PlayerRackProps) {
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
return (
<Rack <Rack
id="opponent-rack" id={id}
objects={opponents} objects={objects}
onChange={setOpponents} onChange={setObjects}
canDetach={(div) => canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect()) overlaps(courtBounds(), div.getBoundingClientRect())
} , [courtBounds])}
onElementDetached={(r, e) => onElementDetached={useCallback((r, e: RackedPlayer) =>
setComponents((components) => [ setComponents((components) => [
...components, ...components,
placePlayerAt( placePlayerAt(
@ -513,87 +481,138 @@ function EditorView({
e, e,
), ),
]) ])
} , [courtBounds, setComponents])}
render={({ team, key }) => ( render={useCallback(({team, key}: { team: PlayerTeam, key: string }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
text={key} text={key}
key={key} key={key}
hasBall={false} hasBall={false}
/> />
)} ), [])}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={(component) => {
if (
component.type == "player" ||
component.type == "phantom"
) {
return renderPlayer(component)
}
if (component.type == BALL_TYPE) {
return (
<CourtBall
key="ball"
ball={component}
onPosValidated={doMoveBall}
onRemove={() => {
setContent((content) =>
removeBall(content),
)
setObjects((objects) => [
...objects,
{ key: "ball" },
])
}}
/> />
) )
} }
throw new Error(
"unknown tactic component " + component, interface CourtPlayerArrowActionProps {
playerInfo: PlayerInfo
player: Player | PlayerPhantom
isInvalid: boolean
content: TacticContent
setContent: (state: SetStateAction<TacticContent>) => void
setPreviewAction: (state: SetStateAction<ActionPreview | null>) => void
courtRef: RefObject<HTMLDivElement>
}
function CourtPlayerArrowAction({
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
return (
<ArrowAction
key={1}
onHeadMoved={(headPos) => {
const arrowHeadPos = middlePos(headPos)
const targetIdx = getComponentCollided(
headPos,
content.components,
) )
const target = content.components[targetIdx]
setPreviewAction((action) => ({
...action!,
segments: [
{
next: ratioWithinBase(
arrowHeadPos,
courtBounds(),
),
},
],
type: getActionKind(
target,
playerInfo.ballState,
),
isInvalid: !overlaps(headPos, courtBounds()) || !isActionValid(player, target, content.components)
}))
}} }}
renderActions={(component) => onHeadPicked={(headPos) => {
component.actions.map((action, i) => ( (document.activeElement as HTMLElement).blur()
<CourtAction
key={"action-" + component.id + "-" + i} setPreviewAction({
action={action} origin: playerInfo.id,
origin={component.id} type: getActionKind(null, playerInfo.ballState),
courtRef={courtRef} target: ratioWithinBase(
onActionDeleted={() => { headPos,
doDeleteAction(action, i, component) courtBounds(),
}} ),
onActionChanges={(a) => segments: [
setContent((content) =>
updateComponent(
{ {
...component, next: ratioWithinBase(
actions: middlePos(headPos),
component.actions.toSpliced( courtBounds(),
i,
1,
a,
), ),
}, },
],
isInvalid: false
})
}}
onHeadDropped={(headRect) => {
if (isInvalid) {
setPreviewAction(null)
return
}
setContent((content) => {
let {createdAction, newContent} =
createAction(
player,
courtBounds(),
headRect,
content, content,
), )
if (
createdAction.type == ActionKind.SHOOT
) {
const targetIdx =
newContent.components.findIndex(
(c) =>
c.id ==
createdAction.target,
)
newContent = dropBallOnComponent(
targetIdx,
newContent,
false
)
newContent = updateComponent(
{
...(newContent.components.find(
(c) => c.id == player.id,
)! as Player | PlayerPhantom),
ballState: BallState.PASSED,
},
newContent,
) )
} }
return newContent
})
setPreviewAction(null)
}}
/> />
))
}
/>
</div>
</div>
</div>
</div>
) )
} }
@ -601,7 +620,7 @@ function isBallOnCourt(content: TacticContent) {
return ( return (
content.components.findIndex( content.components.findIndex(
(c) => (c) =>
(c.type == "player" && c.ballState == BallState.HOLDS) || (c.type == "player" && (c.ballState === BallState.HOLDS_ORIGIN || c.ballState === BallState.HOLDS_BY_PASS)) ||
c.type == BALL_TYPE, c.type == BALL_TYPE,
) != -1 ) != -1
) )

@ -3,7 +3,6 @@ import BendableArrow from "../../components/arrows/BendableArrow"
import {RefObject} from "react" import {RefObject} from "react"
import {MoveToHead, ScreenHead} from "../../components/actions/ArrowAction" import {MoveToHead, ScreenHead} from "../../components/actions/ArrowAction"
import {ComponentId} from "../../model/tactic/Tactic" import {ComponentId} from "../../model/tactic/Tactic"
import { middlePos, Pos, ratioWithinBase } from "../../geo/Pos"
export interface CourtActionProps { export interface CourtActionProps {
origin: ComponentId origin: ComponentId
@ -11,6 +10,7 @@ export interface CourtActionProps {
onActionChanges: (a: Action) => void onActionChanges: (a: Action) => void
onActionDeleted: () => void onActionDeleted: () => void
courtRef: RefObject<HTMLElement> courtRef: RefObject<HTMLElement>
isInvalid: boolean
} }
export function CourtAction({ export function CourtAction({
@ -19,16 +19,20 @@ export function CourtAction({
onActionChanges, onActionChanges,
onActionDeleted, onActionDeleted,
courtRef, courtRef,
isInvalid
}: CourtActionProps) { }: CourtActionProps) {
const color = isInvalid ? "red" : "black"
let head let head
switch (action.type) { switch (action.type) {
case ActionKind.DRIBBLE: case ActionKind.DRIBBLE:
case ActionKind.MOVE: case ActionKind.MOVE:
case ActionKind.SHOOT: case ActionKind.SHOOT:
head = () => <MoveToHead /> head = () => <MoveToHead color={color} />
break break
case ActionKind.SCREEN: case ActionKind.SCREEN:
head = () => <ScreenHead /> head = () => <ScreenHead color={color}/>
break break
} }
@ -56,6 +60,7 @@ export function CourtAction({
style={{ style={{
head, head,
dashArray, dashArray,
color
}} }}
/> />
) )

@ -81,6 +81,4 @@ class AccountGateway {
return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profilePicture"])); return new Account($acc["token"], new User($acc["email"], $acc["username"], $acc["id"], $acc["profilePicture"]));
} }
} }

Loading…
Cancel
Save