fixes and format

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

@ -46,9 +46,7 @@ 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" }}
/>
) )
} }

@ -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 {
@ -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
@ -375,7 +373,15 @@ 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])

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

@ -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(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}, [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 {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box" import { overlaps } from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action" import { Action, ActionKind, moves } from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains" import { removeBall, updateComponent } from "./TacticContentDomains"
import {areInSamePath, changePlayerBallState, getOrigin, isNextInPath, removePlayer} from "./PlayerDomains" import {
import {BALL_TYPE} from "../model/tactic/CourtObjects"; 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,7 +201,7 @@ 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
@ -177,7 +235,9 @@ export function createAction(
) )
let phantomState: BallState let phantomState: BallState
switch (originState) { if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN
else
switch (origin.ballState) {
case BallState.HOLDS_ORIGIN: case BallState.HOLDS_ORIGIN:
phantomState = BallState.HOLDS_BY_PASS phantomState = BallState.HOLDS_BY_PASS
break break
@ -186,7 +246,7 @@ export function createAction(
phantomState = BallState.NONE phantomState = BallState.NONE
break break
default: default:
phantomState = originState phantomState = origin.ballState
} }
const phantom: PlayerPhantom = { const phantom: PlayerPhantom = {
@ -241,7 +301,7 @@ export function createAction(
} }
} }
const phantomId = createPhantom(origin.ballState) const phantomId = createPhantom(false)
const action: Action = { const action: Action = {
target: phantomId, target: phantomId,
@ -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,32 +419,53 @@ 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 =
action.type == ActionKind.SHOOT
? ActionKind.SHOOT ? ActionKind.SHOOT
: getActionKindBetween(origin, actionTarget, newState) : getActionKindBetween(origin, actionTarget, newState)
@ -383,13 +473,17 @@ export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhant
...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
@ -122,6 +138,10 @@ export function truncatePlayerPath(
) )
} }
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,7 +1,21 @@
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,
PlayerInfo,
PlayerTeam,
} from "../model/tactic/Player"
import {
Ball,
BALL_ID,
BALL_TYPE,
CourtObject,
} from "../model/tactic/CourtObjects"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box" import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems" import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState } from "./PlayerDomains" import { changePlayerBallState } from "./PlayerDomains"
@ -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)

@ -22,7 +22,10 @@ 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"
@ -39,15 +42,30 @@ import {
removeBall, removeBall,
updateComponent, updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player" import {
BallState,
Player,
PlayerInfo,
PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" 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 {
changePlayerBallState,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall" import { CourtBall } from "../components/editor/CourtBall"
import { BASE } from "../Constants" import { BASE } from "../Constants"
@ -122,7 +140,6 @@ function EditorView({
onNameChange, onNameChange,
courtType, courtType,
}: EditorViewProps) { }: EditorViewProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -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
@ -195,28 +215,35 @@ function EditorView({
]) ])
} }
const doRemovePlayer = useCallback((component: Player | PlayerPhantom) => { const doRemovePlayer = useCallback(
(component: Player | PlayerPhantom) => {
setContent((c) => removePlayer(component, c)) setContent((c) => removePlayer(component, c))
if (component.type == "player") insertRackedPlayer(component) if (component.type == "player") insertRackedPlayer(component)
}, [setContent]) },
[setContent],
)
const doMoveBall = useCallback((newBounds: DOMRect, from?: Player | PlayerPhantom) => { const doMoveBall = useCallback(
(newBounds: DOMRect, from?: Player | PlayerPhantom) => {
setContent((content) => { setContent((content) => {
if (from) { if (from) {
content = changePlayerBallState(from, BallState.NONE, content) content = changePlayerBallState(
} from,
BallState.NONE,
content = placeBallAt(
newBounds,
courtBounds(),
content, content,
) )
}
content = placeBallAt(newBounds, courtBounds(), content)
return content return content
}) })
}, [courtBounds, setContent]) },
[courtBounds, setContent],
)
const validatePlayerPosition = useCallback((player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => { const validatePlayerPosition = useCallback(
(player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => {
setContent((content) => setContent((content) =>
moveComponent( moveComponent(
newPos, newPos,
@ -231,9 +258,12 @@ function EditorView({
}, },
), ),
) )
}, [courtBounds, setContent]) },
[courtBounds, setContent],
)
const renderAvailablePlayerActions = useCallback((info: PlayerInfo, player: Player | PlayerPhantom) => { const renderAvailablePlayerActions = useCallback(
(info: PlayerInfo, player: Player | PlayerPhantom) => {
let canPlaceArrows: boolean let canPlaceArrows: boolean
if (player.type == "player") { if (player.type == "player") {
@ -259,7 +289,6 @@ function EditorView({
} }
} }
return [ return [
canPlaceArrows && ( canPlaceArrows && (
<CourtPlayerArrowAction <CourtPlayerArrowAction
@ -273,15 +302,22 @@ function EditorView({
setContent={setContent} setContent={setContent}
/> />
), ),
(info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && ( (info.ballState === BallState.HOLDS_ORIGIN ||
<BallAction key={2} onDrop={(ballBounds) => { info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction
key={2}
onDrop={(ballBounds) => {
doMoveBall(ballBounds, player) doMoveBall(ballBounds, player)
}}/> }}
/>
), ),
] ]
}, [content, doMoveBall, previewAction?.isInvalid, setContent]) },
[content, doMoveBall, previewAction?.isInvalid, setContent],
)
const renderPlayer = useCallback((component: Player | PlayerPhantom) => { const renderPlayer = useCallback(
(component: Player | PlayerPhantom) => {
let info: PlayerInfo let info: PlayerInfo
const isPhantom = component.type == "phantom" const isPhantom = component.type == "phantom"
if (isPhantom) { if (isPhantom) {
@ -303,29 +339,39 @@ function EditorView({
key={component.id} key={component.id}
className={isPhantom ? "phantom" : "player"} className={isPhantom ? "phantom" : "player"}
playerInfo={info} playerInfo={info}
onPositionValidated={(newPos) => validatePlayerPosition(component, info, newPos)} onPositionValidated={(newPos) =>
validatePlayerPosition(component, info, newPos)
}
onRemove={() => doRemovePlayer(component)} onRemove={() => doRemovePlayer(component)}
courtRef={courtRef} courtRef={courtRef}
availableActions={() => renderAvailablePlayerActions(info, component)} availableActions={() =>
renderAvailablePlayerActions(info, component)
}
/> />
) )
}, [content.components, doRemovePlayer, renderAvailablePlayerActions, validatePlayerPosition]) },
[
content.components,
doRemovePlayer,
renderAvailablePlayerActions,
validatePlayerPosition,
],
)
const doDeleteAction = useCallback(( const doDeleteAction = useCallback(
action: Action, (action: Action, idx: number, origin: TacticComponent) => {
idx: number,
origin: TacticComponent,
) => {
setContent((content) => removeAction(origin, action, idx, content)) setContent((content) => removeAction(origin, action, idx, content))
}, [setContent]) },
[setContent],
)
const doUpdateAction = useCallback((component: TacticComponent, action: Action, actionIndex: number) => { const doUpdateAction = useCallback(
(component: TacticComponent, action: Action, actionIndex: number) => {
setContent((content) => setContent((content) =>
updateComponent( updateComponent(
{ {
...component, ...component,
actions: actions: component.actions.toSpliced(
component.actions.toSpliced(
actionIndex, actionIndex,
1, 1,
action, action,
@ -334,13 +380,13 @@ function EditorView({
content, content,
), ),
) )
}, [setContent]) },
[setContent],
)
const renderComponent = useCallback((component: TacticComponent) => { const renderComponent = useCallback(
if ( (component: TacticComponent) => {
component.type == "player" || if (component.type == "player" || component.type == "phantom") {
component.type == "phantom"
) {
return renderPlayer(component) return renderPlayer(component)
} }
if (component.type == BALL_TYPE) { if (component.type == BALL_TYPE) {
@ -350,9 +396,7 @@ function EditorView({
ball={component} ball={component}
onPosValidated={doMoveBall} onPosValidated={doMoveBall}
onRemove={() => { onRemove={() => {
setContent((content) => setContent((content) => removeBall(content))
removeBall(content),
)
setObjects((objects) => [ setObjects((objects) => [
...objects, ...objects,
{ key: "ball" }, { key: "ball" },
@ -361,12 +405,13 @@ function EditorView({
/> />
) )
} }
throw new Error( throw new Error("unknown tactic component " + component)
"unknown tactic component " + component, },
[renderPlayer, doMoveBall, setContent],
) )
}, [renderPlayer, doMoveBall, setContent])
const renderActions = useCallback((component: TacticComponent) => const renderActions = useCallback(
(component: TacticComponent) =>
component.actions.map((action, i) => { component.actions.map((action, i) => {
return ( return (
<CourtAction <CourtAction
@ -383,7 +428,9 @@ function EditorView({
} }
/> />
) )
}), [doDeleteAction, doUpdateAction]) }),
[doDeleteAction, doUpdateAction],
)
return ( return (
<div id="main-div"> <div id="main-div">
@ -398,28 +445,42 @@ function EditorView({
<TitleInput <TitleInput
style={titleStyle} style={titleStyle}
default_value={name} default_value={name}
onValidated={useCallback((new_name) => { onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => { onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE) setTitleStyle(success ? {} : ERROR_STYLE)
}) })
}, [onNameChange])} },
[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,13 +488,19 @@ 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">
@ -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 }) => ( )}
render={useCallback(
({ team, key }: { team: PlayerTeam; key: string }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
text={key} text={key}
key={key} key={key}
hasBall={false} hasBall={false}
/> />
), [])} ),
[],
)}
/> />
) )
} }
@ -513,10 +596,12 @@ function CourtPlayerArrowAction({
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,8 +691,10 @@ 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
) )
} }

@ -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
@ -60,7 +59,7 @@ export function CourtAction({
style={{ style={{
head, head,
dashArray, dashArray,
color color,
}} }}
/> />
) )

Loading…
Cancel
Save