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

maxime 1 year ago
parent d95e84c413
commit 2c95bf6c99

@ -46,9 +46,7 @@ export default function ArrowAction({
export function ScreenHead({ color }: { color: string }) {
return (
<div
style={{ backgroundColor: color, height: "5px", width: "25px" }}
/>
<div style={{ backgroundColor: color, height: "5px", width: "25px" }} />
)
}

@ -47,14 +47,14 @@ export interface BendableArrowProps {
export interface ArrowStyle {
width?: number
dashArray?: string
color: string,
color: string
head?: () => ReactElement
tail?: () => ReactElement
}
const ArrowStyleDefaults: ArrowStyle = {
width: 3,
color: "black"
color: "black",
}
export interface Segment {
@ -248,8 +248,6 @@ export default function BendableArrow({
* Updates the states based on given parameters, which causes the arrow to re-render.
*/
const update = useCallback(() => {
const parentBase = area.current!.getBoundingClientRect()
const segment = internalSegments[0] ?? null
@ -375,7 +373,15 @@ export default function BendableArrow({
const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
pathRef.current!.setAttribute("d", d)
Object.assign(svgRef.current!.style, svgStyle)
}, [area, internalSegments, startPos, forceStraight, startRadius, endRadius, wavy])
}, [
area,
internalSegments,
startPos,
forceStraight,
startRadius,
endRadius,
wavy,
])
// Will update the arrow when the props change
useEffect(update, [update])

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

@ -44,8 +44,7 @@ export default function CourtPlayer({
const pos = ratioWithinBase(pieceBounds, parentBounds)
if (pos.x !== x || pos.y != y)
onPositionValidated(pos)
if (pos.x !== x || pos.y != y) onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}>
<div
id={playerInfo.id}
@ -59,9 +58,12 @@ export default function CourtPlayer({
<div
tabIndex={0}
className="player-content"
onKeyUp={useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
onKeyUp={useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key == "Delete") onRemove()
}, [onRemove])}>
},
[onRemove],
)}>
<div className="player-actions">
{availableActions(pieceRef.current!)}
</div>

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

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

@ -1,7 +1,21 @@
import { Pos, ratioWithinBase } from "../geo/Pos"
import {BallState, Player, PlayerInfo, PlayerTeam,} from "../model/tactic/Player"
import {Ball, BALL_ID, BALL_TYPE, CourtObject,} from "../model/tactic/CourtObjects"
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
import {
BallState,
Player,
PlayerInfo,
PlayerTeam,
} from "../model/tactic/Player"
import {
Ball,
BALL_ID,
BALL_TYPE,
CourtObject,
} from "../model/tactic/CourtObjects"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState } from "./PlayerDomains"
@ -69,13 +83,16 @@ export function placeObjectAt(
export function dropBallOnComponent(
targetedComponentIdx: number,
content: TacticContent,
setAsOrigin: boolean
setAsOrigin: boolean,
): TacticContent {
const component = content.components[targetedComponentIdx]
if ((component.type == 'player' || component.type == 'phantom')) {
if (component.type === "player" || component.type === "phantom") {
const newState = setAsOrigin
? (component.ballState === BallState.PASSED || component.ballState === BallState.PASSED_ORIGIN) ? BallState.PASSED_ORIGIN : BallState.HOLDS_ORIGIN
? component.ballState === BallState.PASSED ||
component.ballState === BallState.PASSED_ORIGIN
? BallState.PASSED_ORIGIN
: BallState.HOLDS_ORIGIN
: BallState.HOLDS_BY_PASS
content = changePlayerBallState(component, newState, content)

@ -22,7 +22,10 @@ import {PlayerPiece} from "../components/editor/PlayerPiece"
import { Tactic, TacticComponent, TacticContent } from "../model/tactic/Tactic"
import { fetchAPI } from "../Fetcher"
import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "./editor/CourtAction"
@ -39,15 +42,30 @@ import {
removeBall,
updateComponent,
} 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 CourtPlayer from "../components/editor/CourtPlayer"
import {createAction, getActionKind, isActionValid, removeAction} from "../editor/ActionsDomains"
import {
createAction,
getActionKind,
isActionValid,
removeAction,
} from "../editor/ActionsDomains"
import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import 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 { BASE } from "../Constants"
@ -122,7 +140,6 @@ function EditorView({
onNameChange,
courtType,
}: EditorViewProps) {
const isInGuestMode = id == -1
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(() => {
setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }])
}, [setObjects, content]);
}, [setObjects, content])
const insertRackedPlayer = (player: Player) => {
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))
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) => {
if (from) {
content = changePlayerBallState(from, BallState.NONE, content)
}
content = placeBallAt(
newBounds,
courtBounds(),
content = changePlayerBallState(
from,
BallState.NONE,
content,
)
}
content = placeBallAt(newBounds, courtBounds(), 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) =>
moveComponent(
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
if (player.type == "player") {
@ -259,7 +289,6 @@ function EditorView({
}
}
return [
canPlaceArrows && (
<CourtPlayerArrowAction
@ -273,15 +302,22 @@ function EditorView({
setContent={setContent}
/>
),
(info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction key={2} onDrop={(ballBounds) => {
(info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction
key={2}
onDrop={(ballBounds) => {
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
const isPhantom = component.type == "phantom"
if (isPhantom) {
@ -303,29 +339,39 @@ function EditorView({
key={component.id}
className={isPhantom ? "phantom" : "player"}
playerInfo={info}
onPositionValidated={(newPos) => validatePlayerPosition(component, info, newPos)}
onPositionValidated={(newPos) =>
validatePlayerPosition(component, info, newPos)
}
onRemove={() => doRemovePlayer(component)}
courtRef={courtRef}
availableActions={() => renderAvailablePlayerActions(info, component)}
availableActions={() =>
renderAvailablePlayerActions(info, component)
}
/>
)
}, [content.components, doRemovePlayer, renderAvailablePlayerActions, validatePlayerPosition])
},
[
content.components,
doRemovePlayer,
renderAvailablePlayerActions,
validatePlayerPosition,
],
)
const doDeleteAction = useCallback((
action: Action,
idx: number,
origin: TacticComponent,
) => {
const doDeleteAction = useCallback(
(action: Action, idx: number, origin: TacticComponent) => {
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) =>
updateComponent(
{
...component,
actions:
component.actions.toSpliced(
actions: component.actions.toSpliced(
actionIndex,
1,
action,
@ -334,13 +380,13 @@ function EditorView({
content,
),
)
}, [setContent])
},
[setContent],
)
const renderComponent = useCallback((component: TacticComponent) => {
if (
component.type == "player" ||
component.type == "phantom"
) {
const renderComponent = useCallback(
(component: TacticComponent) => {
if (component.type == "player" || component.type == "phantom") {
return renderPlayer(component)
}
if (component.type == BALL_TYPE) {
@ -350,9 +396,7 @@ function EditorView({
ball={component}
onPosValidated={doMoveBall}
onRemove={() => {
setContent((content) =>
removeBall(content),
)
setContent((content) => removeBall(content))
setObjects((objects) => [
...objects,
{ key: "ball" },
@ -361,12 +405,13 @@ function EditorView({
/>
)
}
throw new Error(
"unknown tactic component " + component,
throw new Error("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) => {
return (
<CourtAction
@ -383,7 +428,9 @@ function EditorView({
}
/>
)
}), [doDeleteAction, doUpdateAction])
}),
[doDeleteAction, doUpdateAction],
)
return (
<div id="main-div">
@ -398,28 +445,42 @@ function EditorView({
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback((new_name) => {
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
}, [onNameChange])}
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right" />
</div>
<div id="edit-div">
<div id="racks">
<PlayerRack id={"allies"} objects={allies} setObjects={setAllies} setComponents={setComponents}
courtRef={courtRef}/>
<PlayerRack
id={"allies"}
objects={allies}
setObjects={setAllies}
setComponents={setComponents}
courtRef={courtRef}
/>
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect())
, [courtBounds])}
onElementDetached={useCallback((r, e: RackedCourtObject) =>
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
@ -427,13 +488,19 @@ function EditorView({
e,
content,
),
)
, [courtBounds, setContent])}
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack id={"opponents"} objects={opponents} setObjects={setOpponents}
setComponents={setComponents} courtRef={courtRef}/>
<PlayerRack
id={"opponents"}
objects={opponents}
setObjects={setOpponents}
setComponents={setComponents}
courtRef={courtRef}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
@ -456,23 +523,35 @@ interface PlayerRackProps {
id: string
objects: RackedPlayer[]
setObjects: (state: RackedPlayer[]) => void
setComponents: (f: (components: TacticComponent[]) => TacticComponent[]) => void
setComponents: (
f: (components: TacticComponent[]) => TacticComponent[],
) => void
courtRef: RefObject<HTMLDivElement>
}
function PlayerRack({id, objects, setObjects, courtRef, setComponents}: PlayerRackProps) {
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
function PlayerRack({
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
return (
<Rack
id={id}
objects={objects}
onChange={setObjects}
canDetach={useCallback((div) =>
overlaps(courtBounds(), div.getBoundingClientRect())
, [courtBounds])}
onElementDetached={useCallback((r, e: RackedPlayer) =>
canDetach={useCallback(
(div) => overlaps(courtBounds(), div.getBoundingClientRect()),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedPlayer) =>
setComponents((components) => [
...components,
placePlayerAt(
@ -480,16 +559,20 @@ function PlayerRack({id, objects, setObjects, courtRef, setComponents}: PlayerRa
courtBounds(),
e,
),
])
, [courtBounds, setComponents])}
render={useCallback(({team, key}: { team: PlayerTeam, key: string }) => (
]),
[courtBounds, setComponents],
)}
render={useCallback(
({ team, key }: { team: PlayerTeam; key: string }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
), [])}
),
[],
)}
/>
)
}
@ -513,10 +596,12 @@ function CourtPlayerArrowAction({
content,
setContent,
setPreviewAction,
courtRef
courtRef,
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef])
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
return (
<ArrowAction
@ -533,29 +618,22 @@ function CourtPlayerArrowAction({
...action!,
segments: [
{
next: ratioWithinBase(
arrowHeadPos,
courtBounds(),
),
next: ratioWithinBase(arrowHeadPos, courtBounds()),
},
],
type: getActionKind(
target,
playerInfo.ballState,
),
isInvalid: !overlaps(headPos, courtBounds()) || !isActionValid(player, target, content.components)
type: getActionKind(target, playerInfo.ballState),
isInvalid:
!overlaps(headPos, courtBounds()) ||
!isActionValid(player, target, content.components),
}))
}}
onHeadPicked={(headPos) => {
(document.activeElement as HTMLElement).blur()
;(document.activeElement as HTMLElement).blur()
setPreviewAction({
origin: playerInfo.id,
type: getActionKind(null, playerInfo.ballState),
target: ratioWithinBase(
headPos,
courtBounds(),
),
target: ratioWithinBase(headPos, courtBounds()),
segments: [
{
next: ratioWithinBase(
@ -564,7 +642,7 @@ function CourtPlayerArrowAction({
),
},
],
isInvalid: false
isInvalid: false,
})
}}
onHeadDropped={(headRect) => {
@ -574,27 +652,21 @@ function CourtPlayerArrowAction({
}
setContent((content) => {
let {createdAction, newContent} =
createAction(
let { createdAction, newContent } = createAction(
player,
courtBounds(),
headRect,
content,
)
if (
createdAction.type == ActionKind.SHOOT
) {
const targetIdx =
newContent.components.findIndex(
(c) =>
c.id ==
createdAction.target,
if (createdAction.type == ActionKind.SHOOT) {
const targetIdx = newContent.components.findIndex(
(c) => c.id == createdAction.target,
)
newContent = dropBallOnComponent(
targetIdx,
newContent,
false
false,
)
newContent = updateComponent(
{
@ -607,7 +679,6 @@ function CourtPlayerArrowAction({
)
}
return newContent
})
setPreviewAction(null)
@ -620,8 +691,10 @@ function isBallOnCourt(content: TacticContent) {
return (
content.components.findIndex(
(c) =>
(c.type == "player" && (c.ballState === BallState.HOLDS_ORIGIN || c.ballState === BallState.HOLDS_BY_PASS)) ||
c.type == BALL_TYPE,
((c.type === "player" || c.type === "phantom") &&
(c.ballState === BallState.HOLDS_ORIGIN ||
c.ballState === BallState.PASSED_ORIGIN)) ||
c.type === BALL_TYPE,
) != -1
)
}

@ -19,9 +19,8 @@ export function CourtAction({
onActionChanges,
onActionDeleted,
courtRef,
isInvalid
isInvalid,
}: CourtActionProps) {
const color = isInvalid ? "red" : "black"
let head
@ -60,7 +59,7 @@ export function CourtAction({
style={{
head,
dashArray,
color
color,
}}
/>
)

Loading…
Cancel
Save