diff --git a/front/components/actions/ArrowAction.tsx b/front/components/actions/ArrowAction.tsx index 86e1a49..8fbae5f 100644 --- a/front/components/actions/ArrowAction.tsx +++ b/front/components/actions/ArrowAction.tsx @@ -44,15 +44,13 @@ export default function ArrowAction({ ) } -export function ScreenHead({color}: {color: string}) { +export function ScreenHead({ color }: { color: string }) { return ( -
+
) } -export function MoveToHead({color}: {color: string}) { +export function MoveToHead({ color }: { color: string }) { return ( diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx index 5a3ac2d..7a4760b 100644 --- a/front/components/arrows/BendableArrow.tsx +++ b/front/components/arrows/BendableArrow.tsx @@ -47,14 +47,14 @@ export interface BendableArrowProps { export interface ArrowStyle { width?: number dashArray?: string - color: string, + color: string head?: () => ReactElement tail?: () => ReactElement } const ArrowStyleDefaults: ArrowStyle = { width: 3, - color: "black" + color: "black", } export interface Segment { @@ -99,20 +99,20 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos { * @constructor */ export default function BendableArrow({ - area, - startPos, + area, + startPos, - segments, - onSegmentsChanges, + segments, + onSegmentsChanges, - forceStraight, - wavy, + forceStraight, + wavy, - style, - startRadius = 0, - endRadius = 0, - onDeleteRequested, - }: BendableArrowProps) { + style, + startRadius = 0, + endRadius = 0, + onDeleteRequested, +}: BendableArrowProps) { const containerRef = useRef(null) const svgRef = useRef(null) const pathRef = useRef(null) @@ -162,7 +162,7 @@ export default function BendableArrow({ * @param parentBase */ function computePoints(parentBase: DOMRect) { - return segments.flatMap(({next, controlPoint}, i) => { + return segments.flatMap(({ next, controlPoint }, i) => { const prev = i == 0 ? startPos : segments[i - 1].next const prevRelative = getPosWithinBase(prev, parentBase) @@ -248,8 +248,6 @@ export default function BendableArrow({ * Updates the states based on given parameters, which causes the arrow to re-render. */ const update = useCallback(() => { - - const parentBase = area.current!.getBoundingClientRect() const segment = internalSegments[0] ?? null @@ -268,8 +266,8 @@ export default function BendableArrow({ const endPrevious = forceStraight ? startRelative : lastSegment.controlPoint - ? posWithinBase(lastSegment.controlPoint, parentBase) - : getPosWithinBase(lastSegment.start, parentBase) + ? posWithinBase(lastSegment.controlPoint, parentBase) + : getPosWithinBase(lastSegment.start, parentBase) const tailPos = constraintInCircle( startRelative, @@ -307,15 +305,15 @@ export default function BendableArrow({ const segmentsRelatives = ( forceStraight ? [ - { - start: startPos, - controlPoint: undefined, - end: lastSegment.end, - }, - ] + { + start: startPos, + controlPoint: undefined, + end: lastSegment.end, + }, + ] : internalSegments - ).map(({start, controlPoint, end}) => { - const svgPosRelativeToBase = {x: left, y: top} + ).map(({ start, controlPoint, end }) => { + const svgPosRelativeToBase = { x: left, y: top } const nextRelative = relativeTo( getPosWithinBase(end, parentBase), @@ -328,9 +326,9 @@ export default function BendableArrow({ const controlPointRelative = controlPoint && !forceStraight ? relativeTo( - posWithinBase(controlPoint, parentBase), - svgPosRelativeToBase, - ) + posWithinBase(controlPoint, parentBase), + svgPosRelativeToBase, + ) : middle(startRelative, nextRelative) return { @@ -341,7 +339,7 @@ export default function BendableArrow({ }) const computedSegments = segmentsRelatives - .map(({start, cp, end: e}, idx) => { + .map(({ start, cp, end: e }, idx) => { let end = e if (idx == segmentsRelatives.length - 1) { //if it is the last element @@ -375,14 +373,22 @@ export default function BendableArrow({ const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments pathRef.current!.setAttribute("d", d) Object.assign(svgRef.current!.style, svgStyle) - }, [area, internalSegments, startPos, forceStraight, startRadius, endRadius, wavy]) + }, [ + area, + internalSegments, + startPos, + forceStraight, + startRadius, + endRadius, + wavy, + ]) // Will update the arrow when the props change useEffect(update, [update]) useEffect(() => { const observer = new MutationObserver(update) - const config = {attributes: true} + const config = { attributes: true } if (typeof startPos == "string") { observer.observe(document.getElementById(startPos)!, config) } @@ -421,7 +427,7 @@ export default function BendableArrow({ if (forceStraight) return const parentBase = area.current!.getBoundingClientRect() - const clickAbsolutePos: Pos = {x: e.pageX, y: e.pageY} + const clickAbsolutePos: Pos = { x: e.pageX, y: e.pageY } const clickPosBaseRatio = ratioWithinBase( clickAbsolutePos, parentBase, @@ -448,13 +454,13 @@ export default function BendableArrow({ const smoothCp = beforeSegment ? add( - currentPos, - minus( - currentPos, - beforeSegment.controlPoint ?? - middle(beforeSegmentPos, currentPos), - ), - ) + currentPos, + minus( + currentPos, + beforeSegment.controlPoint ?? + middle(beforeSegmentPos, currentPos), + ), + ) : segmentCp const result = searchOnSegment( @@ -502,7 +508,7 @@ export default function BendableArrow({ return (
+ style={{ position: "absolute", top: 0, left: 0 }}> {style?.head?.call(style)}
{style?.tail?.call(style)}
@@ -611,7 +617,7 @@ function wavyBezier( const velocity = cubicBeziersDerivative(start, cp1, cp2, end, t) const velocityLength = norm(velocity) //rotate the velocity by 90 deg - const projection = {x: velocity.y, y: -velocity.x} + const projection = { x: velocity.y, y: -velocity.x } return { x: (projection.x / velocityLength) * amplitude, @@ -633,7 +639,7 @@ function wavyBezier( // 3 : down to middle let phase = 0 - for (let t = step; t <= 1;) { + for (let t = step; t <= 1; ) { const pos = cubicBeziers(start, cp1, cp2, end, t) const amplification = getVerticalAmplification(t) @@ -751,14 +757,14 @@ function searchOnSegment( * @constructor */ function ArrowPoint({ - className, - posRatio, - parentBase, - onMoves, - onPosValidated, - onRemove, - radius = 7, - }: ControlPointProps) { + className, + posRatio, + parentBase, + onMoves, + onPosValidated, + onRemove, + radius = 7, +}: ControlPointProps) { const ref = useRef(null) const pos = posWithinBase(posRatio, parentBase) @@ -774,7 +780,7 @@ function ArrowPoint({ const pointPos = middlePos(ref.current!.getBoundingClientRect()) onMoves(ratioWithinBase(pointPos, parentBase)) }} - position={{x: pos.x - radius, y: pos.y - radius}}> + position={{ x: pos.x - radius, y: pos.y - radius }}>
) => { - if (e.key == "Delete") onRemove() - }, [onRemove])}> + onKeyUp={useCallback( + (e: React.KeyboardEvent) => { + if (e.key == "Delete") onRemove() + }, + [onRemove], + )}>
{availableActions(pieceRef.current!)}
diff --git a/front/editor/ActionsDomains.ts b/front/editor/ActionsDomains.ts index cbb21c2..8bb4200 100644 --- a/front/editor/ActionsDomains.ts +++ b/front/editor/ActionsDomains.ts @@ -1,11 +1,21 @@ -import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" -import {ratioWithinBase} from "../geo/Pos" -import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" -import {overlaps} from "../geo/Box" -import {Action, ActionKind, moves} from "../model/tactic/Action" -import {removeBall, updateComponent} from "./TacticContentDomains" -import {areInSamePath, changePlayerBallState, getOrigin, isNextInPath, removePlayer} from "./PlayerDomains" -import {BALL_TYPE} from "../model/tactic/CourtObjects"; +import { BallState, Player, PlayerPhantom } from "../model/tactic/Player" +import { ratioWithinBase } from "../geo/Pos" +import { + ComponentId, + TacticComponent, + TacticContent, +} from "../model/tactic/Tactic" +import { overlaps } from "../geo/Box" +import { Action, ActionKind, moves } from "../model/tactic/Action" +import { removeBall, updateComponent } from "./TacticContentDomains" +import { + areInSamePath, + changePlayerBallState, + getOrigin, + isNextInPath, + removePlayer, +} from "./PlayerDomains" +import { BALL_TYPE } from "../model/tactic/CourtObjects" export function getActionKind( target: TacticComponent | null, @@ -14,9 +24,7 @@ export function getActionKind( switch (ballState) { case BallState.HOLDS_ORIGIN: case BallState.HOLDS_BY_PASS: - return target - ? ActionKind.SHOOT - : ActionKind.DRIBBLE + return target ? ActionKind.SHOOT : ActionKind.DRIBBLE case BallState.PASSED_ORIGIN: case BallState.PASSED: case BallState.NONE: @@ -26,23 +34,38 @@ export function getActionKind( } } -export function getActionKindBetween(origin: Player | PlayerPhantom, target: TacticComponent | null, state: BallState): ActionKind { +export function getActionKindBetween( + origin: Player | PlayerPhantom, + target: TacticComponent | null, + state: BallState, +): ActionKind { //remove the target if the target is a phantom that is within the origin's path - if (target != null && target.type == 'phantom' && areInSamePath(origin, target)) { - target = null; + if ( + target != null && + target.type == "phantom" && + areInSamePath(origin, target) + ) { + target = null } return getActionKind(target, state) } -export function isActionValid(origin: TacticComponent, target: TacticComponent | null, components: TacticComponent[]): boolean { +export function isActionValid( + origin: TacticComponent, + target: TacticComponent | null, + components: TacticComponent[], +): boolean { /// action is valid if the origin is neither a phantom nor a player if (origin.type != "phantom" && origin.type != "player") { return true } // action is invalid if the origin already moves (unless the origin holds a ball which will lead to a ball pass) - if (origin.actions.find(a => moves(a.type)) && origin.ballState != BallState.HOLDS_BY_PASS) { + if ( + origin.actions.find((a) => moves(a.type)) && + origin.ballState != BallState.HOLDS_BY_PASS + ) { return false } //Action is valid if the target is null @@ -56,23 +79,26 @@ export function isActionValid(origin: TacticComponent, target: TacticComponent | } // action is invalid if the target already moves and is not indirectly bound with origin - if (target.actions.find(a => moves(a.type)) && (hasBoundWith(target, origin, components) || hasBoundWith(origin, target, components))) { + if ( + target.actions.find((a) => moves(a.type)) && + (hasBoundWith(target, origin, components) || + hasBoundWith(origin, target, components)) + ) { return false } // Action is invalid if there is already an action between origin and target. - if (origin.actions.find(a => a.target === target?.id) || target?.actions.find(a => a.target === origin.id)) { + if ( + origin.actions.find((a) => a.target === target?.id) || + target?.actions.find((a) => a.target === origin.id) + ) { return false } - // Action is invalid if there is already an anterior action within the target's path if (target.type == "phantom" || target.type == "player") { - // cant have an action with current path - if (areInSamePath(origin, target)) - return false; - + if (areInSamePath(origin, target)) return false if (alreadyHasAnAnteriorActionWith(origin, target, components)) { return false @@ -82,21 +108,25 @@ export function isActionValid(origin: TacticComponent, target: TacticComponent | return true } -function hasBoundWith(origin: TacticComponent, target: TacticComponent, components: TacticComponent[]): boolean { +function hasBoundWith( + origin: TacticComponent, + target: TacticComponent, + components: TacticComponent[], +): boolean { const toVisit = [origin.id] const visited: string[] = [] let itemId: string | undefined while ((itemId = toVisit.pop())) { - - if (visited.indexOf(itemId) !== -1) - continue + if (visited.indexOf(itemId) !== -1) continue visited.push(itemId) - const item = components.find(c => c.id === itemId)! + const item = components.find((c) => c.id === itemId)! - const itemBounds = item.actions.flatMap(a => typeof a.target == "string" ? [a.target] : []) + const itemBounds = item.actions.flatMap((a) => + typeof a.target == "string" ? [a.target] : [], + ) if (itemBounds.indexOf(target.id) !== -1) { return true } @@ -107,30 +137,58 @@ function hasBoundWith(origin: TacticComponent, target: TacticComponent, componen return false } -function alreadyHasAnAnteriorActionWith(origin: Player | PlayerPhantom, target: Player | PlayerPhantom, components: TacticComponent[]): boolean { - const targetOrigin = target.type === "phantom" ? getOrigin(target, components) : target - const targetOriginPath = [targetOrigin.id, ...(targetOrigin.path?.items ?? [])] - - const originOrigin = origin.type === "phantom" ? getOrigin(origin, components) : origin - const originOriginPath = [originOrigin.id, ...(originOrigin.path?.items ?? [])] +function alreadyHasAnAnteriorActionWith( + origin: Player | PlayerPhantom, + target: Player | PlayerPhantom, + components: TacticComponent[], +): boolean { + const targetOrigin = + target.type === "phantom" ? getOrigin(target, components) : target + const targetOriginPath = [ + targetOrigin.id, + ...(targetOrigin.path?.items ?? []), + ] + + const originOrigin = + origin.type === "phantom" ? getOrigin(origin, components) : origin + const originOriginPath = [ + originOrigin.id, + ...(originOrigin.path?.items ?? []), + ] const targetIdx = targetOriginPath.indexOf(target.id) for (let i = targetIdx; i < targetOriginPath.length; i++) { - const phantom = components.find(c => c.id === targetOriginPath[i])! as (Player | PlayerPhantom) - if (phantom.actions.find(a => typeof a.target === "string" && (originOriginPath.indexOf(a.target) !== -1))) { - return true; + const phantom = components.find( + (c) => c.id === targetOriginPath[i], + )! as Player | PlayerPhantom + if ( + phantom.actions.find( + (a) => + typeof a.target === "string" && + originOriginPath.indexOf(a.target) !== -1, + ) + ) { + return true } } const originIdx = originOriginPath.indexOf(origin.id) for (let i = 0; i <= originIdx; i++) { - const phantom = components.find(c => c.id === originOriginPath[i])! as (Player | PlayerPhantom) - if (phantom.actions.find(a => typeof a.target === "string" && targetOriginPath.indexOf(a.target) > targetIdx)) { - return true; + const phantom = components.find( + (c) => c.id === originOriginPath[i], + )! as Player | PlayerPhantom + if ( + phantom.actions.find( + (a) => + typeof a.target === "string" && + targetOriginPath.indexOf(a.target) > targetIdx, + ) + ) { + return true } } - return false; + return false } export function createAction( @@ -143,8 +201,8 @@ export function createAction( * Creates a new phantom component. * Be aware that this function will reassign the `content` parameter. */ - function createPhantom(originState: BallState): ComponentId { - const {x, y} = ratioWithinBase(arrowHead, courtBounds) + function createPhantom(forceHasBall: boolean): ComponentId { + const { x, y } = ratioWithinBase(arrowHead, courtBounds) let itemIndex: number let originPlayer: Player @@ -177,17 +235,19 @@ export function createAction( ) let phantomState: BallState - switch (originState) { - case BallState.HOLDS_ORIGIN: - phantomState = BallState.HOLDS_BY_PASS - break - case BallState.PASSED: - case BallState.PASSED_ORIGIN: - phantomState = BallState.NONE - break - default: - phantomState = originState - } + if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN + else + switch (origin.ballState) { + case BallState.HOLDS_ORIGIN: + phantomState = BallState.HOLDS_BY_PASS + break + case BallState.PASSED: + case BallState.PASSED_ORIGIN: + phantomState = BallState.NONE + break + default: + phantomState = origin.ballState + } const phantom: PlayerPhantom = { type: "phantom", @@ -225,7 +285,7 @@ export function createAction( const action: Action = { target: toId, type: getActionKind(component, origin.ballState), - segments: [{next: toId}], + segments: [{ next: toId }], } return { @@ -241,12 +301,12 @@ export function createAction( } } - const phantomId = createPhantom(origin.ballState) + const phantomId = createPhantom(false) const action: Action = { target: phantomId, type: getActionKind(null, origin.ballState), - segments: [{next: phantomId}], + segments: [{ next: phantomId }], } return { newContent: updateComponent( @@ -279,29 +339,39 @@ export function removeAllActionsTargeting( } } - -export function removeAction(origin: TacticComponent, action: Action, actionIdx: number, content: TacticContent): TacticContent { +export function removeAction( + origin: TacticComponent, + action: Action, + actionIdx: number, + content: TacticContent, +): TacticContent { origin = { ...origin, actions: origin.actions.toSpliced(actionIdx, 1), } - content = updateComponent( - origin, - content, - ) + content = updateComponent(origin, content) if (action.target == null) return content - const target = content.components.find( - (c) => action.target == c.id, - )! + const target = content.components.find((c) => action.target == c.id)! // if the removed action is a shoot, set the origin as holding the ball - if (action.type == ActionKind.SHOOT && (origin.type === "player" || origin.type === "phantom")) { + if ( + action.type == ActionKind.SHOOT && + (origin.type === "player" || origin.type === "phantom") + ) { if (origin.ballState === BallState.PASSED) - content = changePlayerBallState(origin, BallState.HOLDS_BY_PASS, content) + content = changePlayerBallState( + origin, + BallState.HOLDS_BY_PASS, + content, + ) else if (origin.ballState === BallState.PASSED_ORIGIN) - content = changePlayerBallState(origin, BallState.HOLDS_ORIGIN, content) + content = changePlayerBallState( + origin, + BallState.HOLDS_ORIGIN, + content, + ) if (target.type === "player" || target.type === "phantom") content = changePlayerBallState(target, BallState.NONE, content) @@ -315,16 +385,11 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx: path = getOrigin(origin, content.components).path } - if ( - path != null && - path.items.find((c) => c == target.id) - ) { + if (path != null && path.items.find((c) => c == target.id)) { content = removePlayer(target, content) } } - - return content } @@ -335,14 +400,18 @@ export function removeAction(origin: TacticComponent, action: Action, actionIdx: * @param newState * @param content */ -export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent { +export function spreadNewStateFromOriginStateChange( + origin: Player | PlayerPhantom, + newState: BallState, + content: TacticContent, +): TacticContent { if (origin.ballState === newState) { return content } origin = { ...origin, - ballState: newState + ballState: newState, } content = updateComponent(origin, content) @@ -350,47 +419,72 @@ export function spreadNewStateFromOriginStateChange(origin: Player | PlayerPhant for (let i = 0; i < origin.actions.length; i++) { const action = origin.actions[i] if (typeof action.target !== "string") { - continue; + continue } - const actionTarget = content.components.find(c => action.target === c.id)! as Player | PlayerPhantom; + const actionTarget = content.components.find( + (c) => action.target === c.id, + )! as Player | PlayerPhantom let targetState: BallState = actionTarget.ballState let deleteAction = false if (isNextInPath(origin, actionTarget, content.components)) { - /// If the target is the next phantom from the origin, its state is propagated. - targetState = (newState === BallState.PASSED || newState === BallState.PASSED_ORIGIN) ? BallState.NONE : newState - } else if (newState === BallState.NONE && action.type === ActionKind.SHOOT) { + switch (newState) { + case BallState.PASSED: + case BallState.PASSED_ORIGIN: + targetState = BallState.NONE + break + case BallState.HOLDS_ORIGIN: + targetState = BallState.HOLDS_BY_PASS + break + default: + targetState = newState + } + } else if ( + newState === BallState.NONE && + action.type === ActionKind.SHOOT + ) { /// if the new state removes the ball from the player, remove all actions that were meant to shoot the ball deleteAction = true targetState = BallState.NONE // then remove the ball for the target as well - } else if ((newState === BallState.HOLDS_BY_PASS || newState === BallState.HOLDS_ORIGIN) && action.type === ActionKind.SCREEN) { + } else if ( + (newState === BallState.HOLDS_BY_PASS || + newState === BallState.HOLDS_ORIGIN) && + action.type === ActionKind.SCREEN + ) { targetState = BallState.HOLDS_BY_PASS } if (deleteAction) { content = removeAction(origin, action, i, content) - origin = content.components.find(c => c.id === origin.id)! as Player | PlayerPhantom - i--; // step back + origin = content.components.find((c) => c.id === origin.id)! as + | Player + | PlayerPhantom + i-- // step back } else { // do not change the action type if it is a shoot action - const type = action.type == ActionKind.SHOOT - ? ActionKind.SHOOT - : getActionKindBetween(origin, actionTarget, newState) + const type = + action.type == ActionKind.SHOOT + ? ActionKind.SHOOT + : getActionKindBetween(origin, actionTarget, newState) origin = { ...origin, actions: origin.actions.toSpliced(i, 1, { ...action, - type - }) + type, + }), } content = updateComponent(origin, content) } - content = spreadNewStateFromOriginStateChange(actionTarget, targetState, content) + content = spreadNewStateFromOriginStateChange( + actionTarget, + targetState, + content, + ) } return content -} \ No newline at end of file +} diff --git a/front/editor/PlayerDomains.ts b/front/editor/PlayerDomains.ts index 08f70b8..39419a2 100644 --- a/front/editor/PlayerDomains.ts +++ b/front/editor/PlayerDomains.ts @@ -1,8 +1,11 @@ -import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" -import {TacticComponent, TacticContent} from "../model/tactic/Tactic" -import {removeComponent, updateComponent} from "./TacticContentDomains" -import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange} from "./ActionsDomains" -import {ActionKind} from "../model/tactic/Action"; +import { BallState, Player, PlayerPhantom } from "../model/tactic/Player" +import { TacticComponent, TacticContent } from "../model/tactic/Tactic" +import { removeComponent, updateComponent } from "./TacticContentDomains" +import { + removeAllActionsTargeting, + spreadNewStateFromOriginStateChange, +} from "./ActionsDomains" +import { ActionKind } from "../model/tactic/Action" export function getOrigin( pathItem: PlayerPhantom, @@ -34,12 +37,19 @@ export function areInSamePath( * @param components * @returns true if the `other` player is the phantom next-to the origin's path. */ -export function isNextInPath(origin: Player | PlayerPhantom, other: Player | PlayerPhantom, components: TacticComponent[]): boolean { +export function isNextInPath( + origin: Player | PlayerPhantom, + other: Player | PlayerPhantom, + components: TacticComponent[], +): boolean { if (origin.type === "player") { return origin.path?.items[0] === other.id } const originPath = getOrigin(origin, components).path! - return originPath.items!.indexOf(origin.id) === originPath.items!.indexOf(other.id) - 1 + return ( + originPath.items!.indexOf(origin.id) === + originPath.items!.indexOf(other.id) - 1 + ) } export function removePlayerPath( @@ -81,8 +91,14 @@ export function removePlayer( if (action.type !== ActionKind.SHOOT) { continue } - const actionTarget = content.components.find(c => c.id === action.target)! as (Player | PlayerPhantom) - return spreadNewStateFromOriginStateChange(actionTarget, BallState.NONE, content) + const actionTarget = content.components.find( + (c) => c.id === action.target, + )! as Player | PlayerPhantom + return spreadNewStateFromOriginStateChange( + actionTarget, + BallState.NONE, + content, + ) } return content @@ -114,14 +130,18 @@ export function truncatePlayerPath( truncateStartIdx == 0 ? null : { - ...path, - items: path.items.toSpliced(truncateStartIdx), - }, + ...path, + items: path.items.toSpliced(truncateStartIdx), + }, }, content, ) } -export function changePlayerBallState(player: Player | PlayerPhantom, newState: BallState, content: TacticContent): TacticContent { +export function changePlayerBallState( + player: Player | PlayerPhantom, + newState: BallState, + content: TacticContent, +): TacticContent { return spreadNewStateFromOriginStateChange(player, newState, content) -} \ No newline at end of file +} diff --git a/front/editor/TacticContentDomains.ts b/front/editor/TacticContentDomains.ts index d252a10..1c6bfda 100644 --- a/front/editor/TacticContentDomains.ts +++ b/front/editor/TacticContentDomains.ts @@ -1,17 +1,31 @@ -import {Pos, ratioWithinBase} from "../geo/Pos" -import {BallState, Player, PlayerInfo, PlayerTeam,} from "../model/tactic/Player" -import {Ball, BALL_ID, BALL_TYPE, CourtObject,} from "../model/tactic/CourtObjects" -import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" -import {overlaps} from "../geo/Box" -import {RackedCourtObject, RackedPlayer} from "./RackedItems" -import {changePlayerBallState} from "./PlayerDomains" +import { Pos, ratioWithinBase } from "../geo/Pos" +import { + BallState, + Player, + PlayerInfo, + PlayerTeam, +} from "../model/tactic/Player" +import { + Ball, + BALL_ID, + BALL_TYPE, + CourtObject, +} from "../model/tactic/CourtObjects" +import { + ComponentId, + TacticComponent, + TacticContent, +} from "../model/tactic/Tactic" +import { overlaps } from "../geo/Box" +import { RackedCourtObject, RackedPlayer } from "./RackedItems" +import { changePlayerBallState } from "./PlayerDomains" export function placePlayerAt( refBounds: DOMRect, courtBounds: DOMRect, element: RackedPlayer, ): Player { - const {x, y} = ratioWithinBase(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) return { type: "player", @@ -32,7 +46,7 @@ export function placeObjectAt( rackedObject: RackedCourtObject, content: TacticContent, ): TacticContent { - const {x, y} = ratioWithinBase(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) let courtObject: CourtObject @@ -69,13 +83,16 @@ export function placeObjectAt( export function dropBallOnComponent( targetedComponentIdx: number, content: TacticContent, - setAsOrigin: boolean + setAsOrigin: boolean, ): TacticContent { const component = content.components[targetedComponentIdx] - if ((component.type == 'player' || component.type == 'phantom')) { + if (component.type === "player" || component.type === "phantom") { const newState = setAsOrigin - ? (component.ballState === BallState.PASSED || component.ballState === BallState.PASSED_ORIGIN) ? BallState.PASSED_ORIGIN : BallState.HOLDS_ORIGIN + ? component.ballState === BallState.PASSED || + component.ballState === BallState.PASSED_ORIGIN + ? BallState.PASSED_ORIGIN + : BallState.HOLDS_ORIGIN : BallState.HOLDS_BY_PASS content = changePlayerBallState(component, newState, content) @@ -117,7 +134,7 @@ export function placeBallAt( const ballIdx = content.components.findIndex((o) => o.type == "ball") - const {x, y} = ratioWithinBase(refBounds, courtBounds) + const { x, y } = ratioWithinBase(refBounds, courtBounds) const ball: Ball = { type: BALL_TYPE, @@ -227,5 +244,5 @@ export function getRackPlayers( c.type == "player" && c.team == team && c.role == role, ) == -1, ) - .map((key) => ({team, key})) + .map((key) => ({ team, key })) } diff --git a/front/model/tactic/Action.ts b/front/model/tactic/Action.ts index e590696..c97cdd4 100644 --- a/front/model/tactic/Action.ts +++ b/front/model/tactic/Action.ts @@ -18,4 +18,4 @@ export interface MovementAction { export function moves(kind: ActionKind): boolean { return kind != ActionKind.SHOOT -} \ No newline at end of file +} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 90f278e..de7c749 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -14,20 +14,23 @@ import TitleInput from "../components/TitleInput" import PlainCourt from "../assets/court/full_court.svg?react" import HalfCourt from "../assets/court/half_court.svg?react" -import {BallPiece} from "../components/editor/BallPiece" +import { BallPiece } from "../components/editor/BallPiece" -import {Rack} from "../components/Rack" -import {PlayerPiece} from "../components/editor/PlayerPiece" +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" -import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic" -import {fetchAPI} from "../Fetcher" +import { Tactic, TacticComponent, TacticContent } from "../model/tactic/Tactic" +import { fetchAPI } from "../Fetcher" -import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState" +import SavingState, { + SaveState, + SaveStates, +} from "../components/editor/SavingState" -import {BALL_TYPE} from "../model/tactic/CourtObjects" -import {CourtAction} from "./editor/CourtAction" -import {ActionPreview, BasketCourt} from "../components/editor/BasketCourt" -import {overlaps} from "../geo/Box" +import { BALL_TYPE } from "../model/tactic/CourtObjects" +import { CourtAction } from "./editor/CourtAction" +import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" +import { overlaps } from "../geo/Box" import { dropBallOnComponent, getComponentCollided, @@ -39,17 +42,32 @@ import { removeBall, updateComponent, } from "../editor/TacticContentDomains" -import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player" -import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems" +import { + BallState, + Player, + PlayerInfo, + PlayerPhantom, + PlayerTeam, +} from "../model/tactic/Player" +import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" import CourtPlayer from "../components/editor/CourtPlayer" -import {createAction, getActionKind, isActionValid, removeAction} from "../editor/ActionsDomains" +import { + createAction, + getActionKind, + isActionValid, + removeAction, +} from "../editor/ActionsDomains" import ArrowAction from "../components/actions/ArrowAction" -import {middlePos, Pos, ratioWithinBase} from "../geo/Pos" -import {Action, ActionKind} from "../model/tactic/Action" +import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" +import { Action, ActionKind } from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" -import {changePlayerBallState, getOrigin, removePlayer,} from "../editor/PlayerDomains" -import {CourtBall} from "../components/editor/CourtBall" -import {BASE} from "../Constants" +import { + changePlayerBallState, + getOrigin, + removePlayer, +} from "../editor/PlayerDomains" +import { CourtBall } from "../components/editor/CourtBall" +import { BASE } from "../Constants" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -72,7 +90,7 @@ export interface EditorProps { courtType: "PLAIN" | "HALF" } -export default function Editor({id, name, courtType, content}: EditorProps) { +export default function Editor({ id, name, courtType, content }: EditorProps) { const isInGuestMode = id == -1 const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) @@ -98,7 +116,7 @@ export default function Editor({id, name, courtType, content}: EditorProps) { ) return SaveStates.Guest } - return fetchAPI(`tactic/${id}/save`, {content}).then((r) => + return fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok ? SaveStates.Ok : SaveStates.Err, ) }} @@ -107,7 +125,7 @@ export default function Editor({id, name, courtType, content}: EditorProps) { localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name) return true //simulate that the name has been changed } - return fetchAPI(`tactic/${id}/edit/name`, {name}).then( + return fetchAPI(`tactic/${id}/edit/name`, { name }).then( (r) => r.ok, ) }} @@ -117,12 +135,11 @@ export default function Editor({id, name, courtType, content}: EditorProps) { } function EditorView({ - tactic: {id, name, content: initialContent}, - onContentChange, - onNameChange, - courtType, - }: EditorViewProps) { - + tactic: { id, name, content: initialContent }, + onContentChange, + onNameChange, + courtType, +}: EditorViewProps) { const isInGuestMode = id == -1 const [titleStyle, setTitleStyle] = useState({}) @@ -150,7 +167,7 @@ function EditorView({ ) const [objects, setObjects] = useState(() => - isBallOnCourt(content) ? [] : [{key: "ball"}], + isBallOnCourt(content) ? [] : [{ key: "ball" }], ) const [previewAction, setPreviewAction] = useState( @@ -167,11 +184,14 @@ function EditorView({ })) } - const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef]) + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) useEffect(() => { - setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}]) - }, [setObjects, content]); + setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }]) + }, [setObjects, content]) const insertRackedPlayer = (player: Player) => { let setter @@ -183,7 +203,7 @@ function EditorView({ setter = setAllies } if (player.ballState == BallState.HOLDS_BY_PASS) { - setObjects([{key: "ball"}]) + setObjects([{ key: "ball" }]) } setter((players) => [ ...players, @@ -195,195 +215,222 @@ function EditorView({ ]) } - const doRemovePlayer = useCallback((component: Player | PlayerPhantom) => { - setContent((c) => removePlayer(component, c)) - if (component.type == "player") insertRackedPlayer(component) - }, [setContent]) + const doRemovePlayer = useCallback( + (component: Player | PlayerPhantom) => { + setContent((c) => removePlayer(component, c)) + if (component.type == "player") insertRackedPlayer(component) + }, + [setContent], + ) + + const doMoveBall = useCallback( + (newBounds: DOMRect, from?: Player | PlayerPhantom) => { + setContent((content) => { + if (from) { + content = changePlayerBallState( + from, + BallState.NONE, + content, + ) + } - const doMoveBall = useCallback((newBounds: DOMRect, from?: Player | PlayerPhantom) => { - setContent((content) => { - if (from) { - content = changePlayerBallState(from, BallState.NONE, content) - } + content = placeBallAt(newBounds, courtBounds(), content) - content = placeBallAt( - newBounds, - courtBounds(), - content, + return content + }) + }, + [courtBounds, setContent], + ) + + const validatePlayerPosition = useCallback( + (player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => { + setContent((content) => + moveComponent( + newPos, + player, + info, + courtBounds(), + content, + + (content) => { + if (player.type == "player") insertRackedPlayer(player) + return removePlayer(player, content) + }, + ), ) + }, + [courtBounds, setContent], + ) - return content - }) - }, [courtBounds, setContent]) - - const validatePlayerPosition = useCallback((player: Player | PlayerPhantom, info: PlayerInfo, newPos: Pos) => { - setContent((content) => - moveComponent( - newPos, - player, - info, - courtBounds(), - content, - - (content) => { - if (player.type == "player") insertRackedPlayer(player) - return removePlayer(player, content) - }, - ), - ) - }, [courtBounds, setContent]) - - const renderAvailablePlayerActions = useCallback((info: PlayerInfo, player: Player | PlayerPhantom) => { - let canPlaceArrows: boolean - - if (player.type == "player") { - canPlaceArrows = - player.path == null || - player.actions.findIndex( - (p) => p.type != ActionKind.SHOOT, - ) == -1 - } else { - const origin = getOrigin(player, content.components) - const path = origin.path! - // phantoms can only place other arrows if they are the head of the path - canPlaceArrows = - path.items.indexOf(player.id) == path.items.length - 1 - if (canPlaceArrows) { - // and if their only action is to shoot the ball - const phantomActions = player.actions + const renderAvailablePlayerActions = useCallback( + (info: PlayerInfo, player: Player | PlayerPhantom) => { + let canPlaceArrows: boolean + + if (player.type == "player") { canPlaceArrows = - phantomActions.length == 0 || - phantomActions.findIndex( - (c) => c.type != ActionKind.SHOOT, + player.path == null || + player.actions.findIndex( + (p) => p.type != ActionKind.SHOOT, ) == -1 + } else { + const origin = getOrigin(player, content.components) + const path = origin.path! + // phantoms can only place other arrows if they are the head of the path + canPlaceArrows = + path.items.indexOf(player.id) == path.items.length - 1 + if (canPlaceArrows) { + // and if their only action is to shoot the ball + const phantomActions = player.actions + canPlaceArrows = + phantomActions.length == 0 || + phantomActions.findIndex( + (c) => c.type != ActionKind.SHOOT, + ) == -1 + } } - } + return [ + canPlaceArrows && ( + + ), + (info.ballState === BallState.HOLDS_ORIGIN || + info.ballState === BallState.PASSED_ORIGIN) && ( + { + doMoveBall(ballBounds, player) + }} + /> + ), + ] + }, + [content, doMoveBall, previewAction?.isInvalid, setContent], + ) + + const renderPlayer = useCallback( + (component: Player | PlayerPhantom) => { + let info: PlayerInfo + const isPhantom = component.type == "phantom" + if (isPhantom) { + const origin = getOrigin(component, content.components) + info = { + id: component.id, + team: origin.team, + role: origin.role, + bottomRatio: component.bottomRatio, + rightRatio: component.rightRatio, + ballState: component.ballState, + } + } else { + info = component + } - return [ - canPlaceArrows && ( - + validatePlayerPosition(component, info, newPos) + } + onRemove={() => doRemovePlayer(component)} courtRef={courtRef} - setContent={setContent} + availableActions={() => + renderAvailablePlayerActions(info, component) + } /> - ), - (info.ballState === BallState.HOLDS_ORIGIN || info.ballState === BallState.PASSED_ORIGIN) && ( - { - doMoveBall(ballBounds, player) - }}/> - ), - ] - }, [content, doMoveBall, previewAction?.isInvalid, setContent]) - - const renderPlayer = useCallback((component: Player | PlayerPhantom) => { - let info: PlayerInfo - const isPhantom = component.type == "phantom" - if (isPhantom) { - const origin = getOrigin(component, content.components) - info = { - id: component.id, - team: origin.team, - role: origin.role, - bottomRatio: component.bottomRatio, - rightRatio: component.rightRatio, - ballState: component.ballState, - } - } else { - info = component - } + ) + }, + [ + content.components, + doRemovePlayer, + renderAvailablePlayerActions, + validatePlayerPosition, + ], + ) - return ( - validatePlayerPosition(component, info, newPos)} - onRemove={() => doRemovePlayer(component)} - courtRef={courtRef} - availableActions={() => renderAvailablePlayerActions(info, component)} - /> - ) - }, [content.components, doRemovePlayer, renderAvailablePlayerActions, validatePlayerPosition]) - - const doDeleteAction = useCallback(( - action: Action, - idx: number, - origin: TacticComponent, - ) => { - setContent((content) => removeAction(origin, action, idx, content)) - }, [setContent]) - - const doUpdateAction = useCallback((component: TacticComponent, action: Action, actionIndex: number) => { - setContent((content) => - updateComponent( - { - ...component, - actions: - component.actions.toSpliced( + const doDeleteAction = useCallback( + (action: Action, idx: number, origin: TacticComponent) => { + setContent((content) => removeAction(origin, action, idx, content)) + }, + [setContent], + ) + + const doUpdateAction = useCallback( + (component: TacticComponent, action: Action, actionIndex: number) => { + setContent((content) => + updateComponent( + { + ...component, + actions: component.actions.toSpliced( actionIndex, 1, action, ), - }, - content, - ), - ) - }, [setContent]) - - const renderComponent = useCallback((component: TacticComponent) => { - if ( - component.type == "player" || - component.type == "phantom" - ) { - return renderPlayer(component) - } - if (component.type == BALL_TYPE) { - return ( - { - setContent((content) => - removeBall(content), - ) - setObjects((objects) => [ - ...objects, - {key: "ball"}, - ]) - }} - /> + }, + content, + ), ) - } - throw new Error( - "unknown tactic component " + component, - ) - }, [renderPlayer, doMoveBall, setContent]) + }, + [setContent], + ) - const renderActions = useCallback((component: TacticComponent) => - component.actions.map((action, i) => { - return ( - { - doDeleteAction(action, i, component) - }} - onActionChanges={(action) => - doUpdateAction(component, action, i) - } - /> - ) - }), [doDeleteAction, doUpdateAction]) + const renderComponent = useCallback( + (component: TacticComponent) => { + if (component.type == "player" || component.type == "phantom") { + return renderPlayer(component) + } + if (component.type == BALL_TYPE) { + return ( + { + setContent((content) => removeBall(content)) + setObjects((objects) => [ + ...objects, + { key: "ball" }, + ]) + }} + /> + ) + } + throw new Error("unknown tactic component " + component) + }, + [renderPlayer, doMoveBall, setContent], + ) + + const renderActions = useCallback( + (component: TacticComponent) => + component.actions.map((action, i) => { + return ( + { + doDeleteAction(action, i, component) + }} + onActionChanges={(action) => + doUpdateAction(component, action, i) + } + /> + ) + }), + [doDeleteAction, doUpdateAction], + ) return (
@@ -392,34 +439,48 @@ function EditorView({ Home
- +
{ - onNameChange(new_name).then((success) => { - setTitleStyle(success ? {} : ERROR_STYLE) - }) - }, [onNameChange])} + onValidated={useCallback( + (new_name) => { + onNameChange(new_name).then((success) => { + setTitleStyle(success ? {} : ERROR_STYLE) + }) + }, + [onNameChange], + )} />
-
+
- + - overlaps(courtBounds(), div.getBoundingClientRect()) - , [courtBounds])} - onElementDetached={useCallback((r, e: RackedCourtObject) => + canDetach={useCallback( + (div) => + overlaps( + courtBounds(), + div.getBoundingClientRect(), + ), + [courtBounds], + )} + onElementDetached={useCallback( + (r, e: RackedCourtObject) => setContent((content) => placeObjectAt( r.getBoundingClientRect(), @@ -427,19 +488,25 @@ function EditorView({ e, content, ), - ) - , [courtBounds, setContent])} + ), + [courtBounds, setContent], + )} render={renderCourtObject} /> - +
} + courtImage={} courtRef={courtRef} previewAction={previewAction} renderComponent={renderComponent} @@ -456,23 +523,35 @@ interface PlayerRackProps { id: string objects: RackedPlayer[] setObjects: (state: RackedPlayer[]) => void - setComponents: (f: (components: TacticComponent[]) => TacticComponent[]) => void + setComponents: ( + f: (components: TacticComponent[]) => TacticComponent[], + ) => void courtRef: RefObject } -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 ( - 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 }) => ( + + ), + [], + )} /> ) } @@ -506,17 +589,19 @@ interface CourtPlayerArrowActionProps { } function CourtPlayerArrowAction({ - playerInfo, - player, - isInvalid, - - content, - setContent, - setPreviewAction, - courtRef - }: CourtPlayerArrowActionProps) { - - const courtBounds = useCallback(() => courtRef.current!.getBoundingClientRect(), [courtRef]) + playerInfo, + player, + isInvalid, + + content, + setContent, + setPreviewAction, + courtRef, +}: CourtPlayerArrowActionProps) { + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) return ( { - (document.activeElement as HTMLElement).blur() + ;(document.activeElement as HTMLElement).blur() setPreviewAction({ origin: playerInfo.id, type: getActionKind(null, playerInfo.ballState), - target: ratioWithinBase( - headPos, - courtBounds(), - ), + target: ratioWithinBase(headPos, courtBounds()), segments: [ { next: ratioWithinBase( @@ -564,7 +642,7 @@ function CourtPlayerArrowAction({ ), }, ], - isInvalid: false + isInvalid: false, }) }} onHeadDropped={(headRect) => { @@ -574,27 +652,21 @@ function CourtPlayerArrowAction({ } setContent((content) => { - let {createdAction, newContent} = - createAction( - player, - courtBounds(), - headRect, - content, - ) + let { createdAction, newContent } = createAction( + player, + courtBounds(), + headRect, + content, + ) - if ( - createdAction.type == ActionKind.SHOOT - ) { - const targetIdx = - newContent.components.findIndex( - (c) => - c.id == - createdAction.target, - ) + if (createdAction.type == ActionKind.SHOOT) { + const targetIdx = newContent.components.findIndex( + (c) => c.id == createdAction.target, + ) newContent = dropBallOnComponent( targetIdx, newContent, - false + false, ) newContent = updateComponent( { @@ -607,7 +679,6 @@ function CourtPlayerArrowAction({ ) } - return newContent }) setPreviewAction(null) @@ -620,26 +691,28 @@ function isBallOnCourt(content: TacticContent) { return ( content.components.findIndex( (c) => - (c.type == "player" && (c.ballState === BallState.HOLDS_ORIGIN || c.ballState === BallState.HOLDS_BY_PASS)) || - c.type == BALL_TYPE, + ((c.type === "player" || c.type === "phantom") && + (c.ballState === BallState.HOLDS_ORIGIN || + c.ballState === BallState.PASSED_ORIGIN)) || + c.type === BALL_TYPE, ) != -1 ) } function renderCourtObject(courtObject: RackedCourtObject) { if (courtObject.key == "ball") { - return + return } throw new Error("unknown racked court object " + courtObject.key) } -function Court({courtType}: { courtType: string }) { +function Court({ courtType }: { courtType: string }) { return (
{courtType == "PLAIN" ? ( - + ) : ( - + )}
) diff --git a/front/views/editor/CourtAction.tsx b/front/views/editor/CourtAction.tsx index 986854d..c26c0d9 100644 --- a/front/views/editor/CourtAction.tsx +++ b/front/views/editor/CourtAction.tsx @@ -1,8 +1,8 @@ -import {Action, ActionKind} from "../../model/tactic/Action" +import { Action, ActionKind } from "../../model/tactic/Action" import BendableArrow from "../../components/arrows/BendableArrow" -import {RefObject} from "react" -import {MoveToHead, ScreenHead} from "../../components/actions/ArrowAction" -import {ComponentId} from "../../model/tactic/Tactic" +import { RefObject } from "react" +import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" +import { ComponentId } from "../../model/tactic/Tactic" export interface CourtActionProps { origin: ComponentId @@ -19,9 +19,8 @@ export function CourtAction({ onActionChanges, onActionDeleted, courtRef, - isInvalid + isInvalid, }: CourtActionProps) { - const color = isInvalid ? "red" : "black" let head @@ -32,7 +31,7 @@ export function CourtAction({ head = () => break case ActionKind.SCREEN: - head = () => + head = () => break } @@ -60,7 +59,7 @@ export function CourtAction({ style={{ head, dashArray, - color + color, }} /> )