fixes and format
continuous-integration/drone/push Build is passing Details

maxime 1 year ago
parent d95e84c413
commit 2c95bf6c99

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

@ -47,14 +47,14 @@ export interface BendableArrowProps {
export interface ArrowStyle {
width?: number
dashArray?: string
color: string,
color: string
head?: () => ReactElement
tail?: () => ReactElement
}
const ArrowStyleDefaults: ArrowStyle = {
width: 3,
color: "black"
color: "black",
}
export interface Segment {
@ -99,20 +99,20 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos {
* @constructor
*/
export default function BendableArrow({
area,
startPos,
area,
startPos,
segments,
onSegmentsChanges,
segments,
onSegmentsChanges,
forceStraight,
wavy,
forceStraight,
wavy,
style,
startRadius = 0,
endRadius = 0,
onDeleteRequested,
}: BendableArrowProps) {
style,
startRadius = 0,
endRadius = 0,
onDeleteRequested,
}: BendableArrowProps) {
const containerRef = useRef<HTMLDivElement>(null)
const svgRef = useRef<SVGSVGElement>(null)
const pathRef = useRef<SVGPathElement>(null)
@ -162,7 +162,7 @@ export default function BendableArrow({
* @param parentBase
*/
function computePoints(parentBase: DOMRect) {
return segments.flatMap(({next, controlPoint}, i) => {
return segments.flatMap(({ next, controlPoint }, i) => {
const prev = i == 0 ? startPos : segments[i - 1].next
const prevRelative = getPosWithinBase(prev, parentBase)
@ -248,8 +248,6 @@ export default function BendableArrow({
* Updates the states based on given parameters, which causes the arrow to re-render.
*/
const update = useCallback(() => {
const parentBase = area.current!.getBoundingClientRect()
const segment = internalSegments[0] ?? null
@ -268,8 +266,8 @@ export default function BendableArrow({
const endPrevious = forceStraight
? startRelative
: lastSegment.controlPoint
? posWithinBase(lastSegment.controlPoint, parentBase)
: getPosWithinBase(lastSegment.start, parentBase)
? posWithinBase(lastSegment.controlPoint, parentBase)
: getPosWithinBase(lastSegment.start, parentBase)
const tailPos = constraintInCircle(
startRelative,
@ -307,15 +305,15 @@ export default function BendableArrow({
const segmentsRelatives = (
forceStraight
? [
{
start: startPos,
controlPoint: undefined,
end: lastSegment.end,
},
]
{
start: startPos,
controlPoint: undefined,
end: lastSegment.end,
},
]
: internalSegments
).map(({start, controlPoint, end}) => {
const svgPosRelativeToBase = {x: left, y: top}
).map(({ start, controlPoint, end }) => {
const svgPosRelativeToBase = { x: left, y: top }
const nextRelative = relativeTo(
getPosWithinBase(end, parentBase),
@ -328,9 +326,9 @@ export default function BendableArrow({
const controlPointRelative =
controlPoint && !forceStraight
? relativeTo(
posWithinBase(controlPoint, parentBase),
svgPosRelativeToBase,
)
posWithinBase(controlPoint, parentBase),
svgPosRelativeToBase,
)
: middle(startRelative, nextRelative)
return {
@ -341,7 +339,7 @@ export default function BendableArrow({
})
const computedSegments = segmentsRelatives
.map(({start, cp, end: e}, idx) => {
.map(({ start, cp, end: e }, idx) => {
let end = e
if (idx == segmentsRelatives.length - 1) {
//if it is the last element
@ -375,14 +373,22 @@ export default function BendableArrow({
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
pathRef.current!.setAttribute("d", d)
Object.assign(svgRef.current!.style, svgStyle)
}, [area, internalSegments, startPos, forceStraight, startRadius, endRadius, wavy])
}, [
area,
internalSegments,
startPos,
forceStraight,
startRadius,
endRadius,
wavy,
])
// Will update the arrow when the props change
useEffect(update, [update])
useEffect(() => {
const observer = new MutationObserver(update)
const config = {attributes: true}
const config = { attributes: true }
if (typeof startPos == "string") {
observer.observe(document.getElementById(startPos)!, config)
}
@ -421,7 +427,7 @@ export default function BendableArrow({
if (forceStraight) return
const parentBase = area.current!.getBoundingClientRect()
const clickAbsolutePos: Pos = {x: e.pageX, y: e.pageY}
const clickAbsolutePos: Pos = { x: e.pageX, y: e.pageY }
const clickPosBaseRatio = ratioWithinBase(
clickAbsolutePos,
parentBase,
@ -448,13 +454,13 @@ export default function BendableArrow({
const smoothCp = beforeSegment
? add(
currentPos,
minus(
currentPos,
beforeSegment.controlPoint ??
middle(beforeSegmentPos, currentPos),
),
)
currentPos,
minus(
currentPos,
beforeSegment.controlPoint ??
middle(beforeSegmentPos, currentPos),
),
)
: segmentCp
const result = searchOnSegment(
@ -502,7 +508,7 @@ export default function BendableArrow({
return (
<div
ref={containerRef}
style={{position: "absolute", top: 0, left: 0}}>
style={{ position: "absolute", top: 0, left: 0 }}>
<svg
ref={svgRef}
style={{
@ -530,14 +536,14 @@ export default function BendableArrow({
<div
className={"arrow-head"}
style={{position: "absolute", transformOrigin: "center"}}
style={{ position: "absolute", transformOrigin: "center" }}
ref={headRef}>
{style?.head?.call(style)}
</div>
<div
className={"arrow-tail"}
style={{position: "absolute", transformOrigin: "center"}}
style={{ position: "absolute", transformOrigin: "center" }}
ref={tailRef}>
{style?.tail?.call(style)}
</div>
@ -611,7 +617,7 @@ function wavyBezier(
const velocity = cubicBeziersDerivative(start, cp1, cp2, end, t)
const velocityLength = norm(velocity)
//rotate the velocity by 90 deg
const projection = {x: velocity.y, y: -velocity.x}
const projection = { x: velocity.y, y: -velocity.x }
return {
x: (projection.x / velocityLength) * amplitude,
@ -633,7 +639,7 @@ function wavyBezier(
// 3 : down to middle
let phase = 0
for (let t = step; t <= 1;) {
for (let t = step; t <= 1; ) {
const pos = cubicBeziers(start, cp1, cp2, end, t)
const amplification = getVerticalAmplification(t)
@ -751,14 +757,14 @@ function searchOnSegment(
* @constructor
*/
function ArrowPoint({
className,
posRatio,
parentBase,
onMoves,
onPosValidated,
onRemove,
radius = 7,
}: ControlPointProps) {
className,
posRatio,
parentBase,
onMoves,
onPosValidated,
onRemove,
radius = 7,
}: ControlPointProps) {
const ref = useRef<HTMLDivElement>(null)
const pos = posWithinBase(posRatio, parentBase)
@ -774,7 +780,7 @@ function ArrowPoint({
const pointPos = middlePos(ref.current!.getBoundingClientRect())
onMoves(ratioWithinBase(pointPos, parentBase))
}}
position={{x: pos.x - radius, y: pos.y - radius}}>
position={{ x: pos.x - radius, y: pos.y - radius }}>
<div
ref={ref}
className={`arrow-point ${className}`}

@ -37,7 +37,6 @@ export function BasketCourt({
courtImage,
courtRef,
}: BasketCourtProps) {
return (
<div
className="court-container"

@ -1,9 +1,9 @@
import React, {ReactNode, RefObject, useCallback, useRef} from "react"
import React, { ReactNode, RefObject, useCallback, useRef } from "react"
import "../../style/player.css"
import Draggable from "react-draggable"
import {PlayerPiece} from "./PlayerPiece"
import {BallState, PlayerInfo} from "../../model/tactic/Player"
import {NULL_POS, Pos, ratioWithinBase} from "../../geo/Pos"
import { PlayerPiece } from "./PlayerPiece"
import { BallState, PlayerInfo } from "../../model/tactic/Player"
import { NULL_POS, Pos, ratioWithinBase } from "../../geo/Pos"
export interface CourtPlayerProps {
playerInfo: PlayerInfo
@ -19,14 +19,14 @@ export interface CourtPlayerProps {
* A player that is placed on the court, which can be selected, and moved in the associated bounds
* */
export default function CourtPlayer({
playerInfo,
className,
playerInfo,
className,
onPositionValidated,
onRemove,
courtRef,
availableActions,
}: CourtPlayerProps) {
onPositionValidated,
onRemove,
courtRef,
availableActions,
}: CourtPlayerProps) {
const usesBall = playerInfo.ballState != BallState.NONE
const x = playerInfo.rightRatio
const y = playerInfo.bottomRatio
@ -44,8 +44,7 @@ export default function CourtPlayer({
const pos = ratioWithinBase(pieceBounds, parentBounds)
if (pos.x !== x || pos.y != y)
onPositionValidated(pos)
if (pos.x !== x || pos.y != y) onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}>
<div
id={playerInfo.id}
@ -59,9 +58,12 @@ export default function CourtPlayer({
<div
tabIndex={0}
className="player-content"
onKeyUp={useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
}, [onRemove])}>
onKeyUp={useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
},
[onRemove],
)}>
<div className="player-actions">
{availableActions(pieceRef.current!)}
</div>

@ -1,11 +1,21 @@
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player"
import {ratioWithinBase} from "../geo/Pos"
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
import {overlaps} from "../geo/Box"
import {Action, ActionKind, moves} from "../model/tactic/Action"
import {removeBall, updateComponent} from "./TacticContentDomains"
import {areInSamePath, changePlayerBallState, getOrigin, isNextInPath, removePlayer} from "./PlayerDomains"
import {BALL_TYPE} from "../model/tactic/CourtObjects";
import { BallState, Player, PlayerPhantom } from "../model/tactic/Player"
import { ratioWithinBase } from "../geo/Pos"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains"
import {
areInSamePath,
changePlayerBallState,
getOrigin,
isNextInPath,
removePlayer,
} from "./PlayerDomains"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
export function getActionKind(
target: TacticComponent | null,
@ -14,9 +24,7 @@ export function getActionKind(
switch (ballState) {
case BallState.HOLDS_ORIGIN:
case BallState.HOLDS_BY_PASS:
return target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
return target ? ActionKind.SHOOT : ActionKind.DRIBBLE
case BallState.PASSED_ORIGIN:
case BallState.PASSED:
case BallState.NONE:
@ -26,23 +34,38 @@ export function getActionKind(
}
}
export function getActionKindBetween(origin: Player | PlayerPhantom, target: TacticComponent | null, state: BallState): ActionKind {
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 (
target != null &&
target.type == "phantom" &&
areInSamePath(origin, target)
) {
target = null
}
return getActionKind(target, state)
}
export function isActionValid(origin: TacticComponent, target: TacticComponent | null, components: TacticComponent[]): boolean {
export function isActionValid(
origin: TacticComponent,
target: TacticComponent | null,
components: TacticComponent[],
): boolean {
/// action is valid if the origin is neither a phantom nor a player
if (origin.type != "phantom" && origin.type != "player") {
return true
}
// 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) {
if (
origin.actions.find((a) => moves(a.type)) &&
origin.ballState != BallState.HOLDS_BY_PASS
) {
return false
}
//Action is valid if the target is null
@ -56,23 +79,26 @@ export function isActionValid(origin: TacticComponent, target: TacticComponent |
}
// action is invalid if the target already moves and is not indirectly bound with origin
if (target.actions.find(a => moves(a.type)) && (hasBoundWith(target, origin, components) || hasBoundWith(origin, target, components))) {
if (
target.actions.find((a) => moves(a.type)) &&
(hasBoundWith(target, origin, components) ||
hasBoundWith(origin, target, components))
) {
return false
}
// 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)) {
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 (areInSamePath(origin, target)) return false
if (alreadyHasAnAnteriorActionWith(origin, target, components)) {
return false
@ -82,21 +108,25 @@ export function isActionValid(origin: TacticComponent, target: TacticComponent |
return true
}
function hasBoundWith(origin: TacticComponent, target: TacticComponent, components: TacticComponent[]): boolean {
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
if (visited.indexOf(itemId) !== -1) continue
visited.push(itemId)
const item = components.find(c => c.id === itemId)!
const item = components.find((c) => c.id === itemId)!
const itemBounds = item.actions.flatMap(a => typeof a.target == "string" ? [a.target] : [])
const itemBounds = item.actions.flatMap((a) =>
typeof a.target == "string" ? [a.target] : [],
)
if (itemBounds.indexOf(target.id) !== -1) {
return true
}
@ -107,30 +137,58 @@ function hasBoundWith(origin: TacticComponent, target: TacticComponent, componen
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 ?? [])]
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 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;
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;
return false
}
export function createAction(
@ -143,8 +201,8 @@ export function createAction(
* Creates a new phantom component.
* Be aware that this function will reassign the `content` parameter.
*/
function createPhantom(originState: BallState): ComponentId {
const {x, y} = ratioWithinBase(arrowHead, courtBounds)
function createPhantom(forceHasBall: boolean): ComponentId {
const { x, y } = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number
let originPlayer: Player
@ -177,17 +235,19 @@ export function createAction(
)
let phantomState: BallState
switch (originState) {
case BallState.HOLDS_ORIGIN:
phantomState = BallState.HOLDS_BY_PASS
break
case BallState.PASSED:
case BallState.PASSED_ORIGIN:
phantomState = BallState.NONE
break
default:
phantomState = originState
}
if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN
else
switch (origin.ballState) {
case BallState.HOLDS_ORIGIN:
phantomState = BallState.HOLDS_BY_PASS
break
case BallState.PASSED:
case BallState.PASSED_ORIGIN:
phantomState = BallState.NONE
break
default:
phantomState = origin.ballState
}
const phantom: PlayerPhantom = {
type: "phantom",
@ -225,7 +285,7 @@ export function createAction(
const action: Action = {
target: toId,
type: getActionKind(component, origin.ballState),
segments: [{next: toId}],
segments: [{ next: toId }],
}
return {
@ -241,12 +301,12 @@ export function createAction(
}
}
const phantomId = createPhantom(origin.ballState)
const phantomId = createPhantom(false)
const action: Action = {
target: phantomId,
type: getActionKind(null, origin.ballState),
segments: [{next: phantomId}],
segments: [{ next: phantomId }],
}
return {
newContent: updateComponent(
@ -279,29 +339,39 @@ export function removeAllActionsTargeting(
}
}
export function removeAction(origin: TacticComponent, action: Action, actionIdx: number, content: TacticContent): TacticContent {
export function removeAction(
origin: TacticComponent,
action: Action,
actionIdx: number,
content: TacticContent,
): TacticContent {
origin = {
...origin,
actions: origin.actions.toSpliced(actionIdx, 1),
}
content = updateComponent(
origin,
content,
)
content = updateComponent(origin, content)
if (action.target == null) return content
const target = content.components.find(
(c) => action.target == c.id,
)!
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 (
action.type == ActionKind.SHOOT &&
(origin.type === "player" || origin.type === "phantom")
) {
if (origin.ballState === BallState.PASSED)
content = changePlayerBallState(origin, BallState.HOLDS_BY_PASS, content)
content = changePlayerBallState(
origin,
BallState.HOLDS_BY_PASS,
content,
)
else if (origin.ballState === BallState.PASSED_ORIGIN)
content = changePlayerBallState(origin, BallState.HOLDS_ORIGIN, content)
content = changePlayerBallState(
origin,
BallState.HOLDS_ORIGIN,
content,
)
if (target.type === "player" || target.type === "phantom")
content = changePlayerBallState(target, BallState.NONE, content)
@ -315,16 +385,11 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx:
path = getOrigin(origin, content.components).path
}
if (
path != null &&
path.items.find((c) => c == target.id)
) {
if (path != null && path.items.find((c) => c == target.id)) {
content = removePlayer(target, content)
}
}
return content
}
@ -335,14 +400,18 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx:
* @param newState
* @param content
*/
export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent {
export function spreadNewStateFromOriginStateChange(
origin: Player | PlayerPhantom,
newState: BallState,
content: TacticContent,
): TacticContent {
if (origin.ballState === newState) {
return content
}
origin = {
...origin,
ballState: newState
ballState: newState,
}
content = updateComponent(origin, content)
@ -350,47 +419,72 @@ export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhant
for (let i = 0; i < origin.actions.length; i++) {
const action = origin.actions[i]
if (typeof action.target !== "string") {
continue;
continue
}
const actionTarget = content.components.find(c => action.target === c.id)! as Player | PlayerPhantom;
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) {
switch (newState) {
case BallState.PASSED:
case BallState.PASSED_ORIGIN:
targetState = BallState.NONE
break
case BallState.HOLDS_ORIGIN:
targetState = BallState.HOLDS_BY_PASS
break
default:
targetState = 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) {
} 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
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)
const type =
action.type == ActionKind.SHOOT
? ActionKind.SHOOT
: getActionKindBetween(origin, actionTarget, newState)
origin = {
...origin,
actions: origin.actions.toSpliced(i, 1, {
...action,
type
})
type,
}),
}
content = updateComponent(origin, content)
}
content = spreadNewStateFromOriginStateChange(actionTarget, targetState, content)
content = spreadNewStateFromOriginStateChange(
actionTarget,
targetState,
content,
)
}
return content
}
}

@ -1,8 +1,11 @@
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player"
import {TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {removeComponent, updateComponent} from "./TacticContentDomains"
import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange} from "./ActionsDomains"
import {ActionKind} from "../model/tactic/Action";
import { BallState, Player, PlayerPhantom } from "../model/tactic/Player"
import { TacticComponent, TacticContent } from "../model/tactic/Tactic"
import { removeComponent, updateComponent } from "./TacticContentDomains"
import {
removeAllActionsTargeting,
spreadNewStateFromOriginStateChange,
} from "./ActionsDomains"
import { ActionKind } from "../model/tactic/Action"
export function getOrigin(
pathItem: PlayerPhantom,
@ -34,12 +37,19 @@ export function areInSamePath(
* @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 {
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
return (
originPath.items!.indexOf(origin.id) ===
originPath.items!.indexOf(other.id) - 1
)
}
export function removePlayerPath(
@ -81,8 +91,14 @@ export function removePlayer(
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)
const actionTarget = content.components.find(
(c) => c.id === action.target,
)! as Player | PlayerPhantom
return spreadNewStateFromOriginStateChange(
actionTarget,
BallState.NONE,
content,
)
}
return content
@ -114,14 +130,18 @@ export function truncatePlayerPath(
truncateStartIdx == 0
? null
: {
...path,
items: path.items.toSpliced(truncateStartIdx),
},
...path,
items: path.items.toSpliced(truncateStartIdx),
},
},
content,
)
}
export function changePlayerBallState(player: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent {
export function changePlayerBallState(
player: Player | PlayerPhantom,
newState: BallState,
content: TacticContent,
): TacticContent {
return spreadNewStateFromOriginStateChange(player, newState, content)
}
}

@ -1,17 +1,31 @@
import {Pos, ratioWithinBase} from "../geo/Pos"
import {BallState, Player, 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 {RackedCourtObject, RackedPlayer} from "./RackedItems"
import {changePlayerBallState} from "./PlayerDomains"
import { Pos, ratioWithinBase } from "../geo/Pos"
import {
BallState,
Player,
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 { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState } from "./PlayerDomains"
export function placePlayerAt(
refBounds: DOMRect,
courtBounds: DOMRect,
element: RackedPlayer,
): Player {
const {x, y} = ratioWithinBase(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
return {
type: "player",
@ -32,7 +46,7 @@ export function placeObjectAt(
rackedObject: RackedCourtObject,
content: TacticContent,
): TacticContent {
const {x, y} = ratioWithinBase(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject
@ -69,13 +83,16 @@ export function placeObjectAt(
export function dropBallOnComponent(
targetedComponentIdx: number,
content: TacticContent,
setAsOrigin: boolean
setAsOrigin: boolean,
): TacticContent {
const component = content.components[targetedComponentIdx]
if ((component.type == 'player' || component.type == 'phantom')) {
if (component.type === "player" || component.type === "phantom") {
const newState = setAsOrigin
? (component.ballState === BallState.PASSED || component.ballState === BallState.PASSED_ORIGIN) ? BallState.PASSED_ORIGIN : BallState.HOLDS_ORIGIN
? component.ballState === BallState.PASSED ||
component.ballState === BallState.PASSED_ORIGIN
? BallState.PASSED_ORIGIN
: BallState.HOLDS_ORIGIN
: BallState.HOLDS_BY_PASS
content = changePlayerBallState(component, newState, content)
@ -117,7 +134,7 @@ export function placeBallAt(
const ballIdx = content.components.findIndex((o) => o.type == "ball")
const {x, y} = ratioWithinBase(refBounds, courtBounds)
const { x, y } = ratioWithinBase(refBounds, courtBounds)
const ball: Ball = {
type: BALL_TYPE,
@ -227,5 +244,5 @@ export function getRackPlayers(
c.type == "player" && c.team == team && c.role == role,
) == -1,
)
.map((key) => ({team, key}))
.map((key) => ({ team, key }))
}

@ -18,4 +18,4 @@ export interface MovementAction {
export function moves(kind: ActionKind): boolean {
return kind != ActionKind.SHOOT
}
}

@ -14,20 +14,23 @@ import TitleInput from "../components/TitleInput"
import PlainCourt from "../assets/court/full_court.svg?react"
import HalfCourt from "../assets/court/half_court.svg?react"
import {BallPiece} from "../components/editor/BallPiece"
import { BallPiece } from "../components/editor/BallPiece"
import {Rack} from "../components/Rack"
import {PlayerPiece} from "../components/editor/PlayerPiece"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {fetchAPI} from "../Fetcher"
import { Tactic, TacticComponent, TacticContent } from "../model/tactic/Tactic"
import { fetchAPI } 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 "./editor/CourtAction"
import {ActionPreview, BasketCourt} from "../components/editor/BasketCourt"
import {overlaps} from "../geo/Box"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "./editor/CourtAction"
import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt"
import { overlaps } from "../geo/Box"
import {
dropBallOnComponent,
getComponentCollided,
@ -39,17 +42,32 @@ import {
removeBall,
updateComponent,
} from "../editor/TacticContentDomains"
import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player"
import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems"
import {
BallState,
Player,
PlayerInfo,
PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer"
import {createAction, getActionKind, isActionValid, removeAction} from "../editor/ActionsDomains"
import {
createAction,
getActionKind,
isActionValid,
removeAction,
} from "../editor/ActionsDomains"
import ArrowAction from "../components/actions/ArrowAction"
import {middlePos, Pos, ratioWithinBase} from "../geo/Pos"
import {Action, ActionKind} from "../model/tactic/Action"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction"
import {changePlayerBallState, getOrigin, removePlayer,} from "../editor/PlayerDomains"
import {CourtBall} from "../components/editor/CourtBall"
import {BASE} from "../Constants"
import {
changePlayerBallState,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import { BASE } from "../Constants"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@ -72,7 +90,7 @@ export interface EditorProps {
courtType: "PLAIN" | "HALF"
}
export default function Editor({id, name, courtType, content}: EditorProps) {
export default function Editor({ id, name, courtType, content }: EditorProps) {
const isInGuestMode = id == -1
const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY)
@ -98,7 +116,7 @@ export default function Editor({id, name, courtType, content}: EditorProps) {
)
return SaveStates.Guest
}
return fetchAPI(`tactic/${id}/save`, {content}).then((r) =>
return fetchAPI(`tactic/${id}/save`, { content }).then((r) =>
r.ok ? SaveStates.Ok : SaveStates.Err,
)
}}
@ -107,7 +125,7 @@ export default function Editor({id, name, courtType, content}: EditorProps) {
localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
return true //simulate that the name has been changed
}
return fetchAPI(`tactic/${id}/edit/name`, {name}).then(
return fetchAPI(`tactic/${id}/edit/name`, { name }).then(
(r) => r.ok,
)
}}
@ -117,12 +135,11 @@ export default function Editor({id, name, courtType, content}: EditorProps) {
}
function EditorView({
tactic: {id, name, content: initialContent},
onContentChange,
onNameChange,
courtType,
}: EditorViewProps) {
tactic: { id, name, content: initialContent },
onContentChange,
onNameChange,
courtType,
}: EditorViewProps) {
const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -150,7 +167,7 @@ function EditorView({
)
const [objects, setObjects] = useState<RackedCourtObject[]>(() =>
isBallOnCourt(content) ? [] : [{key: "ball"}],
isBallOnCourt(content) ? [] : [{ key: "ball" }],
)
const [previewAction, setPreviewAction] = useState<ActionPreview | null>(
@ -167,11 +184,14 @@ function EditorView({
}))
}
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
useEffect(() => {
setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}])
}, [setObjects, content]);
setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }])
}, [setObjects, content])
const insertRackedPlayer = (player: Player) => {
let setter
@ -183,7 +203,7 @@ function EditorView({
setter = setAllies
}
if (player.ballState == BallState.HOLDS_BY_PASS) {
setObjects([{key: "ball"}])
setObjects([{ key: "ball" }])
}
setter((players) => [
...players,
@ -195,195 +215,222 @@ function EditorView({
])
}
const doRemovePlayer = useCallback((component: Player | PlayerPhantom) => {
setContent((c) => removePlayer(component, c))
if (component.type == "player") insertRackedPlayer(component)
}, [setContent])
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) => {
if (from) {
content = changePlayerBallState(
from,
BallState.NONE,
content,
)
}
const doMoveBall = useCallback((newBounds: DOMRect, from?: Player | PlayerPhantom) => {
setContent((content) => {
if (from) {
content = changePlayerBallState(from, BallState.NONE, content)
}
content = placeBallAt(newBounds, courtBounds(), content)
content = placeBallAt(
newBounds,
courtBounds(),
content,
return content
})
},
[courtBounds, setContent],
)
const validatePlayerPosition = useCallback(
(player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => {
setContent((content) =>
moveComponent(
newPos,
player,
info,
courtBounds(),
content,
(content) => {
if (player.type == "player") insertRackedPlayer(player)
return removePlayer(player, content)
},
),
)
},
[courtBounds, setContent],
)
return content
})
}, [courtBounds, setContent])
const validatePlayerPosition = useCallback((player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => {
setContent((content) =>
moveComponent(
newPos,
player,
info,
courtBounds(),
content,
(content) => {
if (player.type == "player") insertRackedPlayer(player)
return removePlayer(player, content)
},
),
)
}, [courtBounds, setContent])
const renderAvailablePlayerActions = useCallback((info: PlayerInfo, player: Player | PlayerPhantom) => {
let canPlaceArrows: boolean
if (player.type == "player") {
canPlaceArrows =
player.path == null ||
player.actions.findIndex(
(p) => p.type != ActionKind.SHOOT,
) == -1
} else {
const origin = getOrigin(player, content.components)
const path = origin.path!
// phantoms can only place other arrows if they are the head of the path
canPlaceArrows =
path.items.indexOf(player.id) == path.items.length - 1
if (canPlaceArrows) {
// and if their only action is to shoot the ball
const phantomActions = player.actions
const renderAvailablePlayerActions = useCallback(
(info: PlayerInfo, player: Player | PlayerPhantom) => {
let canPlaceArrows: boolean
if (player.type == "player") {
canPlaceArrows =
phantomActions.length == 0 ||
phantomActions.findIndex(
(c) => c.type != ActionKind.SHOOT,
player.path == null ||
player.actions.findIndex(
(p) => p.type != ActionKind.SHOOT,
) == -1
} else {
const origin = getOrigin(player, content.components)
const path = origin.path!
// phantoms can only place other arrows if they are the head of the path
canPlaceArrows =
path.items.indexOf(player.id) == path.items.length - 1
if (canPlaceArrows) {
// and if their only action is to shoot the ball
const phantomActions = player.actions
canPlaceArrows =
phantomActions.length == 0 ||
phantomActions.findIndex(
(c) => c.type != ActionKind.SHOOT,
) == -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 = {
id: component.id,
team: origin.team,
role: origin.role,
bottomRatio: component.bottomRatio,
rightRatio: component.rightRatio,
ballState: component.ballState,
}
} else {
info = component
}
return [
canPlaceArrows && (
<CourtPlayerArrowAction
key={1}
player={player}
isInvalid={previewAction?.isInvalid ?? false}
setPreviewAction={setPreviewAction}
return (
<CourtPlayer
key={component.id}
className={isPhantom ? "phantom" : "player"}
playerInfo={info}
content={content}
onPositionValidated={(newPos) =>
validatePlayerPosition(component, info, newPos)
}
onRemove={() => doRemovePlayer(component)}
courtRef={courtRef}
setContent={setContent}
availableActions={() =>
renderAvailablePlayerActions(info, component)
}
/>
),
(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 = {
id: component.id,
team: origin.team,
role: origin.role,
bottomRatio: component.bottomRatio,
rightRatio: component.rightRatio,
ballState: component.ballState,
}
} else {
info = component
}
)
},
[
content.components,
doRemovePlayer,
renderAvailablePlayerActions,
validatePlayerPosition,
],
)
return (
<CourtPlayer
key={component.id}
className={isPhantom ? "phantom" : "player"}
playerInfo={info}
onPositionValidated={(newPos) => validatePlayerPosition(component, info, newPos)}
onRemove={() => doRemovePlayer(component)}
courtRef={courtRef}
availableActions={() => renderAvailablePlayerActions(info, component)}
/>
)
}, [content.components, doRemovePlayer, renderAvailablePlayerActions, validatePlayerPosition])
const doDeleteAction = useCallback((
action: Action,
idx: number,
origin: TacticComponent,
) => {
setContent((content) => removeAction(origin, action, idx, content))
}, [setContent])
const doUpdateAction = useCallback((component: TacticComponent, action: Action, actionIndex: number) => {
setContent((content) =>
updateComponent(
{
...component,
actions:
component.actions.toSpliced(
const doDeleteAction = useCallback(
(action: Action, idx: number, origin: TacticComponent) => {
setContent((content) => removeAction(origin, action, idx, content))
},
[setContent],
)
const doUpdateAction = useCallback(
(component: TacticComponent, action: Action, actionIndex: number) => {
setContent((content) =>
updateComponent(
{
...component,
actions: component.actions.toSpliced(
actionIndex,
1,
action,
),
},
content,
),
)
}, [setContent])
const renderComponent = useCallback((component: TacticComponent) => {
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"},
])
}}
/>
},
content,
),
)
}
throw new Error(
"unknown tactic component " + component,
)
}, [renderPlayer, doMoveBall, setContent])
},
[setContent],
)
const renderActions = useCallback((component: TacticComponent) =>
component.actions.map((action, i) => {
return (
<CourtAction
key={"action-" + component.id + "-" + i}
action={action}
origin={component.id}
courtRef={courtRef}
isInvalid={false}
onActionDeleted={() => {
doDeleteAction(action, i, component)
}}
onActionChanges={(action) =>
doUpdateAction(component, action, i)
}
/>
)
}), [doDeleteAction, doUpdateAction])
const renderComponent = useCallback(
(component: TacticComponent) => {
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)
},
[renderPlayer, doMoveBall, setContent],
)
const renderActions = useCallback(
(component: TacticComponent) =>
component.actions.map((action, i) => {
return (
<CourtAction
key={"action-" + component.id + "-" + i}
action={action}
origin={component.id}
courtRef={courtRef}
isInvalid={false}
onActionDeleted={() => {
doDeleteAction(action, i, component)
}}
onActionChanges={(action) =>
doUpdateAction(component, action, i)
}
/>
)
}),
[doDeleteAction, doUpdateAction],
)
return (
<div id="main-div">
@ -392,34 +439,48 @@ function EditorView({
Home
</button>
<div id="topbar-left">
<SavingState state={saveState}/>
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback((new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
}, [onNameChange])}
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right"/>
<div id="topbar-right" />
</div>
<div id="edit-div">
<div id="racks">
<PlayerRack id={"allies"} objects={allies} setObjects={setAllies} setComponents={setComponents}
courtRef={courtRef}/>
<PlayerRack
id={"allies"}
objects={allies}
setObjects={setAllies}
setComponents={setComponents}
courtRef={courtRef}
/>
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect())
, [courtBounds])}
onElementDetached={useCallback((r, e: RackedCourtObject) =>
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
@ -427,19 +488,25 @@ function EditorView({
e,
content,
),
)
, [courtBounds, setContent])}
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack id={"opponents"} objects={opponents} setObjects={setOpponents}
setComponents={setComponents} courtRef={courtRef}/>
<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}/>}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
@ -456,23 +523,35 @@ interface PlayerRackProps {
id: string
objects: RackedPlayer[]
setObjects: (state: RackedPlayer[]) => void
setComponents: (f: (components: TacticComponent[]) => TacticComponent[]) => 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])
function PlayerRack({
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
return (
<Rack
id={id}
objects={objects}
onChange={setObjects}
canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect())
, [courtBounds])}
onElementDetached={useCallback((r, e: RackedPlayer) =>
canDetach={useCallback(
(div) => overlaps(courtBounds(), div.getBoundingClientRect()),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedPlayer) =>
setComponents((components) => [
...components,
placePlayerAt(
@ -480,16 +559,20 @@ function PlayerRack({id, objects, setObjects, courtRef, setComponents}: PlayerRa
courtBounds(),
e,
),
])
, [courtBounds, setComponents])}
render={useCallback(({team, key}: { team: PlayerTeam, key: string }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
), [])}
]),
[courtBounds, setComponents],
)}
render={useCallback(
({ team, key }: { team: PlayerTeam; key: string }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
),
[],
)}
/>
)
}
@ -506,17 +589,19 @@ interface CourtPlayerArrowActionProps {
}
function CourtPlayerArrowAction({
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
return (
<ArrowAction
@ -533,29 +618,22 @@ function CourtPlayerArrowAction({
...action!,
segments: [
{
next: ratioWithinBase(
arrowHeadPos,
courtBounds(),
),
next: ratioWithinBase(arrowHeadPos, courtBounds()),
},
],
type: getActionKind(
target,
playerInfo.ballState,
),
isInvalid: !overlaps(headPos, courtBounds()) || !isActionValid(player, target, content.components)
type: getActionKind(target, playerInfo.ballState),
isInvalid:
!overlaps(headPos, courtBounds()) ||
!isActionValid(player, target, content.components),
}))
}}
onHeadPicked={(headPos) => {
(document.activeElement as HTMLElement).blur()
;(document.activeElement as HTMLElement).blur()
setPreviewAction({
origin: playerInfo.id,
type: getActionKind(null, playerInfo.ballState),
target: ratioWithinBase(
headPos,
courtBounds(),
),
target: ratioWithinBase(headPos, courtBounds()),
segments: [
{
next: ratioWithinBase(
@ -564,7 +642,7 @@ function CourtPlayerArrowAction({
),
},
],
isInvalid: false
isInvalid: false,
})
}}
onHeadDropped={(headRect) => {
@ -574,27 +652,21 @@ function CourtPlayerArrowAction({
}
setContent((content) => {
let {createdAction, newContent} =
createAction(
player,
courtBounds(),
headRect,
content,
)
let { createdAction, newContent } = createAction(
player,
courtBounds(),
headRect,
content,
)
if (
createdAction.type == ActionKind.SHOOT
) {
const targetIdx =
newContent.components.findIndex(
(c) =>
c.id ==
createdAction.target,
)
if (createdAction.type == ActionKind.SHOOT) {
const targetIdx = newContent.components.findIndex(
(c) => c.id == createdAction.target,
)
newContent = dropBallOnComponent(
targetIdx,
newContent,
false
false,
)
newContent = updateComponent(
{
@ -607,7 +679,6 @@ function CourtPlayerArrowAction({
)
}
return newContent
})
setPreviewAction(null)
@ -620,26 +691,28 @@ function isBallOnCourt(content: TacticContent) {
return (
content.components.findIndex(
(c) =>
(c.type == "player" && (c.ballState === BallState.HOLDS_ORIGIN || c.ballState === BallState.HOLDS_BY_PASS)) ||
c.type == BALL_TYPE,
((c.type === "player" || c.type === "phantom") &&
(c.ballState === BallState.HOLDS_ORIGIN ||
c.ballState === BallState.PASSED_ORIGIN)) ||
c.type === BALL_TYPE,
) != -1
)
}
function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") {
return <BallPiece/>
return <BallPiece />
}
throw new Error("unknown racked court object " + courtObject.key)
}
function Court({courtType}: { courtType: string }) {
function Court({ courtType }: { courtType: string }) {
return (
<div id="court-image-div">
{courtType == "PLAIN" ? (
<PlainCourt id="court-image"/>
<PlainCourt id="court-image" />
) : (
<HalfCourt id="court-image"/>
<HalfCourt id="court-image" />
)}
</div>
)

@ -1,8 +1,8 @@
import {Action, ActionKind} from "../../model/tactic/Action"
import { Action, ActionKind } from "../../model/tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow"
import {RefObject} from "react"
import {MoveToHead, ScreenHead} from "../../components/actions/ArrowAction"
import {ComponentId} from "../../model/tactic/Tactic"
import { RefObject } from "react"
import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction"
import { ComponentId } from "../../model/tactic/Tactic"
export interface CourtActionProps {
origin: ComponentId
@ -19,9 +19,8 @@ export function CourtAction({
onActionChanges,
onActionDeleted,
courtRef,
isInvalid
isInvalid,
}: CourtActionProps) {
const color = isInvalid ? "red" : "black"
let head
@ -32,7 +31,7 @@ export function CourtAction({
head = () => <MoveToHead color={color} />
break
case ActionKind.SCREEN:
head = () => <ScreenHead color={color}/>
head = () => <ScreenHead color={color} />
break
}
@ -60,7 +59,7 @@ export function CourtAction({
style={{
head,
dashArray,
color
color,
}}
/>
)

Loading…
Cancel
Save