store actions directly inside each components, enhance bendable arrows to hook to DOM elements

pull/95/head
maxime 1 year ago
parent 852f163e4a
commit 85cd8b0383

@ -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<HTMLDivElement>(null)
return (
<Draggable
@ -15,7 +15,7 @@ export default function BallAction({onDrop}: BallActionProps) {
onStop={() => onDrop(ref.current!.getBoundingClientRect())}
position={NULL_POS}>
<div ref={ref}>
<BallPiece/>
<BallPiece />
</div>
</Draggable>
)

@ -29,7 +29,7 @@ import Draggable from "react-draggable"
export interface BendableArrowProps {
area: RefObject<HTMLElement>
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({
<ArrowPoint
key={i + "-2"}
className={"arrow-point-next"}
posRatio={next}
posRatio={getRatioWithinBase(next, parentBase)}
parentBase={parentBase}
onPosValidated={(next) => {
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
}
/**

@ -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<HTMLDivElement>
}
export interface ActionPreview extends Action {
origin: ComponentId
}
export function BasketCourt({
components,
actions,
previewAction,
renderComponent,
renderAction,
renderActions,
courtImage,
courtRef,
}: BasketCourtProps) {
const [internActions, setInternActions] = useState<Action[]>([])
useLayoutEffect(() => setInternActions(actions), [actions])
return (
<div
className="court-container"
@ -39,13 +44,13 @@ export function BasketCourt({
{courtImage}
{components.map(renderComponent)}
{internActions.map((action, idx) => renderAction(action, idx))}
{components.flatMap(renderActions)}
{previewAction && (
<CourtAction
courtRef={courtRef}
action={previewAction}
origin={previewAction.origin}
//do nothing on interacted, not really possible as it's a preview arrow
onActionDeleted={() => {}}
onActionChanges={() => {}}

@ -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<HTMLDivElement>(null)
const x = ball.rightRatio
@ -27,7 +21,6 @@ export function CourtBall({
onStop={() =>
onPosValidated(pieceRef.current!.getBoundingClientRect())
}
onDrag={onMoves}
position={NULL_POS}
nodeRef={pieceRef}>
<div

@ -1,15 +1,14 @@
import {ReactNode, RefObject, useRef} from "react"
import { ReactNode, RefObject, useRef } from "react"
import "../../style/player.css"
import Draggable from "react-draggable"
import {PlayerPiece} from "./PlayerPiece"
import {BallState, PlayerInfo} from "../../model/tactic/Player"
import {NULL_POS, Pos, ratioWithinBase} from "../../geo/Pos"
import { PlayerPiece } from "./PlayerPiece"
import { BallState, PlayerInfo } from "../../model/tactic/Player"
import { NULL_POS, Pos, ratioWithinBase } from "../../geo/Pos"
export interface CourtPlayerProps {
playerInfo: PlayerInfo
className?: string
onMoves: () => void
onPositionValidated: (newPos: Pos) => void
onRemove: () => void
courtRef: RefObject<HTMLElement>
@ -23,7 +22,6 @@ export default function CourtPlayer({
playerInfo,
className,
onMoves,
onPositionValidated,
onRemove,
courtRef,
@ -38,7 +36,6 @@ export default function CourtPlayer({
<Draggable
handle=".player-piece"
nodeRef={pieceRef}
onDrag={onMoves}
//The piece is positioned using top/bottom style attributes instead
position={NULL_POS}
onStop={() => {

@ -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,
}
}

@ -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)

@ -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 }))
}

@ -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[]
}

@ -45,7 +45,7 @@ export interface PlayerInfo {
export enum BallState {
NONE,
HOLDS,
SHOOTED
SHOOTED,
}
export interface Player extends Component<"player">, PlayerInfo {

@ -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<T> {
* 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[]
}

@ -5,6 +5,7 @@
.arrow-action-icon {
user-select: none;
-moz-user-select: none;
-webkit-user-drag: none;
max-width: 17px;
max-height: 17px;
}

@ -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<CSSProperties>({})
@ -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<RackedCourtObject[]>(
() => isBallOnCourt(content) ? [] : [{key: "ball"}],
const [objects, setObjects] = useState<RackedCourtObject[]>(() =>
isBallOnCourt(content) ? [] : [{ key: "ball" }],
)
const [previewAction, setPreviewAction] = useState<Action | null>(null)
const [previewAction, setPreviewAction] = useState<ActionPreview | null>(
null,
)
const courtRef = useRef<HTMLDivElement>(null)
const setActions = (action: SetStateAction<Action[]>) => {
setContent((c) => ({
...c,
actions: typeof action == "function" ? action(c.actions) : action,
}))
}
const actionsReRenderHooks = []
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
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 && (
<ArrowAction
key={1}
onHeadMoved={(headPos) => {
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 && (
<BallAction
key={2}
onDrop={doMoveBall}
/>
<BallAction key={2} onDrop={doMoveBall} />
),
]}
/>
)
}
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 (
<div id="main-div">
<div id="topbar-div">
@ -353,7 +433,7 @@ function EditorView({
Home
</button>
<div id="topbar-left">
<SavingState state={saveState}/>
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
@ -366,7 +446,7 @@ function EditorView({
}}
/>
</div>
<div id="topbar-right"/>
<div id="topbar-right" />
</div>
<div id="edit-div">
<div id="racks">
@ -387,7 +467,7 @@ function EditorView({
),
])
}
render={({team, key}) => (
render={({ team, key }) => (
<PlayerPiece
team={team}
text={key}
@ -434,7 +514,7 @@ function EditorView({
),
])
}
render={({team, key}) => (
render={({ team, key }) => (
<PlayerPiece
team={team}
text={key}
@ -448,8 +528,7 @@ function EditorView({
<div id="court-div-bounds">
<BasketCourt
components={content.components}
actions={content.actions}
courtImage={<Court courtType={courtType}/>}
courtImage={<Court courtType={courtType} />}
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) => (
<CourtAction
key={i}
action={action}
courtRef={courtRef}
onActionDeleted={() => {
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) => (
<CourtAction
key={"action-" + component.id + "-" + i}
action={action}
origin={component.id}
courtRef={courtRef}
onActionDeleted={() => {
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,
),
}))
}
/>
)}
),
)
}
/>
))
}
/>
</div>
</div>
@ -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 <BallPiece/>
return <BallPiece />
}
throw new Error("unknown racked court object " + courtObject.key)
}
function Court({courtType}: { courtType: string }) {
function Court({ courtType }: { courtType: string }) {
return (
<div id="court-image-div">
{courtType == "PLAIN" ? (
<PlainCourt id="court-image"/>
<PlainCourt id="court-image" />
) : (
<HalfCourt id="court-image"/>
<HalfCourt id="court-image" />
)}
</div>
)

@ -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({
<BendableArrow
forceStraight={action.type == ActionKind.SHOOT}
area={courtRef}
startPos={action.moveFrom}
startPos={origin}
segments={action.segments}
onSegmentsChanges={(edges) => {
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={{

@ -26,7 +26,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
);

@ -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(),
]);
}

Loading…
Cancel
Save