fixes and format

pull/95/head
maxime 1 year ago
parent e97821a4fa
commit 15c75ee269

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

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

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

@ -1,11 +1,21 @@
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" import { BallState, Player, PlayerPhantom } from "../model/tactic/Player"
import {ratioWithinBase} from "../geo/Pos" import { ratioWithinBase } from "../geo/Pos"
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" import {
import {overlaps} from "../geo/Box" ComponentId,
import {Action, ActionKind, moves} from "../model/tactic/Action" TacticComponent,
import {removeBall, updateComponent} from "./TacticContentDomains" TacticContent,
import {areInSamePath, changePlayerBallState, getOrigin, isNextInPath, removePlayer} from "./PlayerDomains" } from "../model/tactic/Tactic"
import {BALL_TYPE} from "../model/tactic/CourtObjects"; 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( export function getActionKind(
target: TacticComponent | null, target: TacticComponent | null,
@ -14,9 +24,7 @@ export function getActionKind(
switch (ballState) { switch (ballState) {
case BallState.HOLDS_ORIGIN: case BallState.HOLDS_ORIGIN:
case BallState.HOLDS_BY_PASS: case BallState.HOLDS_BY_PASS:
return target return target ? ActionKind.SHOOT : ActionKind.DRIBBLE
? ActionKind.SHOOT
: ActionKind.DRIBBLE
case BallState.PASSED_ORIGIN: case BallState.PASSED_ORIGIN:
case BallState.PASSED: case BallState.PASSED:
case BallState.NONE: 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 //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)) { if (
target = null; target != null &&
target.type == "phantom" &&
areInSamePath(origin, target)
) {
target = null
} }
return getActionKind(target, state) 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 /// action is valid if the origin is neither a phantom nor a player
if (origin.type != "phantom" && origin.type != "player") { if (origin.type != "phantom" && origin.type != "player") {
return true return true
} }
// action is invalid if the origin already moves (unless the origin holds a ball which will lead to a ball pass) // 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 return false
} }
//Action is valid if the target is null //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 // 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 return false
} }
// Action is invalid if there is already an action between origin and target. // 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 return false
} }
// Action is invalid if there is already an anterior action within the target's path // Action is invalid if there is already an anterior action within the target's path
if (target.type == "phantom" || target.type == "player") { if (target.type == "phantom" || target.type == "player") {
// cant have an action with current path // cant have an action with current path
if (areInSamePath(origin, target)) if (areInSamePath(origin, target)) return false
return false;
if (alreadyHasAnAnteriorActionWith(origin, target, components)) { if (alreadyHasAnAnteriorActionWith(origin, target, components)) {
return false return false
@ -82,21 +108,25 @@ export function isActionValid(origin: TacticComponent, target: TacticComponent |
return true 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 toVisit = [origin.id]
const visited: string[] = [] const visited: string[] = []
let itemId: string | undefined let itemId: string | undefined
while ((itemId = toVisit.pop())) { while ((itemId = toVisit.pop())) {
if (visited.indexOf(itemId) !== -1) continue
if (visited.indexOf(itemId) !== -1)
continue
visited.push(itemId) 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) { if (itemBounds.indexOf(target.id) !== -1) {
return true return true
} }
@ -107,30 +137,58 @@ function hasBoundWith(origin: TacticComponent, target: TacticComponent, componen
return false return false
} }
function alreadyHasAnAnteriorActionWith(origin: Player | PlayerPhantom, target: Player | PlayerPhantom, components: TacticComponent[]): boolean { function alreadyHasAnAnteriorActionWith(
const targetOrigin = target.type === "phantom" ? getOrigin(target, components) : target origin: Player | PlayerPhantom,
const targetOriginPath = [targetOrigin.id, ...(targetOrigin.path?.items ?? [])] target: Player | PlayerPhantom,
components: TacticComponent[],
const originOrigin = origin.type === "phantom" ? getOrigin(origin, components) : origin ): boolean {
const originOriginPath = [originOrigin.id, ...(originOrigin.path?.items ?? [])] 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) const targetIdx = targetOriginPath.indexOf(target.id)
for (let i = targetIdx; i < targetOriginPath.length; i++) { for (let i = targetIdx; i < targetOriginPath.length; i++) {
const phantom = components.find(c => c.id === targetOriginPath[i])! as (Player | PlayerPhantom) const phantom = components.find(
if (phantom.actions.find(a => typeof a.target === "string" && (originOriginPath.indexOf(a.target) !== -1))) { (c) => c.id === targetOriginPath[i],
return true; )! 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) const originIdx = originOriginPath.indexOf(origin.id)
for (let i = 0; i <= originIdx; i++) { for (let i = 0; i <= originIdx; i++) {
const phantom = components.find(c => c.id === originOriginPath[i])! as (Player | PlayerPhantom) const phantom = components.find(
if (phantom.actions.find(a => typeof a.target === "string" && targetOriginPath.indexOf(a.target) > targetIdx)) { (c) => c.id === originOriginPath[i],
return true; )! 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( export function createAction(
@ -143,8 +201,8 @@ export function createAction(
* 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.
*/ */
function createPhantom(originState: BallState): ComponentId { function createPhantom(forceHasBall: boolean): ComponentId {
const {x, y} = ratioWithinBase(arrowHead, courtBounds) const { x, y } = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number let itemIndex: number
let originPlayer: Player let originPlayer: Player
@ -177,17 +235,19 @@ export function createAction(
) )
let phantomState: BallState let phantomState: BallState
switch (originState) { if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN
case BallState.HOLDS_ORIGIN: else
phantomState = BallState.HOLDS_BY_PASS switch (origin.ballState) {
break case BallState.HOLDS_ORIGIN:
case BallState.PASSED: phantomState = BallState.HOLDS_BY_PASS
case BallState.PASSED_ORIGIN: break
phantomState = BallState.NONE case BallState.PASSED:
break case BallState.PASSED_ORIGIN:
default: phantomState = BallState.NONE
phantomState = originState break
} default:
phantomState = origin.ballState
}
const phantom: PlayerPhantom = { const phantom: PlayerPhantom = {
type: "phantom", type: "phantom",
@ -225,7 +285,7 @@ export function createAction(
const action: Action = { const action: Action = {
target: toId, target: toId,
type: getActionKind(component, origin.ballState), type: getActionKind(component, origin.ballState),
segments: [{next: toId}], segments: [{ next: toId }],
} }
return { return {
@ -241,12 +301,12 @@ export function createAction(
} }
} }
const phantomId = createPhantom(origin.ballState) const phantomId = createPhantom(false)
const action: Action = { const action: Action = {
target: phantomId, target: phantomId,
type: getActionKind(null, origin.ballState), type: getActionKind(null, origin.ballState),
segments: [{next: phantomId}], segments: [{ next: phantomId }],
} }
return { return {
newContent: updateComponent( newContent: updateComponent(
@ -279,29 +339,39 @@ export function removeAllActionsTargeting(
} }
} }
export function removeAction(
export function removeAction(origin: TacticComponent, action: Action, actionIdx: number, content: TacticContent): TacticContent { origin: TacticComponent,
action: Action,
actionIdx: number,
content: TacticContent,
): TacticContent {
origin = { origin = {
...origin, ...origin,
actions: origin.actions.toSpliced(actionIdx, 1), actions: origin.actions.toSpliced(actionIdx, 1),
} }
content = updateComponent( content = updateComponent(origin, content)
origin,
content,
)
if (action.target == null) return content if (action.target == null) return content
const target = content.components.find( const target = content.components.find((c) => action.target == c.id)!
(c) => action.target == c.id,
)!
// if the removed action is a shoot, set the origin as holding the ball // 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) 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) 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") if (target.type === "player" || target.type === "phantom")
content = changePlayerBallState(target, BallState.NONE, content) content = changePlayerBallState(target, BallState.NONE, content)
@ -315,16 +385,11 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx:
path = getOrigin(origin, content.components).path path = getOrigin(origin, content.components).path
} }
if ( if (path != null && path.items.find((c) => c == target.id)) {
path != null &&
path.items.find((c) => c == target.id)
) {
content = removePlayer(target, content) content = removePlayer(target, content)
} }
} }
return content return content
} }
@ -335,14 +400,18 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx:
* @param newState * @param newState
* @param content * @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) { if (origin.ballState === newState) {
return content return content
} }
origin = { origin = {
...origin, ...origin,
ballState: newState ballState: newState,
} }
content = updateComponent(origin, content) content = updateComponent(origin, content)
@ -350,47 +419,72 @@ export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhant
for (let i = 0; i < origin.actions.length; i++) { for (let i = 0; i < origin.actions.length; i++) {
const action = origin.actions[i] const action = origin.actions[i]
if (typeof action.target !== "string") { 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 targetState: BallState = actionTarget.ballState
let deleteAction = false let deleteAction = false
if (isNextInPath(origin, actionTarget, content.components)) { if (isNextInPath(origin, actionTarget, content.components)) {
/// If the target is the next phantom from the origin, its state is propagated. switch (newState) {
targetState = (newState === BallState.PASSED || newState === BallState.PASSED_ORIGIN) ? BallState.NONE : newState case BallState.PASSED:
} else if (newState === BallState.NONE && action.type === ActionKind.SHOOT) { 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 /// if the new state removes the ball from the player, remove all actions that were meant to shoot the ball
deleteAction = true deleteAction = true
targetState = BallState.NONE // then remove the ball for the target as well 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 targetState = BallState.HOLDS_BY_PASS
} }
if (deleteAction) { if (deleteAction) {
content = removeAction(origin, action, i, content) content = removeAction(origin, action, i, content)
origin = content.components.find(c => c.id === origin.id)! as Player | PlayerPhantom origin = content.components.find((c) => c.id === origin.id)! as
i--; // step back | Player
| PlayerPhantom
i-- // step back
} else { } else {
// do not change the action type if it is a shoot action // do not change the action type if it is a shoot action
const type = action.type == ActionKind.SHOOT const type =
? ActionKind.SHOOT action.type == ActionKind.SHOOT
: getActionKindBetween(origin, actionTarget, newState) ? ActionKind.SHOOT
: getActionKindBetween(origin, actionTarget, newState)
origin = { origin = {
...origin, ...origin,
actions: origin.actions.toSpliced(i, 1, { actions: origin.actions.toSpliced(i, 1, {
...action, ...action,
type type,
}) }),
} }
content = updateComponent(origin, content) content = updateComponent(origin, content)
} }
content = spreadNewStateFromOriginStateChange(actionTarget, targetState, content) content = spreadNewStateFromOriginStateChange(
actionTarget,
targetState,
content,
)
} }
return content return content
} }

@ -1,8 +1,11 @@
import {BallState, 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, spreadNewStateFromOriginStateChange} from "./ActionsDomains" import {
import {ActionKind} from "../model/tactic/Action"; removeAllActionsTargeting,
spreadNewStateFromOriginStateChange,
} from "./ActionsDomains"
import { ActionKind } from "../model/tactic/Action"
export function getOrigin( export function getOrigin(
pathItem: PlayerPhantom, pathItem: PlayerPhantom,
@ -34,12 +37,19 @@ export function areInSamePath(
* @param components * @param components
* @returns true if the `other` player is the phantom next-to the origin's path. * @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") { if (origin.type === "player") {
return origin.path?.items[0] === other.id return origin.path?.items[0] === other.id
} }
const originPath = getOrigin(origin, components).path! 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( export function removePlayerPath(
@ -81,8 +91,14 @@ export function removePlayer(
if (action.type !== ActionKind.SHOOT) { if (action.type !== ActionKind.SHOOT) {
continue continue
} }
const actionTarget = content.components.find(c => c.id === action.target)! as (Player | PlayerPhantom) const actionTarget = content.components.find(
return spreadNewStateFromOriginStateChange(actionTarget, BallState.NONE, content) (c) => c.id === action.target,
)! as Player | PlayerPhantom
return spreadNewStateFromOriginStateChange(
actionTarget,
BallState.NONE,
content,
)
} }
return content return content
@ -114,14 +130,18 @@ export function truncatePlayerPath(
truncateStartIdx == 0 truncateStartIdx == 0
? null ? null
: { : {
...path, ...path,
items: path.items.toSpliced(truncateStartIdx), items: path.items.toSpliced(truncateStartIdx),
}, },
}, },
content, 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) return spreadNewStateFromOriginStateChange(player, newState, content)
} }

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

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

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

Loading…
Cancel
Save