use one array of TacticComponent

maxime.batista 1 year ago committed by maxime
parent 3ace793578
commit 05a22c6f7a

@ -22,7 +22,7 @@ import {
ratioWithinBase, ratioWithinBase,
relativeTo, relativeTo,
norm, norm,
} from "./Pos" } from "../../geo/Pos"
import "../../style/bendable_arrows.css" import "../../style/bendable_arrows.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"

@ -1,7 +1,8 @@
import "../../style/ball.css" import "../../style/ball.css"
import BallSvg from "../../assets/icon/ball.svg?react" import BallSvg from "../../assets/icon/ball.svg?react"
import {BALL_ID} from "../../model/tactic/Ball";
export function BallPiece() { export function BallPiece() {
return <BallSvg className={"ball"} /> return <BallSvg id={BALL_ID} className={"ball"}/>
} }

@ -12,16 +12,17 @@ import CourtPlayer from "./CourtPlayer"
import { Player } from "../../model/tactic/Player" import { Player } from "../../model/tactic/Player"
import { Action, ActionKind } from "../../model/tactic/Action" import { Action, ActionKind } from "../../model/tactic/Action"
import ArrowAction from "../actions/ArrowAction" import ArrowAction from "../actions/ArrowAction"
import { middlePos, ratioWithinBase } from "../arrows/Pos" import { middlePos, ratioWithinBase } from "../../geo/Pos"
import BallAction from "../actions/BallAction" import BallAction from "../actions/BallAction"
import { CourtObject } from "../../model/tactic/Ball" import {BALL_ID} from "../../model/tactic/Ball"
import { contains } from "../arrows/Box" import { contains, overlaps } from "../../geo/Box"
import { CourtAction } from "../../views/editor/CourtAction" import { CourtAction } from "../../views/editor/CourtAction"
import { TacticComponent } from "../../model/tactic/Tactic"
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] components: TacticComponent[]
actions: Action[] actions: Action[]
objects: CourtObject[]
renderAction: (a: Action, key: number) => ReactElement renderAction: (a: Action, key: number) => ReactElement
setActions: (f: (a: Action[]) => Action[]) => void setActions: (f: (a: Action[]) => Action[]) => void
@ -37,9 +38,8 @@ export interface BasketCourtProps {
} }
export function BasketCourt({ export function BasketCourt({
players, components,
actions, actions,
objects,
renderAction, renderAction,
setActions, setActions,
onPlayerRemove, onPlayerRemove,
@ -59,33 +59,31 @@ export function BasketCourt({
courtBounds, courtBounds,
) )
for (const player of players) { for (const component of components) {
if (player.id == origin.id) { if (component.id == origin.id) {
continue continue
} }
const playerBounds = document const playerBounds = document
.getElementById(player.id)! .getElementById(component.id)!
.getBoundingClientRect() .getBoundingClientRect()
if ( if (overlaps(playerBounds, arrowHead)) {
!(
playerBounds.top > arrowHead.bottom ||
playerBounds.right < arrowHead.left ||
playerBounds.bottom < arrowHead.top ||
playerBounds.left > arrowHead.right
)
) {
const targetPos = document const targetPos = document
.getElementById(player.id)! .getElementById(component.id)!
.getBoundingClientRect() .getBoundingClientRect()
const end = ratioWithinBase(middlePos(targetPos), courtBounds) const end = ratioWithinBase(middlePos(targetPos), courtBounds)
const action: Action = { const action: Action = {
fromPlayerId: originRef.id, fromId: originRef.id,
toPlayerId: player.id, toId: component.id,
type: origin.hasBall ? ActionKind.SHOOT : ActionKind.SCREEN, type:
component.type == "player"
? origin.hasBall
? ActionKind.SHOOT
: ActionKind.SCREEN
: ActionKind.MOVE,
moveFrom: start, moveFrom: start,
segments: [{ next: end }], segments: [{ next: end }],
} }
@ -95,7 +93,7 @@ export function BasketCourt({
} }
const action: Action = { const action: Action = {
fromPlayerId: originRef.id, fromId: originRef.id,
type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE, type: origin.hasBall ? ActionKind.DRIBBLE : ActionKind.MOVE,
moveFrom: ratioWithinBase( moveFrom: ratioWithinBase(
middlePos(originRef.getBoundingClientRect()), middlePos(originRef.getBoundingClientRect()),
@ -110,20 +108,20 @@ export function BasketCourt({
const [previewAction, setPreviewAction] = useState<Action | null>(null) const [previewAction, setPreviewAction] = useState<Action | null>(null)
const updateActionsRelatedTo = useCallback((player: Player) => { const updateActionsRelatedTo = useCallback((comp: TacticComponent) => {
const newPos = ratioWithinBase( const newPos = ratioWithinBase(
middlePos( middlePos(
document.getElementById(player.id)!.getBoundingClientRect(), document.getElementById(comp.id)!.getBoundingClientRect(),
), ),
courtRef.current!.getBoundingClientRect(), courtRef.current!.getBoundingClientRect(),
) )
setActions((actions) => setActions((actions) =>
actions.map((a) => { actions.map((a) => {
if (a.fromPlayerId == player.id) { if (a.fromId == comp.id) {
return { ...a, moveFrom: newPos } return { ...a, moveFrom: newPos }
} }
if (a.toPlayerId == player.id) { if (a.toId == comp.id) {
const segments = a.segments.toSpliced( const segments = a.segments.toSpliced(
a.segments.length - 1, a.segments.length - 1,
1, 1,
@ -151,7 +149,10 @@ export function BasketCourt({
style={{ position: "relative" }}> style={{ position: "relative" }}>
{courtImage} {courtImage}
{players.map((player) => ( {components.map((component) => {
if (component.type == "player") {
const player = component
return (
<CourtPlayer <CourtPlayer
key={player.id} key={player.id}
player={player} player={player}
@ -168,17 +169,28 @@ export function BasketCourt({
const arrowHeadPos = middlePos(headPos) const arrowHeadPos = middlePos(headPos)
const target = players.find( const target = components.find(
(p) => (c) =>
p != player && c.id != player.id &&
contains( contains(
document document
.getElementById(p.id)! .getElementById(c.id)!
.getBoundingClientRect(), .getBoundingClientRect(),
arrowHeadPos, arrowHeadPos,
), ),
) )
const type =
target?.type == "player"
? player.hasBall
? target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
: target
? ActionKind.SCREEN
: ActionKind.MOVE
: ActionKind.MOVE
setPreviewAction((action) => ({ setPreviewAction((action) => ({
...action!, ...action!,
segments: [ segments: [
@ -189,17 +201,13 @@ export function BasketCourt({
), ),
}, },
], ],
type: player.hasBall type,
? target
? ActionKind.SHOOT
: ActionKind.DRIBBLE
: target
? ActionKind.SCREEN
: ActionKind.MOVE,
})) }))
}} }}
onHeadPicked={(headPos) => { onHeadPicked={(headPos) => {
;(document.activeElement as HTMLElement).blur() ;(
document.activeElement as HTMLElement
).blur()
const baseBounds = const baseBounds =
courtRef.current!.getBoundingClientRect() courtRef.current!.getBoundingClientRect()
@ -207,8 +215,8 @@ export function BasketCourt({
type: player.hasBall type: player.hasBall
? ActionKind.DRIBBLE ? ActionKind.DRIBBLE
: ActionKind.MOVE, : ActionKind.MOVE,
fromPlayerId: player.id, fromId: player.id,
toPlayerId: undefined, toId: undefined,
moveFrom: ratioWithinBase( moveFrom: ratioWithinBase(
middlePos( middlePos(
pieceRef.getBoundingClientRect(), pieceRef.getBoundingClientRect(),
@ -234,30 +242,32 @@ export function BasketCourt({
<BallAction <BallAction
key={2} key={2}
onDrop={(ref) => onDrop={(ref) =>
onBallMoved(ref.getBoundingClientRect()) onBallMoved(
ref.getBoundingClientRect(),
)
} }
/> />
), ),
]} ]}
/> />
))} )
}
{internActions.map((action, idx) => renderAction(action, idx))} if (component.type == BALL_ID) {
{objects.map((object) => {
if (object.type == "ball") {
return ( return (
<CourtBall <CourtBall
onMoved={onBallMoved} onPosValidated={onBallMoved}
ball={object} onMoves={() => updateActionsRelatedTo(component)}
ball={component}
onRemove={onBallRemove} onRemove={onBallRemove}
key="ball" key="ball"
/> />
) )
} }
throw new Error("unknown court object" + object.type) throw new Error("unknown tactic component " + component)
})} })}
{internActions.map((action, idx) => renderAction(action, idx))}
{previewAction && ( {previewAction && (
<CourtAction <CourtAction
courtRef={courtRef} courtRef={courtRef}

@ -4,12 +4,18 @@ import { BallPiece } from "./BallPiece"
import { Ball } from "../../model/tactic/Ball" import { Ball } from "../../model/tactic/Ball"
export interface CourtBallProps { export interface CourtBallProps {
onMoved: (rect: DOMRect) => void onPosValidated: (rect: DOMRect) => void
onMoves: () => void
onRemove: () => void onRemove: () => void
ball: Ball ball: Ball
} }
export function CourtBall({ onMoved, ball, onRemove }: CourtBallProps) { export function CourtBall({
onPosValidated,
ball,
onRemove,
onMoves,
}: CourtBallProps) {
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
const x = ball.rightRatio const x = ball.rightRatio
@ -17,7 +23,10 @@ export function CourtBall({ onMoved, ball, onRemove }: CourtBallProps) {
return ( return (
<Draggable <Draggable
onStop={() => onMoved(pieceRef.current!.getBoundingClientRect())} onStop={() =>
onPosValidated(pieceRef.current!.getBoundingClientRect())
}
onDrag={onMoves}
nodeRef={pieceRef}> nodeRef={pieceRef}>
<div <div
className={"ball-div"} className={"ball-div"}

@ -3,7 +3,7 @@ import "../../style/player.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
import { Player } from "../../model/tactic/Player" import { Player } from "../../model/tactic/Player"
import { NULL_POS, ratioWithinBase } from "../arrows/Pos" import { NULL_POS, ratioWithinBase } from "../../geo/Pos"
export interface PlayerProps { export interface PlayerProps {
player: Player player: Player
@ -44,13 +44,14 @@ export default function CourtPlayer({
const { x, y } = ratioWithinBase(pieceBounds, parentBounds) const { x, y } = ratioWithinBase(pieceBounds, parentBounds)
onChange({ onChange({
type: "player",
id: player.id, id: player.id,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
team: player.team, team: player.team,
role: player.role, role: player.role,
hasBall: player.hasBall, hasBall: player.hasBall,
}) } as Player)
}}> }}>
<div <div
id={player.id} id={player.id}

@ -28,6 +28,14 @@ export function surrounds(pos: Pos, width: number, height: number): Box {
} }
} }
export function overlaps(a: Box, b: Box): boolean {
if (a.x + a.width < b.x || b.x + b.width < a.x) {
return false
}
return !(a.y + a.height < b.y || b.y + b.height < a.y)
}
export function contains(box: Box, pos: Pos): boolean { export function contains(box: Box, pos: Pos): boolean {
return ( return (
pos.x >= box.x && pos.x >= box.x &&

@ -1,6 +1,7 @@
import { Pos } from "../../components/arrows/Pos"
import { Pos } from "../../geo/Pos"
import { Segment } from "../../components/arrows/BendableArrow" import { Segment } from "../../components/arrows/BendableArrow"
import { PlayerId } from "./Player" import { ComponentId } from "./Tactic"
export enum ActionKind { export enum ActionKind {
SCREEN = "SCREEN", SCREEN = "SCREEN",
@ -12,8 +13,8 @@ export enum ActionKind {
export type Action = { type: ActionKind } & MovementAction export type Action = { type: ActionKind } & MovementAction
export interface MovementAction { export interface MovementAction {
fromPlayerId: PlayerId fromId: ComponentId
toPlayerId?: PlayerId toId?: ComponentId
moveFrom: Pos moveFrom: Pos
segments: Segment[] segments: Segment[]
} }

@ -1,17 +1,9 @@
export type CourtObject = { type: "ball" } & Ball import { Component } from "./Tactic"
export interface Ball { export const BALL_ID = "ball"
/** export const BALL_TYPE = "ball"
* The ball is a "ball" court object
*/
readonly type: "ball"
/** //place here all different kinds of objects
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) export type CourtObject = Ball
*/
readonly bottomRatio: number export type Ball = Component<typeof BALL_TYPE>
/**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/
readonly rightRatio: number
}

@ -1,3 +1,5 @@
import {Component} from "./Tactic";
export type PlayerId = string export type PlayerId = string
export enum PlayerTeam { export enum PlayerTeam {
@ -7,7 +9,9 @@ export enum PlayerTeam {
export interface Player { export interface Player {
readonly id: PlayerId readonly id: PlayerId
}
export interface Player extends Component<"player"> {
/** /**
* the player's team * the player's team
* */ * */
@ -18,18 +22,9 @@ export interface Player {
* */ * */
readonly role: string readonly role: string
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/
readonly rightRatio: number
/** /**
* True if the player has a basketball * True if the player has a basketball
*/ */
readonly hasBall: boolean readonly hasBall: boolean
} }

@ -1,6 +1,6 @@
import {Player} from "./Player" import {Player} from "./Player"
import { CourtObject } from "./Ball"
import {Action} from "./Action" import {Action} from "./Action"
import {CourtObject} from "./Ball"
export interface Tactic { export interface Tactic {
id: number id: number
@ -9,7 +9,29 @@ export interface Tactic {
} }
export interface TacticContent { export interface TacticContent {
players: Player[] components: TacticComponent[]
objects: CourtObject[]
actions: Action[] actions: Action[]
} }
export type TacticComponent = Player | CourtObject
export type ComponentId = string
export interface Component<T> {
/**
* The component's type
*/
readonly type: T
/**
* The component's identifier
*/
readonly id: ComponentId
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/
readonly rightRatio: number
}

@ -1,12 +1,4 @@
import { import {CSSProperties, Dispatch, SetStateAction, useCallback, useMemo, useRef, useState,} from "react"
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"
@ -16,23 +8,20 @@ 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 { Player } from "../model/tactic/Player" import {Player, PlayerTeam} from "../model/tactic/Player"
import { Tactic, TacticContent } from "../model/tactic/Tactic" import {Tactic, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {fetchAPI} from "../Fetcher" import {fetchAPI} from "../Fetcher"
import { PlayerTeam } from "../model/tactic/Player"
import SavingState, { import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { CourtObject } from "../model/tactic/Ball" import {BALL_ID, BALL_TYPE, CourtObject, Ball} from "../model/tactic/Ball"
import {CourtAction} from "./editor/CourtAction" import {CourtAction} from "./editor/CourtAction"
import {BasketCourt} from "../components/editor/BasketCourt" import {BasketCourt} from "../components/editor/BasketCourt"
import { ratioWithinBase } from "../components/arrows/Pos"
import {Action, ActionKind} from "../model/tactic/Action" import {Action, ActionKind} from "../model/tactic/Action"
import {BASE} from "../Constants" import {BASE} from "../Constants"
import {overlaps} from "../geo/Box"
import {ratioWithinBase} from "../geo/Pos"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -135,10 +124,10 @@ function EditorView({
) )
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
getRackPlayers(PlayerTeam.Allies, content.players), getRackPlayers(PlayerTeam.Allies, content.components),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
getRackPlayers(PlayerTeam.Opponents, content.players), getRackPlayers(PlayerTeam.Opponents, content.components),
) )
const [objects, setObjects] = useState<RackedCourtObject[]>( const [objects, setObjects] = useState<RackedCourtObject[]>(
@ -151,15 +140,10 @@ function EditorView({
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
// check if refBounds overlaps courtBounds // check if refBounds overlaps courtBounds
return !( return overlaps(courtBounds, bounds)
bounds.top > courtBounds.bottom ||
bounds.right < courtBounds.left ||
bounds.bottom < courtBounds.top ||
bounds.left > courtBounds.right
)
} }
const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { const onRackPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => {
const refBounds = ref.getBoundingClientRect() const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
@ -168,23 +152,24 @@ function EditorView({
setContent((content) => { setContent((content) => {
return { return {
...content, ...content,
players: [ components: [
...content.players, ...content.components,
{ {
type: "player",
id: "player-" + element.key + "-" + element.team, id: "player-" + element.key + "-" + element.team,
team: element.team, team: element.team,
role: element.key, role: element.key,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
hasBall: false, hasBall: false,
}, } as Player,
], ],
actions: content.actions, actions: content.actions,
} }
}) })
} }
const onObjectDetach = ( const onRackedObjectDetach = (
ref: HTMLDivElement, ref: HTMLDivElement,
rackedObject: RackedCourtObject, rackedObject: RackedCourtObject,
) => { ) => {
@ -196,27 +181,22 @@ function EditorView({
let courtObject: CourtObject let courtObject: CourtObject
switch (rackedObject.key) { switch (rackedObject.key) {
case "ball": case BALL_TYPE:
const ballObj = content.objects.findIndex( const ballObj = content.components.findIndex(
(o) => o.type == "ball", (o) => o.type == BALL_TYPE,
) )
const playerCollidedIdx = getPlayerCollided( const playerCollidedIdx = getComponentCollided(
refBounds, refBounds,
content.players, content.components.toSpliced(ballObj, 1),
) )
if (playerCollidedIdx != -1) { if (playerCollidedIdx != -1) {
onBallDropOnPlayer(playerCollidedIdx) onBallDropOnComponent(playerCollidedIdx)
setContent((content) => {
return {
...content,
objects: content.objects.toSpliced(ballObj, 1),
}
})
return return
} }
courtObject = { courtObject = {
type: "ball", type: BALL_TYPE,
id: BALL_ID,
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
} }
@ -229,38 +209,34 @@ function EditorView({
setContent((content) => { setContent((content) => {
return { return {
...content, ...content,
objects: [...content.objects, courtObject], components: [...content.components, courtObject],
} }
}) })
} }
const getPlayerCollided = ( const getComponentCollided = (
bounds: DOMRect, bounds: DOMRect,
players: Player[], components: TacticComponent[],
): number | -1 => { ): number | -1 => {
for (let i = 0; i < players.length; i++) { for (let i = 0; i < components.length; i++) {
const player = players[i] const component = components[i]
const playerBounds = document const playerBounds = document
.getElementById(player.id)! .getElementById(component.id)!
.getBoundingClientRect() .getBoundingClientRect()
const doesOverlap = !( if (overlaps(playerBounds, bounds)) {
bounds.top > playerBounds.bottom ||
bounds.right < playerBounds.left ||
bounds.bottom < playerBounds.top ||
bounds.left > playerBounds.right
)
if (doesOverlap) {
return i return i
} }
} }
return -1 return -1
} }
function updateActions(actions: Action[], players: Player[]) { function updateActions(actions: Action[], components: TacticComponent[]) {
return actions.map((action) => { return actions.map((action) => {
const originHasBall = players.find( const originHasBall = (
(p) => p.id == action.fromPlayerId, components.find(
)!.hasBall (p) => p.type == "player" && p.id == action.fromId,
)! as Player
).hasBall
let type = action.type let type = action.type
@ -280,80 +256,101 @@ function EditorView({
}) })
} }
const onBallDropOnPlayer = (playerCollidedIdx: number) => { const onBallDropOnComponent = (collidedComponentIdx: number) => {
setContent((content) => { setContent((content) => {
const ballObj = content.objects.findIndex((o) => o.type == "ball") const ballObj = content.components.findIndex(
let player = content.players.at(playerCollidedIdx) as Player (p) => p.type == BALL_TYPE,
const players = content.players.toSpliced(playerCollidedIdx, 1, { )
...player, let component = content.components[collidedComponentIdx]
if (component.type != "player") {
return content //do nothing if the ball isn't dropped on a player.
}
const components = content.components.toSpliced(
collidedComponentIdx,
1,
{
...component,
hasBall: true, hasBall: true,
}) },
)
// Maybe the ball is not present on the court as an object component
// if so, don't bother removing it from the court.
// This can occur if the user drags and drop the ball from a player that already has the ball
// to another component
if (ballObj != -1) {
components.splice(ballObj, 1)
}
return { return {
...content, ...content,
actions: updateActions(content.actions, players), actions: updateActions(content.actions, components),
players, components,
objects: content.objects.toSpliced(ballObj, 1),
} }
}) })
} }
const onBallDrop = (refBounds: DOMRect) => { const onBallMoved = (refBounds: DOMRect) => {
if (!isBoundsOnCourt(refBounds)) { if (!isBoundsOnCourt(refBounds)) {
removeCourtBall() removeCourtBall()
return return
} }
const playerCollidedIdx = getPlayerCollided(refBounds, content.players) const playerCollidedIdx = getComponentCollided(
refBounds,
content.components,
)
if (playerCollidedIdx != -1) { if (playerCollidedIdx != -1) {
setContent((content) => { setContent((content) => {
return { return {
...content, ...content,
players: content.players.map((player) => ({ components: content.components.map((c) =>
...player, c.type == "player"
? {
...c,
hasBall: false, hasBall: false,
})), }
: c,
),
} }
}) })
onBallDropOnPlayer(playerCollidedIdx) onBallDropOnComponent(playerCollidedIdx)
return return
} }
if (content.objects.findIndex((o) => o.type == "ball") != -1) { if (content.components.findIndex((o) => o.type == "ball") != -1) {
return return
} }
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const { x, y } = ratioWithinBase(refBounds, courtBounds) const { x, y } = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject const courtObject = {
type: BALL_TYPE,
courtObject = { id: BALL_ID,
type: "ball",
rightRatio: x, rightRatio: x,
bottomRatio: y, bottomRatio: y,
} } as Ball
const players = content.players.map((player) => ({ let components = content.components.map((c) =>
...player, c.type == "player"
? {
...c,
hasBall: false, hasBall: false,
})) }
: c,
)
components = [...components, courtObject]
setContent((content) => { setContent((content) => ({
return {
...content, ...content,
actions: updateActions(content.actions, players), actions: updateActions(content.actions, components),
players, components,
objects: [...content.objects, courtObject], }))
}
})
} }
const removePlayer = (player: Player) => { const removePlayer = (player: Player) => {
setContent((content) => ({ setContent((content) => ({
...content, ...content,
players: toSplicedPlayers(content.players, player, false), components: replaceOrInsert(content.components, player, false),
objects: [...content.objects],
actions: content.actions.filter( actions: content.actions.filter(
(a) => (a) => a.toId !== player.id && a.fromId !== player.id,
a.toPlayerId !== player.id && a.fromPlayerId !== player.id,
), ),
})) }))
let setter let setter
@ -379,14 +376,21 @@ function EditorView({
const removeCourtBall = () => { const removeCourtBall = () => {
setContent((content) => { setContent((content) => {
const ballObj = content.objects.findIndex((o) => o.type == "ball") const ballObj = content.components.findIndex(
(o) => o.type == "ball",
)
const components = content.components.map((c) =>
c.type == "player"
? ({
...c,
hasBall: false,
} as Player)
: c,
)
components.splice(ballObj, 1)
return { return {
...content, ...content,
players: content.players.map((player) => ({ components,
...player,
hasBall: false,
})),
objects: content.objects.toSpliced(ballObj, 1),
} }
}) })
setObjects([{ key: "ball" }]) setObjects([{ key: "ball" }])
@ -423,7 +427,7 @@ function EditorView({
canDetach={(div) => canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect()) isBoundsOnCourt(div.getBoundingClientRect())
} }
onElementDetached={onPieceDetach} onElementDetached={onRackPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
@ -441,7 +445,7 @@ function EditorView({
canDetach={(div) => canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect()) isBoundsOnCourt(div.getBoundingClientRect())
} }
onElementDetached={onObjectDetach} onElementDetached={onRackedObjectDetach}
render={renderCourtObject} render={renderCourtObject}
/> />
@ -452,7 +456,7 @@ function EditorView({
canDetach={(div) => canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect()) isBoundsOnCourt(div.getBoundingClientRect())
} }
onElementDetached={onPieceDetach} onElementDetached={onRackPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
@ -466,16 +470,14 @@ function EditorView({
<div id="court-div"> <div id="court-div">
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
players={content.players} components={content.components}
objects={content.objects}
actions={content.actions} actions={content.actions}
onBallMoved={onBallDrop} onBallMoved={onBallMoved}
courtImage={<Court courtType={courtType} />} courtImage={<Court courtType={courtType} />}
courtRef={courtDivContentRef} courtRef={courtDivContentRef}
setActions={(actions) => setActions={(actions) =>
setContent((content) => ({ setContent((content) => ({
...content, ...content,
players: content.players,
actions: actions(content.actions), actions: actions(content.actions),
})) }))
} }
@ -515,8 +517,8 @@ function EditorView({
} }
setContent((content) => ({ setContent((content) => ({
...content, ...content,
players: toSplicedPlayers( components: replaceOrInsert(
content.players, content.components,
player, player,
true, true,
), ),
@ -533,10 +535,11 @@ function EditorView({
} }
function isBallOnCourt(content: TacticContent) { function isBallOnCourt(content: TacticContent) {
if (content.players.findIndex((p) => p.hasBall) != -1) { return (
return true content.components.findIndex(
} (c) => (c.type == "player" && c.hasBall) || c.type == BALL_TYPE,
return content.objects.findIndex((o) => o.type == "ball") != -1 ) != -1
)
} }
function renderCourtObject(courtObject: RackedCourtObject) { function renderCourtObject(courtObject: RackedCourtObject) {
@ -558,12 +561,18 @@ function Court({ courtType }: { courtType: string }) {
) )
} }
function getRackPlayers(team: PlayerTeam, players: Player[]): RackedPlayer[] {
function getRackPlayers(
team: PlayerTeam,
components: TacticComponent[],
): RackedPlayer[] {
return ["1", "2", "3", "4", "5"] return ["1", "2", "3", "4", "5"]
.filter( .filter(
(role) => (role) =>
players.findIndex((p) => p.team == team && p.role == role) == components.findIndex(
-1, (c) =>
c.type == "player" && c.team == team && c.role == role,
) == -1,
) )
.map((key) => ({ team, key })) .map((key) => ({ team, key }))
} }
@ -611,14 +620,11 @@ function useContentState<S>(
return [content, setContentSynced, savingState] return [content, setContentSynced, savingState]
} }
function toSplicedPlayers( function replaceOrInsert<A extends TacticComponent>(
players: Player[], array: A[],
player: Player, it: A,
replace: boolean, replace: boolean,
): Player[] { ): A[] {
const idx = players.findIndex( const idx = array.findIndex((i) => i.id == it.id)
(p) => p.team === player.team && p.role === player.role, return array.toSpliced(idx, 1, ...(replace ? [it] : []))
)
return players.toSpliced(idx, 1, ...(replace ? [player] : []))
} }

@ -46,7 +46,7 @@ export function CourtAction({
}} }}
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.toPlayerId ? 26 : 17} endRadius={action.toId ? 26 : 17}
startRadius={0} startRadius={0}
onDeleteRequested={onActionDeleted} onDeleteRequested={onActionDeleted}
style={{ style={{

@ -17,7 +17,7 @@ export function Header({ username }: { username: string }) {
location.pathname = BASE + "/" location.pathname = BASE + "/"
}}> }}>
<span id="IQ">IQ</span> <span id="IQ">IQ</span>
<span id="Ball">Ball</span> <span id="Ball">CourtObjects</span>
</h1> </h1>
</div> </div>
<div id="header-right"> <div id="header-right">

@ -20,7 +20,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 '{"players": [], "actions": [], "objects": []}' NOT NULL, content varchar DEFAULT '{"components": [], "actions": []}' 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" => '{"players": [], "objects": [], "actions": []}', "content" => '{"components": [], "actions": []}',
"courtType" => $courtType->name(), "courtType" => $courtType->name(),
]); ]);
} }

@ -52,7 +52,7 @@
<body> <body>
<button onclick="location.pathname='{{ path('/disconnect') }}'"> Se déconnecter</button> <button onclick="location.pathname='{{ path('/disconnect') }}'"> Se déconnecter</button>
<div id="bandeau"> <div id="bandeau">
<h1>IQ Ball</h1> <h1>IQ CourtObjects</h1>
<div id="account" onclick="location.pathname='{{ path('/settings') }}'"> <div id="account" onclick="location.pathname='{{ path('/settings') }}'">
<img <img
src="{{ path('/assets/icon/account.svg') }}" src="{{ path('/assets/icon/account.svg') }}"

Loading…
Cancel
Save