diff --git a/front/components/actions/BallAction.tsx b/front/components/actions/BallAction.tsx index f4af373..87779df 100644 --- a/front/components/actions/BallAction.tsx +++ b/front/components/actions/BallAction.tsx @@ -1,13 +1,13 @@ -import {BallPiece} from "../editor/BallPiece" +import { BallPiece } from "../editor/BallPiece" import Draggable from "react-draggable" -import {useRef} from "react" -import {NULL_POS} from "../../geo/Pos"; +import { useRef } from "react" +import { NULL_POS } from "../../geo/Pos" export interface BallActionProps { onDrop: (el: DOMRect) => void } -export default function BallAction({onDrop}: BallActionProps) { +export default function BallAction({ onDrop }: BallActionProps) { const ref = useRef(null) return ( onDrop(ref.current!.getBoundingClientRect())} position={NULL_POS}>
- +
) diff --git a/front/components/arrows/BendableArrow.tsx b/front/components/arrows/BendableArrow.tsx index e46fb7b..46598f2 100644 --- a/front/components/arrows/BendableArrow.tsx +++ b/front/components/arrows/BendableArrow.tsx @@ -29,7 +29,7 @@ import Draggable from "react-draggable" export interface BendableArrowProps { area: RefObject - startPos: Pos + startPos: Pos | string segments: Segment[] onSegmentsChanges: (edges: Segment[]) => void forceStraight: boolean @@ -55,7 +55,7 @@ const ArrowStyleDefaults: ArrowStyle = { } export interface Segment { - next: Pos + next: Pos | string controlPoint?: Pos } @@ -162,8 +162,8 @@ export default function BendableArrow({ return segments.flatMap(({ next, controlPoint }, i) => { const prev = i == 0 ? startPos : segments[i - 1].next - const prevRelative = posWithinBase(prev, parentBase) - const nextRelative = posWithinBase(next, parentBase) + const prevRelative = getPosWithinBase(prev, parentBase) + const nextRelative = getPosWithinBase(next, parentBase) const cpPos = controlPoint || @@ -204,7 +204,7 @@ export default function BendableArrow({ { const currentSegment = segments[i] @@ -252,19 +252,19 @@ export default function BendableArrow({ const lastSegment = internalSegments[internalSegments.length - 1] - const startRelative = posWithinBase(startPos, parentBase) - const endRelative = posWithinBase(lastSegment.end, parentBase) + const startRelative = getPosWithinBase(startPos, parentBase) + const endRelative = getPosWithinBase(lastSegment.end, parentBase) const startNext = segment.controlPoint && !forceStraight ? posWithinBase(segment.controlPoint, parentBase) - : posWithinBase(segment.end, parentBase) + : getPosWithinBase(segment.end, parentBase) const endPrevious = forceStraight ? startRelative : lastSegment.controlPoint ? posWithinBase(lastSegment.controlPoint, parentBase) - : posWithinBase(lastSegment.start, parentBase) + : getPosWithinBase(lastSegment.start, parentBase) const tailPos = constraintInCircle( startRelative, @@ -313,11 +313,11 @@ export default function BendableArrow({ const svgPosRelativeToBase = { x: left, y: top } const nextRelative = relativeTo( - posWithinBase(end, parentBase), + getPosWithinBase(end, parentBase), svgPosRelativeToBase, ) const startRelative = relativeTo( - posWithinBase(start, parentBase), + getPosWithinBase(start, parentBase), svgPosRelativeToBase, ) const controlPointRelative = @@ -382,6 +382,22 @@ export default function BendableArrow({ // Will update the arrow when the props change useEffect(update, [update]) + useEffect(() => { + const observer = new MutationObserver(update) + const config = { attributes: true } + if (typeof startPos == "string") { + observer.observe(document.getElementById(startPos)!, config) + } + + for (const segment of segments) { + if (typeof segment.next == "string") { + observer.observe(document.getElementById(segment.next)!, config) + } + } + + return () => observer.disconnect() + }, [startPos, segments]) + // Adds a selection handler // Also force an update when the window is resized useEffect(() => { @@ -418,10 +434,16 @@ export default function BendableArrow({ for (let i = 0; i < segments.length; i++) { const segment = segments[i] const beforeSegment = i != 0 ? segments[i - 1] : undefined - const beforeSegmentPos = i > 1 ? segments[i - 2].next : startPos + const beforeSegmentPos = getRatioWithinBase( + i > 1 ? segments[i - 2].next : startPos, + parentBase, + ) - const currentPos = beforeSegment ? beforeSegment.next : startPos - const nextPos = segment.next + const currentPos = getRatioWithinBase( + beforeSegment ? beforeSegment.next : startPos, + parentBase, + ) + const nextPos = getRatioWithinBase(segment.next, parentBase) const segmentCp = segment.controlPoint ? segment.controlPoint : middle(currentPos, nextPos) @@ -529,6 +551,24 @@ export default function BendableArrow({ ) } +function getPosWithinBase(target: Pos | string, area: DOMRect): Pos { + if (typeof target != "string") { + return posWithinBase(target, area) + } + + const targetPos = document.getElementById(target)!.getBoundingClientRect() + return relativeTo(middlePos(targetPos), area) +} + +function getRatioWithinBase(target: Pos | string, area: DOMRect): Pos { + if (typeof target != "string") { + return target + } + + const targetPos = document.getElementById(target)!.getBoundingClientRect() + return ratioWithinBase(middlePos(targetPos), area) +} + interface ControlPointProps { className: string posRatio: Pos @@ -546,9 +586,9 @@ enum PointSegmentSearchResult { } interface FullSegment { - start: Pos + start: Pos | string controlPoint: Pos | null - end: Pos + end: Pos | string } /** diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index f684e1b..5815f7a 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,36 +1,41 @@ -import {ReactElement, ReactNode, RefObject, useLayoutEffect, useState,} from "react" -import {Action} from "../../model/tactic/Action" - -import {CourtAction} from "../../views/editor/CourtAction" -import {TacticComponent} from "../../model/tactic/Tactic" +import { + ReactElement, + ReactNode, + RefObject, + useEffect, + useLayoutEffect, + useState, +} from "react" +import { Action } from "../../model/tactic/Action" + +import { CourtAction } from "../../views/editor/CourtAction" +import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" export interface BasketCourtProps { components: TacticComponent[] - actions: Action[] - previewAction: Action | null + previewAction: ActionPreview | null renderComponent: (comp: TacticComponent) => ReactNode - renderAction: (action: Action, idx: number) => ReactNode + renderActions: (comp: TacticComponent) => ReactNode[] courtImage: ReactElement courtRef: RefObject } +export interface ActionPreview extends Action { + origin: ComponentId +} + export function BasketCourt({ components, - actions, previewAction, renderComponent, - renderAction, + renderActions, courtImage, courtRef, }: BasketCourtProps) { - const [internActions, setInternActions] = useState([]) - - useLayoutEffect(() => setInternActions(actions), [actions]) - return (
renderAction(action, idx))} + {components.flatMap(renderActions)} {previewAction && ( {}} onActionChanges={() => {}} diff --git a/front/components/editor/CourtBall.tsx b/front/components/editor/CourtBall.tsx index 53ae408..b167126 100644 --- a/front/components/editor/CourtBall.tsx +++ b/front/components/editor/CourtBall.tsx @@ -6,17 +6,11 @@ import { Ball } from "../../model/tactic/CourtObjects" export interface CourtBallProps { onPosValidated: (rect: DOMRect) => void - onMoves: () => void onRemove: () => void ball: Ball } -export function CourtBall({ - onPosValidated, - ball, - onRemove, - onMoves, -}: CourtBallProps) { +export function CourtBall({ onPosValidated, ball, onRemove }: CourtBallProps) { const pieceRef = useRef(null) const x = ball.rightRatio @@ -27,7 +21,6 @@ export function CourtBall({ onStop={() => onPosValidated(pieceRef.current!.getBoundingClientRect()) } - onDrag={onMoves} position={NULL_POS} nodeRef={pieceRef}>
void onPositionValidated: (newPos: Pos) => void onRemove: () => void courtRef: RefObject @@ -23,7 +22,6 @@ export default function CourtPlayer({ playerInfo, className, - onMoves, onPositionValidated, onRemove, courtRef, @@ -38,7 +36,6 @@ export default function CourtPlayer({ { diff --git a/front/editor/ActionsDomains.ts b/front/editor/ActionsDomains.ts index 1179dc7..54b6198 100644 --- a/front/editor/ActionsDomains.ts +++ b/front/editor/ActionsDomains.ts @@ -1,20 +1,24 @@ -import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" -import {middlePos, ratioWithinBase} from "../geo/Pos" -import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" -import {overlaps} from "../geo/Box" -import {Action, ActionKind} from "../model/tactic/Action" -import {removeBall, updateComponent} from "./TacticContentDomains" -import {getOrigin} from "./PlayerDomains" - -export function refreshAllActions( - actions: Action[], - components: TacticComponent[], -) { - return actions.map((action) => ({ - ...action, - type: getActionKindFrom(action.fromId, action.toId, components), - })) -} +import { BallState, Player, PlayerPhantom } from "../model/tactic/Player" +import { middlePos, ratioWithinBase } from "../geo/Pos" +import { + ComponentId, + TacticComponent, + TacticContent, +} from "../model/tactic/Tactic" +import { overlaps } from "../geo/Box" +import { Action, ActionKind } from "../model/tactic/Action" +import { removeBall, updateComponent } from "./TacticContentDomains" +import { getOrigin } from "./PlayerDomains" + +// export function refreshAllActions( +// actions: Action[], +// components: TacticComponent[], +// ) { +// return actions.map((action) => ({ +// ...action, +// type: getActionKindFrom(action.fromId, action.toId, components), +// })) +// } export function getActionKindFrom( originId: ComponentId, @@ -22,7 +26,7 @@ export function getActionKindFrom( components: TacticComponent[], ): ActionKind { const origin = components.find((p) => p.id == originId)! - const target = components.find(p => p.id == targetId) + const target = components.find((p) => p.id == targetId) let ballState = BallState.NONE @@ -30,12 +34,17 @@ export function getActionKindFrom( ballState = origin.ballState } - let hasTarget = target ? (target.type != 'phantom' || target.originPlayerId != origin.id) : false + let hasTarget = target + ? target.type != "phantom" || target.originPlayerId != origin.id + : false return getActionKind(hasTarget, ballState) } -export function getActionKind(hasTarget: boolean, ballState: BallState): ActionKind { +export function getActionKind( + hasTarget: boolean, + ballState: BallState, +): ActionKind { switch (ballState) { case BallState.HOLDS: return hasTarget ? ActionKind.SHOOT : ActionKind.DRIBBLE @@ -51,20 +60,14 @@ export function placeArrow( courtBounds: DOMRect, arrowHead: DOMRect, content: TacticContent, -): { createdAction: Action, newContent: TacticContent } { - const originRef = document.getElementById(origin.id)! - const start = ratioWithinBase( - middlePos(originRef.getBoundingClientRect()), - courtBounds, - ) - +): { createdAction: Action; newContent: TacticContent } { /** * Creates a new phantom component. * Be aware that this function will reassign the `content` parameter. * @param receivesBall */ function createPhantom(receivesBall: boolean): ComponentId { - const {x, y} = ratioWithinBase(arrowHead, courtBounds) + const { x, y } = ratioWithinBase(arrowHead, courtBounds) let itemIndex: number let originPlayer: Player @@ -99,16 +102,17 @@ export function placeArrow( const ballState = receivesBall ? BallState.HOLDS : origin.ballState == BallState.HOLDS - ? BallState.HOLDS - : BallState.NONE + ? BallState.HOLDS + : BallState.NONE const phantom: PlayerPhantom = { + actions: [], type: "phantom", id: phantomId, rightRatio: x, bottomRatio: y, originPlayerId: originPlayer.id, - ballState + ballState, } content = { ...content, @@ -127,12 +131,6 @@ export function placeArrow( .getBoundingClientRect() if (overlaps(componentBounds, arrowHead)) { - const targetPos = document - .getElementById(component.id)! - .getBoundingClientRect() - - const end = ratioWithinBase(middlePos(targetPos), courtBounds) - let toId = component.id if (component.type == "ball") { @@ -141,19 +139,20 @@ export function placeArrow( } const action: Action = { - fromId: originRef.id, - toId, + target: toId, type: getActionKind(true, origin.ballState), - moveFrom: start, - segments: [{next: end}], + segments: [{ next: component.id }], } return { - newContent: { - ...content, - actions: [...content.actions, action], - }, - createdAction: action + newContent: updateComponent( + { + ...origin, + actions: [...origin.actions, action], + }, + content, + ), + createdAction: action, } } } @@ -161,54 +160,37 @@ export function placeArrow( const phantomId = createPhantom(origin.ballState == BallState.HOLDS) const action: Action = { - fromId: originRef.id, - toId: phantomId, + target: phantomId, type: getActionKind(false, origin.ballState), - moveFrom: ratioWithinBase( - middlePos(originRef.getBoundingClientRect()), - courtBounds, - ), - segments: [ - {next: ratioWithinBase(middlePos(arrowHead), courtBounds)}, - ], + segments: [{ next: phantomId }], } return { - newContent: { - ...content, - actions: [...content.actions, action], - }, - createdAction: action + newContent: updateComponent( + { + ...content.components.find((c) => c.id == origin.id)!, + actions: [...origin.actions, action], + }, + content, + ), + createdAction: action, } } -export function repositionActionsRelatedTo( - compId: ComponentId, - courtBounds: DOMRect, - actions: Action[], -): Action[] { - const posRect = document.getElementById(compId)?.getBoundingClientRect() - const newPos = posRect != undefined - ? ratioWithinBase(middlePos(posRect), courtBounds) - : undefined - - return actions.flatMap((action) => { - if (newPos == undefined) { - return [] - } - - if (action.fromId == compId) { - return [{...action, moveFrom: newPos}] - } - - if (action.toId == compId) { - const lastIdx = action.segments.length - 1 - const segments = action.segments.toSpliced(lastIdx, 1, { - ...action.segments[lastIdx], - next: newPos!, - }) - return [{...action, segments}] - } +export function removeAllActionsTargeting( + componentId: ComponentId, + content: TacticContent, +): TacticContent { + let components = [] + for (let i = 0; i < content.components.length; i++) { + const component = content.components[i] + components.push({ + ...component, + actions: component.actions.filter((a) => a.target != componentId), + }) + } - return action - }) + return { + ...content, + components, + } } diff --git a/front/editor/PlayerDomains.ts b/front/editor/PlayerDomains.ts index 9ef1d45..b7c69df 100644 --- a/front/editor/PlayerDomains.ts +++ b/front/editor/PlayerDomains.ts @@ -1,6 +1,7 @@ import { Player, PlayerPhantom } from "../model/tactic/Player" import { TacticComponent, TacticContent } from "../model/tactic/Tactic" import { removeComponent, updateComponent } from "./TacticContentDomains" +import { removeAllActionsTargeting } from "./ActionsDomains" export function getOrigin( pathItem: PlayerPhantom, @@ -34,6 +35,8 @@ export function removePlayer( player: Player | PlayerPhantom, content: TacticContent, ): TacticContent { + content = removeAllActionsTargeting(player.id, content) + if (player.type == "phantom") { const origin = getOrigin(player, content.components) return truncatePlayerPath(origin, player, content) @@ -54,10 +57,10 @@ export function truncatePlayerPath( let truncateStartIdx = -1 - for (let j = 0; j < path.items.length; j++) { - const pathPhantomId = path.items[j] + for (let i = 0; i < path.items.length; i++) { + const pathPhantomId = path.items[i] if (truncateStartIdx != -1 || pathPhantomId == phantom.id) { - if (truncateStartIdx == -1) truncateStartIdx = j + if (truncateStartIdx == -1) truncateStartIdx = i //remove the phantom from the tactic content = removeComponent(pathPhantomId, content) diff --git a/front/editor/TacticContentDomains.ts b/front/editor/TacticContentDomains.ts index bec65bc..d0a24ba 100644 --- a/front/editor/TacticContentDomains.ts +++ b/front/editor/TacticContentDomains.ts @@ -1,18 +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 {refreshAllActions} from "./ActionsDomains" -import {getOrigin} 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 { getOrigin } 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", @@ -23,6 +36,7 @@ export function placePlayerAt( bottomRatio: y, ballState: BallState.NONE, path: null, + actions: [], } } @@ -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 @@ -52,6 +66,7 @@ export function placeObjectAt( id: BALL_ID, rightRatio: x, bottomRatio: y, + actions: [], } break @@ -75,10 +90,10 @@ export function dropBallOnComponent( let origin let isPhantom: boolean - if (component.type == 'phantom') { + if (component.type == "phantom") { isPhantom = true origin = getOrigin(component, components) - } else if (component.type == 'player') { + } else if (component.type == "player") { isPhantom = false origin = component } else { @@ -91,11 +106,17 @@ export function dropBallOnComponent( }) if (origin.path != null) { const phantoms = origin.path!.items - const headingPhantoms = isPhantom ? phantoms.slice(phantoms.indexOf(component.id)) : phantoms - components = components.map(c => headingPhantoms.indexOf(c.id) != -1 ? { - ...c, - hasBall: true - } : c) + const headingPhantoms = isPhantom + ? phantoms.slice(phantoms.indexOf(component.id)) + : phantoms + components = components.map((c) => + headingPhantoms.indexOf(c.id) != -1 + ? { + ...c, + hasBall: true, + } + : c, + ) } const ballObj = components.findIndex((p) => p.type == BALL_TYPE) @@ -109,7 +130,6 @@ export function dropBallOnComponent( } return { ...content, - actions: refreshAllActions(content.actions, components), components, } } @@ -118,11 +138,11 @@ export function removeBall(content: TacticContent): TacticContent { const ballObj = content.components.findIndex((o) => o.type == "ball") const components = content.components.map((c) => - (c.type == 'player' || c.type == 'phantom') + c.type == "player" || c.type == "phantom" ? { - ...c, - hasBall: false, - } + ...c, + hasBall: false, + } : c, ) @@ -133,7 +153,6 @@ export function removeBall(content: TacticContent): TacticContent { return { ...content, - actions: refreshAllActions(content.actions, components), components, } } @@ -147,7 +166,7 @@ export function placeBallAt( removed: boolean } { if (!overlaps(courtBounds, refBounds)) { - return {newContent: removeBall(content), removed: true} + return { newContent: removeBall(content), removed: true } } const playerCollidedIdx = getComponentCollided( refBounds, @@ -159,11 +178,11 @@ export function placeBallAt( newContent: dropBallOnComponent(playerCollidedIdx, { ...content, components: content.components.map((c) => - c.type == "player" || c.type == 'phantom' + c.type == "player" || c.type == "phantom" ? { - ...c, - hasBall: false, - } + ...c, + hasBall: false, + } : c, ), }), @@ -173,14 +192,14 @@ 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 components = content.components.map((c) => c.type == "player" || c.type == "phantom" ? { - ...c, - hasBall: false, - } + ...c, + hasBall: false, + } : c, ) @@ -189,6 +208,7 @@ export function placeBallAt( id: BALL_ID, rightRatio: x, bottomRatio: y, + actions: [], } if (ballIdx != -1) { components.splice(ballIdx, 1, ball) @@ -199,7 +219,6 @@ export function placeBallAt( return { newContent: { ...content, - actions: refreshAllActions(content.actions, components), components, }, removed: false, @@ -243,9 +262,6 @@ export function removeComponent( return { ...content, components: content.components.toSpliced(componentIdx, 1), - actions: content.actions.filter( - (a) => a.toId !== componentId && a.fromId !== componentId, - ), } } @@ -295,5 +311,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 f22dfaf..be5b155 100644 --- a/front/model/tactic/Action.ts +++ b/front/model/tactic/Action.ts @@ -12,8 +12,7 @@ export enum ActionKind { export type Action = { type: ActionKind } & MovementAction export interface MovementAction { - fromId: ComponentId - toId: ComponentId | null - moveFrom: Pos + // fromId: ComponentId + target: ComponentId | Pos segments: Segment[] } diff --git a/front/model/tactic/Player.ts b/front/model/tactic/Player.ts index 7df59ec..41738d3 100644 --- a/front/model/tactic/Player.ts +++ b/front/model/tactic/Player.ts @@ -45,7 +45,7 @@ export interface PlayerInfo { export enum BallState { NONE, HOLDS, - SHOOTED + SHOOTED, } export interface Player extends Component<"player">, PlayerInfo { diff --git a/front/model/tactic/Tactic.ts b/front/model/tactic/Tactic.ts index c641ac4..dfe1190 100644 --- a/front/model/tactic/Tactic.ts +++ b/front/model/tactic/Tactic.ts @@ -10,7 +10,7 @@ export interface Tactic { export interface TacticContent { components: TacticComponent[] - actions: Action[] + //actions: Action[] } export type TacticComponent = Player | CourtObject | PlayerPhantom @@ -34,4 +34,6 @@ export interface Component { * Percentage of the component's position to the right (0 means left, 1 means right, 0.5 means middle) */ readonly rightRatio: number + + readonly actions: Action[] } diff --git a/front/style/actions/arrow_action.css b/front/style/actions/arrow_action.css index 3aa88d7..77bfa4c 100644 --- a/front/style/actions/arrow_action.css +++ b/front/style/actions/arrow_action.css @@ -5,6 +5,7 @@ .arrow-action-icon { user-select: none; -moz-user-select: none; + -webkit-user-drag: none; max-width: 17px; max-height: 17px; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 5163bcc..7a2321b 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,23 +1,34 @@ -import {CSSProperties, Dispatch, SetStateAction, useCallback, useMemo, useRef, useState,} from "react" +import { + CSSProperties, + Dispatch, + SetStateAction, + useCallback, + useMemo, + useRef, + useState, +} from "react" import "../style/editor.css" 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 {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, @@ -26,19 +37,30 @@ import { placeBallAt, placeObjectAt, placePlayerAt, - removeBall, updateComponent, + removeBall, + updateComponent, } from "../editor/TacticContentDomains" -import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player" -import {RackedCourtObject} from "../editor/RackedItems" +import { + BallState, + Player, + PlayerInfo, + PlayerPhantom, + PlayerTeam, +} from "../model/tactic/Player" +import { RackedCourtObject } from "../editor/RackedItems" import CourtPlayer from "../components/editor/CourtPlayer" -import {getActionKind, placeArrow, repositionActionsRelatedTo,} from "../editor/ActionsDomains" +import { getActionKind, placeArrow } from "../editor/ActionsDomains" import ArrowAction from "../components/actions/ArrowAction" -import {middlePos, ratioWithinBase} from "../geo/Pos" -import {Action, ActionKind} from "../model/tactic/Action" +import { middlePos, ratioWithinBase } from "../geo/Pos" +import { Action, ActionKind } from "../model/tactic/Action" import BallAction from "../components/actions/BallAction" -import {getOrigin, removePlayer, truncatePlayerPath,} from "../editor/PlayerDomains" -import {CourtBall} from "../components/editor/CourtBall" -import {BASE} from "../Constants" +import { + getOrigin, + removePlayer, + truncatePlayerPath, +} from "../editor/PlayerDomains" +import { CourtBall } from "../components/editor/CourtBall" +import { BASE } from "../Constants" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -61,7 +83,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) @@ -87,7 +109,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, ) }} @@ -96,7 +118,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, ) }} @@ -106,11 +128,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({}) @@ -130,27 +152,24 @@ function EditorView({ ), ) - const [allies, setAllies] = useState( - () => getRackPlayers(PlayerTeam.Allies, content.components), + const [allies, setAllies] = useState(() => + getRackPlayers(PlayerTeam.Allies, content.components), ) - const [opponents, setOpponents] = useState( - () => getRackPlayers(PlayerTeam.Opponents, content.components), + const [opponents, setOpponents] = useState(() => + getRackPlayers(PlayerTeam.Opponents, content.components), ) - const [objects, setObjects] = useState( - () => isBallOnCourt(content) ? [] : [{key: "ball"}], + const [objects, setObjects] = useState(() => + isBallOnCourt(content) ? [] : [{ key: "ball" }], ) - const [previewAction, setPreviewAction] = useState(null) + const [previewAction, setPreviewAction] = useState( + null, + ) const courtRef = useRef(null) - const setActions = (action: SetStateAction) => { - setContent((c) => ({ - ...c, - actions: typeof action == "function" ? action(c.actions) : action, - })) - } + const actionsReRenderHooks = [] const setComponents = (action: SetStateAction) => { setContent((c) => ({ @@ -170,7 +189,7 @@ function EditorView({ setter = setAllies } if (player.ballState == BallState.HOLDS) { - setObjects([{key: "ball"}]) + setObjects([{ key: "ball" }]) } setter((players) => [ ...players, @@ -184,14 +203,14 @@ function EditorView({ const doMoveBall = (newBounds: DOMRect) => { setContent((content) => { - const {newContent, removed} = placeBallAt( + const { newContent, removed } = placeBallAt( newBounds, courtBounds(), content, ) if (removed) { - setObjects((objects) => [...objects, {key: "ball"}]) + setObjects((objects) => [...objects, { key: "ball" }]) } return newContent @@ -209,13 +228,18 @@ function EditorView({ const origin = getOrigin(component, content.components) const path = origin.path! // phantoms can only place other arrows if they are the head of the path - canPlaceArrows = path.items.indexOf(component.id) == path.items.length - 1 + canPlaceArrows = + path.items.indexOf(component.id) == path.items.length - 1 if (canPlaceArrows) { // and if their only action is to shoot the ball // list the actions the phantoms does - const phantomArrows = content.actions.filter(c => c.fromId == component.id) - canPlaceArrows = phantomArrows.length == 0 || phantomArrows.findIndex(c => c.type != ActionKind.SHOOT) == -1 + const phantomActions = component.actions + canPlaceArrows = + phantomActions.length == 0 || + phantomActions.findIndex( + (c) => c.type != ActionKind.SHOOT, + ) == -1 } info = { @@ -230,7 +254,11 @@ function EditorView({ // a player info = component // can place arrows only if the - canPlaceArrows = component.path == null || content.actions.findIndex(p => p.fromId == component.id && p.type != ActionKind.SHOOT) == -1 + canPlaceArrows = + component.path == null || + component.actions.findIndex( + (p) => p.type != ActionKind.SHOOT, + ) == -1 } return ( @@ -238,11 +266,6 @@ function EditorView({ key={component.id} className={isPhantom ? "phantom" : "player"} playerInfo={info} - onMoves={() => - setActions((actions) => - repositionActionsRelatedTo(info.id, courtBounds(), actions), - ) - } onPositionValidated={(newPos) => { setContent((content) => moveComponent( @@ -264,13 +287,16 @@ function EditorView({ if (!isPhantom) insertRackedPlayer(component) }} courtRef={courtRef} - availableActions={(pieceRef) => [ + availableActions={() => [ canPlaceArrows && ( { const arrowHeadPos = middlePos(headPos) - const targetIdx = getComponentCollided(headPos, content.components) + const targetIdx = getComponentCollided( + headPos, + content.components, + ) setPreviewAction((action) => ({ ...action!, @@ -282,20 +308,20 @@ function EditorView({ ), }, ], - type: getActionKind(targetIdx != -1, info.ballState), + type: getActionKind( + targetIdx != -1, + info.ballState, + ), })) }} onHeadPicked={(headPos) => { - (document.activeElement as HTMLElement).blur() + ;(document.activeElement as HTMLElement).blur() setPreviewAction({ + origin: component.id, type: getActionKind(false, info.ballState), - fromId: info.id, - toId: null, - moveFrom: ratioWithinBase( - middlePos( - pieceRef.getBoundingClientRect(), - ), + target: ratioWithinBase( + headPos, courtBounds(), ), segments: [ @@ -310,25 +336,41 @@ function EditorView({ }} onHeadDropped={(headRect) => { setContent((content) => { - let {createdAction, newContent} = placeArrow( - component, - courtBounds(), - headRect, - content, - ) + let { createdAction, newContent } = + placeArrow( + component, + courtBounds(), + headRect, + content, + ) let originNewBallState = component.ballState - if (createdAction.type == ActionKind.SHOOT) { - const targetIdx = newContent.components.findIndex(c => c.id == createdAction.toId) - newContent = dropBallOnComponent(targetIdx, newContent) + if ( + createdAction.type == ActionKind.SHOOT + ) { + const targetIdx = + newContent.components.findIndex( + (c) => + c.id == + createdAction.target, + ) + newContent = dropBallOnComponent( + targetIdx, + newContent, + ) originNewBallState = BallState.SHOOTED } - newContent = updateComponent({ - ...(newContent.components.find(c => c.id == component.id)! as Player | PlayerPhantom), - ballState: originNewBallState - }, newContent) + newContent = updateComponent( + { + ...(newContent.components.find( + (c) => c.id == component.id, + )! as Player | PlayerPhantom), + ballState: originNewBallState, + }, + newContent, + ) return newContent }) setPreviewAction(null) @@ -336,16 +378,54 @@ function EditorView({ /> ), info.ballState != BallState.NONE && ( - + ), ]} /> ) } + const doDeleteAction = ( + action: Action, + idx: number, + component: TacticComponent, + ) => { + setContent((content) => { + content = updateComponent( + { + ...component, + actions: component.actions.toSpliced(idx, 1), + }, + content, + ) + + if (action.target == null) return content + + const target = content.components.find( + (c) => action.target == c.id, + )! + + if (target.type == "phantom") { + let path = null + if (component.type == "player") { + path = component.path + } else if (component.type == "phantom") { + path = getOrigin(component, content.components).path + } + + if ( + path == null || + path.items.find((c) => c == target.id) == null + ) { + return content + } + content = removePlayer(target, content) + } + + return content + }) + } + return (
@@ -353,7 +433,7 @@ function EditorView({ Home
- +
-
+
@@ -387,7 +467,7 @@ function EditorView({ ), ]) } - render={({team, key}) => ( + render={({ team, key }) => ( ( + render={({ team, key }) => ( } + courtImage={} courtRef={courtRef} previewAction={previewAction} renderComponent={(component) => { @@ -465,20 +544,14 @@ function EditorView({ key="ball" ball={component} onPosValidated={doMoveBall} - onMoves={() => - setActions((actions) => - repositionActionsRelatedTo( - component.id, - courtBounds(), - actions, - ), - ) - } onRemove={() => { setContent((content) => removeBall(content), ) - setObjects(objects => [...objects, {key: "ball"}]) + setObjects((objects) => [ + ...objects, + { key: "ball" }, + ]) }} /> ) @@ -487,60 +560,35 @@ function EditorView({ "unknown tactic component " + component, ) }} - renderAction={(action, i) => ( - { - setContent((content) => { - content = { - ...content, - actions: - content.actions.toSpliced( - i, - 1, - ), - } - - if (action.toId == null) - return content - - const target = - content.components.find( - (c) => action.toId == c.id, - )! - - if (target.type == "phantom") { - const origin = getOrigin( - target, - content.components, - ) - if (origin.id != action.fromId) { - return content - } - content = truncatePlayerPath( - origin, - target, + renderActions={(component) => + component.actions.map((action, i) => ( + { + doDeleteAction(action, i, component) + }} + onActionChanges={(a) => + setContent((content) => + updateComponent( + { + ...component, + actions: + component.actions.toSpliced( + i, + 1, + a, + ), + }, content, - ) - } - - return content - }) - }} - onActionChanges={(a) => - setContent((content) => ({ - ...content, - actions: content.actions.toSpliced( - i, - 1, - a, - ), - })) - } - /> - )} + ), + ) + } + /> + )) + } />
@@ -552,25 +600,27 @@ function EditorView({ function isBallOnCourt(content: TacticContent) { return ( content.components.findIndex( - (c) => (c.type == "player" && c.ballState == BallState.HOLDS) || c.type == BALL_TYPE, + (c) => + (c.type == "player" && c.ballState == BallState.HOLDS) || + 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 22a4147..e4f5fa9 100644 --- a/front/views/editor/CourtAction.tsx +++ b/front/views/editor/CourtAction.tsx @@ -2,8 +2,11 @@ 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 { middlePos, Pos, ratioWithinBase } from "../../geo/Pos" export interface CourtActionProps { + origin: ComponentId action: Action onActionChanges: (a: Action) => void onActionDeleted: () => void @@ -11,6 +14,7 @@ export interface CourtActionProps { } export function CourtAction({ + origin, action, onActionChanges, onActionDeleted, @@ -39,14 +43,14 @@ export function CourtAction({ { onActionChanges({ ...action, segments: edges }) }} wavy={action.type == ActionKind.DRIBBLE} //TODO place those magic values in constants - endRadius={action.toId ? 26 : 17} + endRadius={action.target ? 26 : 17} startRadius={10} onDeleteRequested={onActionDeleted} style={{ diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index a8ce6bd..4ae6347 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -20,7 +20,7 @@ CREATE TABLE Tactic name varchar NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, owner integer NOT NULL, - content varchar DEFAULT '{"components": [], "actions": []}' NOT NULL, + content varchar DEFAULT '{"components": []}' NOT NULL, court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL, FOREIGN KEY (owner) REFERENCES Account ); diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index b6ebd6e..ec21324 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -42,7 +42,7 @@ class EditorController { return ViewHttpResponse::react("views/Editor.tsx", [ "id" => -1, //-1 id means that the editor will not support saves "name" => TacticModel::TACTIC_DEFAULT_NAME, - "content" => '{"components": [], "actions": []}', + "content" => '{"components": []}', "courtType" => $courtType->name(), ]); }