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 Draggable from "react-draggable"
import {useRef} from "react" import { useRef } from "react"
import {NULL_POS} from "../../geo/Pos"; import { NULL_POS } from "../../geo/Pos"
export interface BallActionProps { export interface BallActionProps {
onDrop: (el: DOMRect) => void onDrop: (el: DOMRect) => void
} }
export default function BallAction({onDrop}: BallActionProps) { export default function BallAction({ onDrop }: BallActionProps) {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
return ( return (
<Draggable <Draggable
@ -15,7 +15,7 @@ export default function BallAction({onDrop}: BallActionProps) {
onStop={() => onDrop(ref.current!.getBoundingClientRect())} onStop={() => onDrop(ref.current!.getBoundingClientRect())}
position={NULL_POS}> position={NULL_POS}>
<div ref={ref}> <div ref={ref}>
<BallPiece/> <BallPiece />
</div> </div>
</Draggable> </Draggable>
) )

@ -29,7 +29,7 @@ import Draggable from "react-draggable"
export interface BendableArrowProps { export interface BendableArrowProps {
area: RefObject<HTMLElement> area: RefObject<HTMLElement>
startPos: Pos startPos: Pos | string
segments: Segment[] segments: Segment[]
onSegmentsChanges: (edges: Segment[]) => void onSegmentsChanges: (edges: Segment[]) => void
forceStraight: boolean forceStraight: boolean
@ -55,7 +55,7 @@ const ArrowStyleDefaults: ArrowStyle = {
} }
export interface Segment { export interface Segment {
next: Pos next: Pos | string
controlPoint?: Pos controlPoint?: Pos
} }
@ -162,8 +162,8 @@ export default function BendableArrow({
return segments.flatMap(({ next, controlPoint }, i) => { return segments.flatMap(({ next, controlPoint }, i) => {
const prev = i == 0 ? startPos : segments[i - 1].next const prev = i == 0 ? startPos : segments[i - 1].next
const prevRelative = posWithinBase(prev, parentBase) const prevRelative = getPosWithinBase(prev, parentBase)
const nextRelative = posWithinBase(next, parentBase) const nextRelative = getPosWithinBase(next, parentBase)
const cpPos = const cpPos =
controlPoint || controlPoint ||
@ -204,7 +204,7 @@ export default function BendableArrow({
<ArrowPoint <ArrowPoint
key={i + "-2"} key={i + "-2"}
className={"arrow-point-next"} className={"arrow-point-next"}
posRatio={next} posRatio={getRatioWithinBase(next, parentBase)}
parentBase={parentBase} parentBase={parentBase}
onPosValidated={(next) => { onPosValidated={(next) => {
const currentSegment = segments[i] const currentSegment = segments[i]
@ -252,19 +252,19 @@ export default function BendableArrow({
const lastSegment = internalSegments[internalSegments.length - 1] const lastSegment = internalSegments[internalSegments.length - 1]
const startRelative = posWithinBase(startPos, parentBase) const startRelative = getPosWithinBase(startPos, parentBase)
const endRelative = posWithinBase(lastSegment.end, parentBase) const endRelative = getPosWithinBase(lastSegment.end, parentBase)
const startNext = const startNext =
segment.controlPoint && !forceStraight segment.controlPoint && !forceStraight
? posWithinBase(segment.controlPoint, parentBase) ? posWithinBase(segment.controlPoint, parentBase)
: posWithinBase(segment.end, parentBase) : getPosWithinBase(segment.end, parentBase)
const endPrevious = forceStraight const endPrevious = forceStraight
? startRelative ? startRelative
: lastSegment.controlPoint : lastSegment.controlPoint
? posWithinBase(lastSegment.controlPoint, parentBase) ? posWithinBase(lastSegment.controlPoint, parentBase)
: posWithinBase(lastSegment.start, parentBase) : getPosWithinBase(lastSegment.start, parentBase)
const tailPos = constraintInCircle( const tailPos = constraintInCircle(
startRelative, startRelative,
@ -313,11 +313,11 @@ export default function BendableArrow({
const svgPosRelativeToBase = { x: left, y: top } const svgPosRelativeToBase = { x: left, y: top }
const nextRelative = relativeTo( const nextRelative = relativeTo(
posWithinBase(end, parentBase), getPosWithinBase(end, parentBase),
svgPosRelativeToBase, svgPosRelativeToBase,
) )
const startRelative = relativeTo( const startRelative = relativeTo(
posWithinBase(start, parentBase), getPosWithinBase(start, parentBase),
svgPosRelativeToBase, svgPosRelativeToBase,
) )
const controlPointRelative = const controlPointRelative =
@ -382,6 +382,22 @@ export default function BendableArrow({
// Will update the arrow when the props change // Will update the arrow when the props change
useEffect(update, [update]) 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 // Adds a selection handler
// Also force an update when the window is resized // Also force an update when the window is resized
useEffect(() => { useEffect(() => {
@ -418,10 +434,16 @@ export default function BendableArrow({
for (let i = 0; i < segments.length; i++) { for (let i = 0; i < segments.length; i++) {
const segment = segments[i] const segment = segments[i]
const beforeSegment = i != 0 ? segments[i - 1] : undefined 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 currentPos = getRatioWithinBase(
const nextPos = segment.next beforeSegment ? beforeSegment.next : startPos,
parentBase,
)
const nextPos = getRatioWithinBase(segment.next, parentBase)
const segmentCp = segment.controlPoint const segmentCp = segment.controlPoint
? segment.controlPoint ? segment.controlPoint
: middle(currentPos, nextPos) : 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 { interface ControlPointProps {
className: string className: string
posRatio: Pos posRatio: Pos
@ -546,9 +586,9 @@ enum PointSegmentSearchResult {
} }
interface FullSegment { interface FullSegment {
start: Pos start: Pos | string
controlPoint: Pos | null controlPoint: Pos | null
end: Pos end: Pos | string
} }
/** /**

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

@ -6,17 +6,11 @@ import { Ball } from "../../model/tactic/CourtObjects"
export interface CourtBallProps { export interface CourtBallProps {
onPosValidated: (rect: DOMRect) => void onPosValidated: (rect: DOMRect) => void
onMoves: () => void
onRemove: () => void onRemove: () => void
ball: Ball ball: Ball
} }
export function CourtBall({ export function CourtBall({ onPosValidated, ball, onRemove }: CourtBallProps) {
onPosValidated,
ball,
onRemove,
onMoves,
}: CourtBallProps) {
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
const x = ball.rightRatio const x = ball.rightRatio
@ -27,7 +21,6 @@ export function CourtBall({
onStop={() => onStop={() =>
onPosValidated(pieceRef.current!.getBoundingClientRect()) onPosValidated(pieceRef.current!.getBoundingClientRect())
} }
onDrag={onMoves}
position={NULL_POS} position={NULL_POS}
nodeRef={pieceRef}> nodeRef={pieceRef}>
<div <div

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

@ -1,20 +1,24 @@
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player" import { BallState, Player, PlayerPhantom } from "../model/tactic/Player"
import {middlePos, ratioWithinBase} from "../geo/Pos" import { middlePos, ratioWithinBase } from "../geo/Pos"
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" import {
import {overlaps} from "../geo/Box" ComponentId,
import {Action, ActionKind} from "../model/tactic/Action" TacticComponent,
import {removeBall, updateComponent} from "./TacticContentDomains" TacticContent,
import {getOrigin} from "./PlayerDomains" } from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
export function refreshAllActions( import { Action, ActionKind } from "../model/tactic/Action"
actions: Action[], import { removeBall, updateComponent } from "./TacticContentDomains"
components: TacticComponent[], import { getOrigin } from "./PlayerDomains"
) {
return actions.map((action) => ({ // export function refreshAllActions(
...action, // actions: Action[],
type: getActionKindFrom(action.fromId, action.toId, components), // components: TacticComponent[],
})) // ) {
} // return actions.map((action) => ({
// ...action,
// type: getActionKindFrom(action.fromId, action.toId, components),
// }))
// }
export function getActionKindFrom( export function getActionKindFrom(
originId: ComponentId, originId: ComponentId,
@ -22,7 +26,7 @@ export function getActionKindFrom(
components: TacticComponent[], components: TacticComponent[],
): ActionKind { ): ActionKind {
const origin = components.find((p) => p.id == originId)! 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 let ballState = BallState.NONE
@ -30,12 +34,17 @@ export function getActionKindFrom(
ballState = origin.ballState 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) return getActionKind(hasTarget, ballState)
} }
export function getActionKind(hasTarget: boolean, ballState: BallState): ActionKind { export function getActionKind(
hasTarget: boolean,
ballState: BallState,
): ActionKind {
switch (ballState) { switch (ballState) {
case BallState.HOLDS: case BallState.HOLDS:
return hasTarget ? ActionKind.SHOOT : ActionKind.DRIBBLE return hasTarget ? ActionKind.SHOOT : ActionKind.DRIBBLE
@ -51,20 +60,14 @@ export function placeArrow(
courtBounds: DOMRect, courtBounds: DOMRect,
arrowHead: DOMRect, arrowHead: DOMRect,
content: TacticContent, content: TacticContent,
): { createdAction: Action, newContent: TacticContent } { ): { createdAction: Action; newContent: TacticContent } {
const originRef = document.getElementById(origin.id)!
const start = ratioWithinBase(
middlePos(originRef.getBoundingClientRect()),
courtBounds,
)
/** /**
* Creates a new phantom component. * Creates a new phantom component.
* Be aware that this function will reassign the `content` parameter. * Be aware that this function will reassign the `content` parameter.
* @param receivesBall * @param receivesBall
*/ */
function createPhantom(receivesBall: boolean): ComponentId { function createPhantom(receivesBall: boolean): ComponentId {
const {x, y} = ratioWithinBase(arrowHead, courtBounds) const { x, y } = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number let itemIndex: number
let originPlayer: Player let originPlayer: Player
@ -99,16 +102,17 @@ export function placeArrow(
const ballState = receivesBall const ballState = receivesBall
? BallState.HOLDS ? BallState.HOLDS
: origin.ballState == BallState.HOLDS : origin.ballState == BallState.HOLDS
? BallState.HOLDS ? BallState.HOLDS
: BallState.NONE : BallState.NONE
const phantom: PlayerPhantom = { const phantom: PlayerPhantom = {
actions: [],
type: "phantom", type: "phantom",
id: phantomId, id: phantomId,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
originPlayerId: originPlayer.id, originPlayerId: originPlayer.id,
ballState ballState,
} }
content = { content = {
...content, ...content,
@ -127,12 +131,6 @@ export function placeArrow(
.getBoundingClientRect() .getBoundingClientRect()
if (overlaps(componentBounds, arrowHead)) { if (overlaps(componentBounds, arrowHead)) {
const targetPos = document
.getElementById(component.id)!
.getBoundingClientRect()
const end = ratioWithinBase(middlePos(targetPos), courtBounds)
let toId = component.id let toId = component.id
if (component.type == "ball") { if (component.type == "ball") {
@ -141,19 +139,20 @@ export function placeArrow(
} }
const action: Action = { const action: Action = {
fromId: originRef.id, target: toId,
toId,
type: getActionKind(true, origin.ballState), type: getActionKind(true, origin.ballState),
moveFrom: start, segments: [{ next: component.id }],
segments: [{next: end}],
} }
return { return {
newContent: { newContent: updateComponent(
...content, {
actions: [...content.actions, action], ...origin,
}, actions: [...origin.actions, action],
createdAction: action },
content,
),
createdAction: action,
} }
} }
} }
@ -161,54 +160,37 @@ export function placeArrow(
const phantomId = createPhantom(origin.ballState == BallState.HOLDS) const phantomId = createPhantom(origin.ballState == BallState.HOLDS)
const action: Action = { const action: Action = {
fromId: originRef.id, target: phantomId,
toId: phantomId,
type: getActionKind(false, origin.ballState), type: getActionKind(false, origin.ballState),
moveFrom: ratioWithinBase( segments: [{ next: phantomId }],
middlePos(originRef.getBoundingClientRect()),
courtBounds,
),
segments: [
{next: ratioWithinBase(middlePos(arrowHead), courtBounds)},
],
} }
return { return {
newContent: { newContent: updateComponent(
...content, {
actions: [...content.actions, action], ...content.components.find((c) => c.id == origin.id)!,
}, actions: [...origin.actions, action],
createdAction: action },
content,
),
createdAction: action,
} }
} }
export function repositionActionsRelatedTo( export function removeAllActionsTargeting(
compId: ComponentId, componentId: ComponentId,
courtBounds: DOMRect, content: TacticContent,
actions: Action[], ): TacticContent {
): Action[] { let components = []
const posRect = document.getElementById(compId)?.getBoundingClientRect() for (let i = 0; i < content.components.length; i++) {
const newPos = posRect != undefined const component = content.components[i]
? ratioWithinBase(middlePos(posRect), courtBounds) components.push({
: undefined ...component,
actions: component.actions.filter((a) => a.target != componentId),
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}]
}
return action return {
}) ...content,
components,
}
} }

@ -1,6 +1,7 @@
import { Player, PlayerPhantom } from "../model/tactic/Player" import { Player, PlayerPhantom } from "../model/tactic/Player"
import { TacticComponent, TacticContent } from "../model/tactic/Tactic" import { TacticComponent, TacticContent } from "../model/tactic/Tactic"
import { removeComponent, updateComponent } from "./TacticContentDomains" import { removeComponent, updateComponent } from "./TacticContentDomains"
import { removeAllActionsTargeting } from "./ActionsDomains"
export function getOrigin( export function getOrigin(
pathItem: PlayerPhantom, pathItem: PlayerPhantom,
@ -34,6 +35,8 @@ export function removePlayer(
player: Player | PlayerPhantom, player: Player | PlayerPhantom,
content: TacticContent, content: TacticContent,
): TacticContent { ): TacticContent {
content = removeAllActionsTargeting(player.id, content)
if (player.type == "phantom") { if (player.type == "phantom") {
const origin = getOrigin(player, content.components) const origin = getOrigin(player, content.components)
return truncatePlayerPath(origin, player, content) return truncatePlayerPath(origin, player, content)
@ -54,10 +57,10 @@ export function truncatePlayerPath(
let truncateStartIdx = -1 let truncateStartIdx = -1
for (let j = 0; j < path.items.length; j++) { for (let i = 0; i < path.items.length; i++) {
const pathPhantomId = path.items[j] const pathPhantomId = path.items[i]
if (truncateStartIdx != -1 || pathPhantomId == phantom.id) { if (truncateStartIdx != -1 || pathPhantomId == phantom.id) {
if (truncateStartIdx == -1) truncateStartIdx = j if (truncateStartIdx == -1) truncateStartIdx = i
//remove the phantom from the tactic //remove the phantom from the tactic
content = removeComponent(pathPhantomId, content) content = removeComponent(pathPhantomId, content)

@ -1,18 +1,31 @@
import {Pos, ratioWithinBase} from "../geo/Pos" import { Pos, ratioWithinBase } from "../geo/Pos"
import {BallState, Player, PlayerInfo, PlayerTeam} from "../model/tactic/Player" import {
import {Ball, BALL_ID, BALL_TYPE, CourtObject} from "../model/tactic/CourtObjects" BallState,
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic" Player,
import {overlaps} from "../geo/Box" PlayerInfo,
import {RackedCourtObject, RackedPlayer} from "./RackedItems" PlayerTeam,
import {refreshAllActions} from "./ActionsDomains" } from "../model/tactic/Player"
import {getOrigin} from "./PlayerDomains"; 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( export function placePlayerAt(
refBounds: DOMRect, refBounds: DOMRect,
courtBounds: DOMRect, courtBounds: DOMRect,
element: RackedPlayer, element: RackedPlayer,
): Player { ): Player {
const {x, y} = ratioWithinBase(refBounds, courtBounds) const { x, y } = ratioWithinBase(refBounds, courtBounds)
return { return {
type: "player", type: "player",
@ -23,6 +36,7 @@ export function placePlayerAt(
bottomRatio: y, bottomRatio: y,
ballState: BallState.NONE, ballState: BallState.NONE,
path: null, path: null,
actions: [],
} }
} }
@ -32,7 +46,7 @@ export function placeObjectAt(
rackedObject: RackedCourtObject, rackedObject: RackedCourtObject,
content: TacticContent, content: TacticContent,
): TacticContent { ): TacticContent {
const {x, y} = ratioWithinBase(refBounds, courtBounds) const { x, y } = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject let courtObject: CourtObject
@ -52,6 +66,7 @@ export function placeObjectAt(
id: BALL_ID, id: BALL_ID,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
actions: [],
} }
break break
@ -75,10 +90,10 @@ export function dropBallOnComponent(
let origin let origin
let isPhantom: boolean let isPhantom: boolean
if (component.type == 'phantom') { if (component.type == "phantom") {
isPhantom = true isPhantom = true
origin = getOrigin(component, components) origin = getOrigin(component, components)
} else if (component.type == 'player') { } else if (component.type == "player") {
isPhantom = false isPhantom = false
origin = component origin = component
} else { } else {
@ -91,11 +106,17 @@ export function dropBallOnComponent(
}) })
if (origin.path != null) { if (origin.path != null) {
const phantoms = origin.path!.items const phantoms = origin.path!.items
const headingPhantoms = isPhantom ? phantoms.slice(phantoms.indexOf(component.id)) : phantoms const headingPhantoms = isPhantom
components = components.map(c => headingPhantoms.indexOf(c.id) != -1 ? { ? phantoms.slice(phantoms.indexOf(component.id))
...c, : phantoms
hasBall: true components = components.map((c) =>
} : c) headingPhantoms.indexOf(c.id) != -1
? {
...c,
hasBall: true,
}
: c,
)
} }
const ballObj = components.findIndex((p) => p.type == BALL_TYPE) const ballObj = components.findIndex((p) => p.type == BALL_TYPE)
@ -109,7 +130,6 @@ export function dropBallOnComponent(
} }
return { return {
...content, ...content,
actions: refreshAllActions(content.actions, components),
components, components,
} }
} }
@ -118,11 +138,11 @@ export function removeBall(content: TacticContent): TacticContent {
const ballObj = content.components.findIndex((o) => o.type == "ball") const ballObj = content.components.findIndex((o) => o.type == "ball")
const components = content.components.map((c) => const components = content.components.map((c) =>
(c.type == 'player' || c.type == 'phantom') c.type == "player" || c.type == "phantom"
? { ? {
...c, ...c,
hasBall: false, hasBall: false,
} }
: c, : c,
) )
@ -133,7 +153,6 @@ export function removeBall(content: TacticContent): TacticContent {
return { return {
...content, ...content,
actions: refreshAllActions(content.actions, components),
components, components,
} }
} }
@ -147,7 +166,7 @@ export function placeBallAt(
removed: boolean removed: boolean
} { } {
if (!overlaps(courtBounds, refBounds)) { if (!overlaps(courtBounds, refBounds)) {
return {newContent: removeBall(content), removed: true} return { newContent: removeBall(content), removed: true }
} }
const playerCollidedIdx = getComponentCollided( const playerCollidedIdx = getComponentCollided(
refBounds, refBounds,
@ -159,11 +178,11 @@ export function placeBallAt(
newContent: dropBallOnComponent(playerCollidedIdx, { newContent: dropBallOnComponent(playerCollidedIdx, {
...content, ...content,
components: content.components.map((c) => components: content.components.map((c) =>
c.type == "player" || c.type == 'phantom' c.type == "player" || c.type == "phantom"
? { ? {
...c, ...c,
hasBall: false, hasBall: false,
} }
: c, : c,
), ),
}), }),
@ -173,14 +192,14 @@ export function placeBallAt(
const ballIdx = content.components.findIndex((o) => o.type == "ball") 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) => const components = content.components.map((c) =>
c.type == "player" || c.type == "phantom" c.type == "player" || c.type == "phantom"
? { ? {
...c, ...c,
hasBall: false, hasBall: false,
} }
: c, : c,
) )
@ -189,6 +208,7 @@ export function placeBallAt(
id: BALL_ID, id: BALL_ID,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
actions: [],
} }
if (ballIdx != -1) { if (ballIdx != -1) {
components.splice(ballIdx, 1, ball) components.splice(ballIdx, 1, ball)
@ -199,7 +219,6 @@ export function placeBallAt(
return { return {
newContent: { newContent: {
...content, ...content,
actions: refreshAllActions(content.actions, components),
components, components,
}, },
removed: false, removed: false,
@ -243,9 +262,6 @@ export function removeComponent(
return { return {
...content, ...content,
components: content.components.toSpliced(componentIdx, 1), 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, c.type == "player" && c.team == team && c.role == role,
) == -1, ) == -1,
) )
.map((key) => ({team, key})) .map((key) => ({ team, key }))
} }

@ -12,8 +12,7 @@ export enum ActionKind {
export type Action = { type: ActionKind } & MovementAction export type Action = { type: ActionKind } & MovementAction
export interface MovementAction { export interface MovementAction {
fromId: ComponentId // fromId: ComponentId
toId: ComponentId | null target: ComponentId | Pos
moveFrom: Pos
segments: Segment[] segments: Segment[]
} }

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

@ -10,7 +10,7 @@ export interface Tactic {
export interface TacticContent { export interface TacticContent {
components: TacticComponent[] components: TacticComponent[]
actions: Action[] //actions: Action[]
} }
export type TacticComponent = Player | CourtObject | PlayerPhantom 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) * Percentage of the component's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
readonly rightRatio: number readonly rightRatio: number
readonly actions: Action[]
} }

@ -5,6 +5,7 @@
.arrow-action-icon { .arrow-action-icon {
user-select: none; user-select: none;
-moz-user-select: none; -moz-user-select: none;
-webkit-user-drag: none;
max-width: 17px; max-width: 17px;
max-height: 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 "../style/editor.css"
import TitleInput from "../components/TitleInput" import TitleInput from "../components/TitleInput"
import PlainCourt from "../assets/court/full_court.svg?react" import PlainCourt from "../assets/court/full_court.svg?react"
import HalfCourt from "../assets/court/half_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 { Rack } from "../components/Rack"
import {PlayerPiece} from "../components/editor/PlayerPiece" import { PlayerPiece } from "../components/editor/PlayerPiece"
import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic" import { Tactic, TacticComponent, TacticContent } from "../model/tactic/Tactic"
import {fetchAPI} from "../Fetcher" import { fetchAPI } from "../Fetcher"
import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState" import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import {BALL_TYPE} from "../model/tactic/CourtObjects" import { BALL_TYPE } from "../model/tactic/CourtObjects"
import {CourtAction} from "./editor/CourtAction" import { CourtAction } from "./editor/CourtAction"
import {BasketCourt} from "../components/editor/BasketCourt" import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt"
import {overlaps} from "../geo/Box" import { overlaps } from "../geo/Box"
import { import {
dropBallOnComponent, dropBallOnComponent,
getComponentCollided, getComponentCollided,
@ -26,19 +37,30 @@ import {
placeBallAt, placeBallAt,
placeObjectAt, placeObjectAt,
placePlayerAt, placePlayerAt,
removeBall, updateComponent, removeBall,
updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
import {BallState, Player, PlayerInfo, PlayerPhantom, PlayerTeam,} from "../model/tactic/Player" import {
import {RackedCourtObject} from "../editor/RackedItems" BallState,
Player,
PlayerInfo,
PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer" 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 ArrowAction from "../components/actions/ArrowAction"
import {middlePos, ratioWithinBase} from "../geo/Pos" import { middlePos, ratioWithinBase } from "../geo/Pos"
import {Action, ActionKind} from "../model/tactic/Action" import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction" import BallAction from "../components/actions/BallAction"
import {getOrigin, removePlayer, truncatePlayerPath,} from "../editor/PlayerDomains" import {
import {CourtBall} from "../components/editor/CourtBall" getOrigin,
import {BASE} from "../Constants" removePlayer,
truncatePlayerPath,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import { BASE } from "../Constants"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -61,7 +83,7 @@ export interface EditorProps {
courtType: "PLAIN" | "HALF" 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 isInGuestMode = id == -1
const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) 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 SaveStates.Guest
} }
return fetchAPI(`tactic/${id}/save`, {content}).then((r) => return fetchAPI(`tactic/${id}/save`, { content }).then((r) =>
r.ok ? SaveStates.Ok : SaveStates.Err, 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) localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
return true //simulate that the name has been changed 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, (r) => r.ok,
) )
}} }}
@ -106,11 +128,11 @@ export default function Editor({id, name, courtType, content}: EditorProps) {
} }
function EditorView({ function EditorView({
tactic: {id, name, content: initialContent}, tactic: { id, name, content: initialContent },
onContentChange, onContentChange,
onNameChange, onNameChange,
courtType, courtType,
}: EditorViewProps) { }: EditorViewProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -130,27 +152,24 @@ function EditorView({
), ),
) )
const [allies, setAllies] = useState( const [allies, setAllies] = useState(() =>
() => getRackPlayers(PlayerTeam.Allies, content.components), getRackPlayers(PlayerTeam.Allies, content.components),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(() =>
() => getRackPlayers(PlayerTeam.Opponents, content.components), getRackPlayers(PlayerTeam.Opponents, content.components),
) )
const [objects, setObjects] = useState<RackedCourtObject[]>( const [objects, setObjects] = useState<RackedCourtObject[]>(() =>
() => isBallOnCourt(content) ? [] : [{key: "ball"}], isBallOnCourt(content) ? [] : [{ key: "ball" }],
) )
const [previewAction, setPreviewAction] = useState<Action | null>(null) const [previewAction, setPreviewAction] = useState<ActionPreview | null>(
null,
)
const courtRef = useRef<HTMLDivElement>(null) const courtRef = useRef<HTMLDivElement>(null)
const setActions = (action: SetStateAction<Action[]>) => { const actionsReRenderHooks = []
setContent((c) => ({
...c,
actions: typeof action == "function" ? action(c.actions) : action,
}))
}
const setComponents = (action: SetStateAction<TacticComponent[]>) => { const setComponents = (action: SetStateAction<TacticComponent[]>) => {
setContent((c) => ({ setContent((c) => ({
@ -170,7 +189,7 @@ function EditorView({
setter = setAllies setter = setAllies
} }
if (player.ballState == BallState.HOLDS) { if (player.ballState == BallState.HOLDS) {
setObjects([{key: "ball"}]) setObjects([{ key: "ball" }])
} }
setter((players) => [ setter((players) => [
...players, ...players,
@ -184,14 +203,14 @@ function EditorView({
const doMoveBall = (newBounds: DOMRect) => { const doMoveBall = (newBounds: DOMRect) => {
setContent((content) => { setContent((content) => {
const {newContent, removed} = placeBallAt( const { newContent, removed } = placeBallAt(
newBounds, newBounds,
courtBounds(), courtBounds(),
content, content,
) )
if (removed) { if (removed) {
setObjects((objects) => [...objects, {key: "ball"}]) setObjects((objects) => [...objects, { key: "ball" }])
} }
return newContent return newContent
@ -209,13 +228,18 @@ function EditorView({
const origin = getOrigin(component, content.components) const origin = getOrigin(component, content.components)
const path = origin.path! const path = origin.path!
// phantoms can only place other arrows if they are the head of the 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) { if (canPlaceArrows) {
// and if their only action is to shoot the ball // and if their only action is to shoot the ball
// list the actions the phantoms does // list the actions the phantoms does
const phantomArrows = content.actions.filter(c => c.fromId == component.id) const phantomActions = component.actions
canPlaceArrows = phantomArrows.length == 0 || phantomArrows.findIndex(c => c.type != ActionKind.SHOOT) == -1 canPlaceArrows =
phantomActions.length == 0 ||
phantomActions.findIndex(
(c) => c.type != ActionKind.SHOOT,
) == -1
} }
info = { info = {
@ -230,7 +254,11 @@ function EditorView({
// a player // a player
info = component info = component
// can place arrows only if the // 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 ( return (
@ -238,11 +266,6 @@ function EditorView({
key={component.id} key={component.id}
className={isPhantom ? "phantom" : "player"} className={isPhantom ? "phantom" : "player"}
playerInfo={info} playerInfo={info}
onMoves={() =>
setActions((actions) =>
repositionActionsRelatedTo(info.id, courtBounds(), actions),
)
}
onPositionValidated={(newPos) => { onPositionValidated={(newPos) => {
setContent((content) => setContent((content) =>
moveComponent( moveComponent(
@ -264,13 +287,16 @@ function EditorView({
if (!isPhantom) insertRackedPlayer(component) if (!isPhantom) insertRackedPlayer(component)
}} }}
courtRef={courtRef} courtRef={courtRef}
availableActions={(pieceRef) => [ availableActions={() => [
canPlaceArrows && ( canPlaceArrows && (
<ArrowAction <ArrowAction
key={1} key={1}
onHeadMoved={(headPos) => { onHeadMoved={(headPos) => {
const arrowHeadPos = middlePos(headPos) const arrowHeadPos = middlePos(headPos)
const targetIdx = getComponentCollided(headPos, content.components) const targetIdx = getComponentCollided(
headPos,
content.components,
)
setPreviewAction((action) => ({ setPreviewAction((action) => ({
...action!, ...action!,
@ -282,20 +308,20 @@ function EditorView({
), ),
}, },
], ],
type: getActionKind(targetIdx != -1, info.ballState), type: getActionKind(
targetIdx != -1,
info.ballState,
),
})) }))
}} }}
onHeadPicked={(headPos) => { onHeadPicked={(headPos) => {
(document.activeElement as HTMLElement).blur() ;(document.activeElement as HTMLElement).blur()
setPreviewAction({ setPreviewAction({
origin: component.id,
type: getActionKind(false, info.ballState), type: getActionKind(false, info.ballState),
fromId: info.id, target: ratioWithinBase(
toId: null, headPos,
moveFrom: ratioWithinBase(
middlePos(
pieceRef.getBoundingClientRect(),
),
courtBounds(), courtBounds(),
), ),
segments: [ segments: [
@ -310,25 +336,41 @@ function EditorView({
}} }}
onHeadDropped={(headRect) => { onHeadDropped={(headRect) => {
setContent((content) => { setContent((content) => {
let {createdAction, newContent} = placeArrow( let { createdAction, newContent } =
component, placeArrow(
courtBounds(), component,
headRect, courtBounds(),
content, headRect,
) content,
)
let originNewBallState = component.ballState let originNewBallState = component.ballState
if (createdAction.type == ActionKind.SHOOT) { if (
const targetIdx = newContent.components.findIndex(c => c.id == createdAction.toId) createdAction.type == ActionKind.SHOOT
newContent = dropBallOnComponent(targetIdx, newContent) ) {
const targetIdx =
newContent.components.findIndex(
(c) =>
c.id ==
createdAction.target,
)
newContent = dropBallOnComponent(
targetIdx,
newContent,
)
originNewBallState = BallState.SHOOTED originNewBallState = BallState.SHOOTED
} }
newContent = updateComponent({ newContent = updateComponent(
...(newContent.components.find(c => c.id == component.id)! as Player | PlayerPhantom), {
ballState: originNewBallState ...(newContent.components.find(
}, newContent) (c) => c.id == component.id,
)! as Player | PlayerPhantom),
ballState: originNewBallState,
},
newContent,
)
return newContent return newContent
}) })
setPreviewAction(null) setPreviewAction(null)
@ -336,16 +378,54 @@ function EditorView({
/> />
), ),
info.ballState != BallState.NONE && ( info.ballState != BallState.NONE && (
<BallAction <BallAction key={2} onDrop={doMoveBall} />
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 ( return (
<div id="main-div"> <div id="main-div">
<div id="topbar-div"> <div id="topbar-div">
@ -353,7 +433,7 @@ function EditorView({
Home Home
</button> </button>
<div id="topbar-left"> <div id="topbar-left">
<SavingState state={saveState}/> <SavingState state={saveState} />
</div> </div>
<div id="title-input-div"> <div id="title-input-div">
<TitleInput <TitleInput
@ -366,7 +446,7 @@ function EditorView({
}} }}
/> />
</div> </div>
<div id="topbar-right"/> <div id="topbar-right" />
</div> </div>
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
@ -387,7 +467,7 @@ function EditorView({
), ),
]) ])
} }
render={({team, key}) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
text={key} text={key}
@ -434,7 +514,7 @@ function EditorView({
), ),
]) ])
} }
render={({team, key}) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
text={key} text={key}
@ -448,8 +528,7 @@ function EditorView({
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
components={content.components} components={content.components}
actions={content.actions} courtImage={<Court courtType={courtType} />}
courtImage={<Court courtType={courtType}/>}
courtRef={courtRef} courtRef={courtRef}
previewAction={previewAction} previewAction={previewAction}
renderComponent={(component) => { renderComponent={(component) => {
@ -465,20 +544,14 @@ function EditorView({
key="ball" key="ball"
ball={component} ball={component}
onPosValidated={doMoveBall} onPosValidated={doMoveBall}
onMoves={() =>
setActions((actions) =>
repositionActionsRelatedTo(
component.id,
courtBounds(),
actions,
),
)
}
onRemove={() => { onRemove={() => {
setContent((content) => setContent((content) =>
removeBall(content), removeBall(content),
) )
setObjects(objects => [...objects, {key: "ball"}]) setObjects((objects) => [
...objects,
{ key: "ball" },
])
}} }}
/> />
) )
@ -487,60 +560,35 @@ function EditorView({
"unknown tactic component " + component, "unknown tactic component " + component,
) )
}} }}
renderAction={(action, i) => ( renderActions={(component) =>
<CourtAction component.actions.map((action, i) => (
key={i} <CourtAction
action={action} key={"action-" + component.id + "-" + i}
courtRef={courtRef} action={action}
onActionDeleted={() => { origin={component.id}
setContent((content) => { courtRef={courtRef}
content = { onActionDeleted={() => {
...content, doDeleteAction(action, i, component)
actions: }}
content.actions.toSpliced( onActionChanges={(a) =>
i, setContent((content) =>
1, updateComponent(
), {
} ...component,
actions:
if (action.toId == null) component.actions.toSpliced(
return content i,
1,
const target = a,
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,
content, content,
) ),
} )
}
return content />
}) ))
}} }
onActionChanges={(a) =>
setContent((content) => ({
...content,
actions: content.actions.toSpliced(
i,
1,
a,
),
}))
}
/>
)}
/> />
</div> </div>
</div> </div>
@ -552,25 +600,27 @@ function EditorView({
function isBallOnCourt(content: TacticContent) { function isBallOnCourt(content: TacticContent) {
return ( return (
content.components.findIndex( 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 ) != -1
) )
} }
function renderCourtObject(courtObject: RackedCourtObject) { function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") { if (courtObject.key == "ball") {
return <BallPiece/> return <BallPiece />
} }
throw new Error("unknown racked court object " + courtObject.key) throw new Error("unknown racked court object " + courtObject.key)
} }
function Court({courtType}: { courtType: string }) { function Court({ courtType }: { courtType: string }) {
return ( return (
<div id="court-image-div"> <div id="court-image-div">
{courtType == "PLAIN" ? ( {courtType == "PLAIN" ? (
<PlainCourt id="court-image"/> <PlainCourt id="court-image" />
) : ( ) : (
<HalfCourt id="court-image"/> <HalfCourt id="court-image" />
)} )}
</div> </div>
) )

@ -2,8 +2,11 @@ import { Action, ActionKind } from "../../model/tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow" import BendableArrow from "../../components/arrows/BendableArrow"
import { RefObject } from "react" import { RefObject } from "react"
import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction"
import { ComponentId } from "../../model/tactic/Tactic"
import { middlePos, Pos, ratioWithinBase } from "../../geo/Pos"
export interface CourtActionProps { export interface CourtActionProps {
origin: ComponentId
action: Action action: Action
onActionChanges: (a: Action) => void onActionChanges: (a: Action) => void
onActionDeleted: () => void onActionDeleted: () => void
@ -11,6 +14,7 @@ export interface CourtActionProps {
} }
export function CourtAction({ export function CourtAction({
origin,
action, action,
onActionChanges, onActionChanges,
onActionDeleted, onActionDeleted,
@ -39,14 +43,14 @@ export function CourtAction({
<BendableArrow <BendableArrow
forceStraight={action.type == ActionKind.SHOOT} forceStraight={action.type == ActionKind.SHOOT}
area={courtRef} area={courtRef}
startPos={action.moveFrom} startPos={origin}
segments={action.segments} segments={action.segments}
onSegmentsChanges={(edges) => { onSegmentsChanges={(edges) => {
onActionChanges({ ...action, segments: edges }) onActionChanges({ ...action, segments: edges })
}} }}
wavy={action.type == ActionKind.DRIBBLE} wavy={action.type == ActionKind.DRIBBLE}
//TODO place those magic values in constants //TODO place those magic values in constants
endRadius={action.toId ? 26 : 17} endRadius={action.target ? 26 : 17}
startRadius={10} startRadius={10}
onDeleteRequested={onActionDeleted} onDeleteRequested={onActionDeleted}
style={{ style={{

@ -26,7 +26,7 @@ CREATE TABLE Tactic
name varchar NOT NULL, name varchar NOT NULL,
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
owner integer 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, court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL,
FOREIGN KEY (owner) REFERENCES Account FOREIGN KEY (owner) REFERENCES Account
); );

@ -42,7 +42,7 @@ class EditorController {
return ViewHttpResponse::react("views/Editor.tsx", [ return ViewHttpResponse::react("views/Editor.tsx", [
"id" => -1, //-1 id means that the editor will not support saves "id" => -1, //-1 id means that the editor will not support saves
"name" => TacticModel::TACTIC_DEFAULT_NAME, "name" => TacticModel::TACTIC_DEFAULT_NAME,
"content" => '{"components": [], "actions": []}', "content" => '{"components": []}',
"courtType" => $courtType->name(), "courtType" => $courtType->name(),
]); ]);
} }

Loading…
Cancel
Save