format
continuous-integration/drone/push Build is passing Details

pull/113/head
maxime.batista 1 year ago
parent 5963290f67
commit 5359bb12de

@ -1,8 +1,14 @@
import {ReactElement, ReactNode, RefObject, useLayoutEffect, useState} from "react"
import {Action} from "../../model/tactic/Action"
import {
ReactElement,
ReactNode,
RefObject,
useLayoutEffect,
useState,
} from "react"
import { Action } from "../../model/tactic/Action"
import {CourtAction} from "./CourtAction.tsx"
import {ComponentId, TacticComponent} from "../../model/tactic/Tactic"
import { CourtAction } from "./CourtAction.tsx"
import { ComponentId, TacticComponent } from "../../model/tactic/Tactic"
export interface BasketCourtProps {
components: TacticComponent[]
@ -21,21 +27,20 @@ export interface ActionPreview extends Action {
}
export function BasketCourt({
components,
previewAction,
components,
previewAction,
renderComponent,
renderActions,
courtImage,
courtRef,
}: BasketCourtProps) {
renderComponent,
renderActions,
courtImage,
courtRef,
}: BasketCourtProps) {
const [forceEmptyComponents, setForceEmptyComponents] = useState(true)
useLayoutEffect(() => {
setForceEmptyComponents(false)
}, [setForceEmptyComponents]);
}, [setForceEmptyComponents])
const usedComponents = forceEmptyComponents ? [] : components
@ -43,7 +48,7 @@ export function BasketCourt({
<div
className="court-container"
ref={courtRef}
style={{position: "relative"}}>
style={{ position: "relative" }}>
{courtImage}
{usedComponents.map(renderComponent)}
@ -56,10 +61,8 @@ export function BasketCourt({
origin={previewAction.origin}
isInvalid={previewAction.isInvalid}
//do nothing on interacted, not really possible as it's a preview arrow
onActionDeleted={() => {
}}
onActionChanges={() => {
}}
onActionDeleted={() => {}}
onActionChanges={() => {}}
/>
)}
</div>

@ -1,8 +1,8 @@
import {useRef} from "react"
import { useRef } from "react"
import Draggable from "react-draggable"
import {BallPiece} from "./BallPiece"
import {NULL_POS} from "../../geo/Pos"
import {Ball} from "../../model/tactic/CourtObjects"
import { BallPiece } from "./BallPiece"
import { NULL_POS } from "../../geo/Pos"
import { Ball } from "../../model/tactic/CourtObjects"
export interface CourtBallProps {
onPosValidated: (rect: DOMRect) => void
@ -10,10 +10,10 @@ export interface CourtBallProps {
ball: Ball
}
export function CourtBall({onPosValidated, ball, onRemove}: CourtBallProps) {
export function CourtBall({ onPosValidated, ball, onRemove }: CourtBallProps) {
const pieceRef = useRef<HTMLDivElement>(null)
const {x, y} = ball.pos
const { x, y } = ball.pos
return (
<Draggable
@ -34,7 +34,7 @@ export function CourtBall({onPosValidated, ball, onRemove}: CourtBallProps) {
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<BallPiece/>
<BallPiece />
</div>
</Draggable>
)

@ -1,9 +1,9 @@
import React, {ReactNode, RefObject, useCallback, useRef} from "react"
import React, { ReactNode, RefObject, useCallback, 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
@ -23,16 +23,16 @@ export const PLAYER_RADIUS_PIXELS = 20
* A player that is placed on the court, which can be selected, and moved in the associated bounds
* */
export default function CourtPlayer({
playerInfo,
className,
playerInfo,
className,
onPositionValidated,
onRemove,
courtRef,
availableActions,
}: CourtPlayerProps) {
onPositionValidated,
onRemove,
courtRef,
availableActions,
}: CourtPlayerProps) {
const usesBall = playerInfo.ballState != BallState.NONE
const {x, y} = playerInfo.pos
const { x, y } = playerInfo.pos
const pieceRef = useRef<HTMLDivElement>(null)
return (
@ -47,8 +47,11 @@ export default function CourtPlayer({
const pos = ratioWithinBase(pieceBounds, parentBounds)
if (Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY || Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY) onPositionValidated(pos)
if (
Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY ||
Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY
)
onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}>
<div
id={playerInfo.id}

@ -1,9 +1,18 @@
import {BallState, Player, PlayerLike, PlayerPhantom} from "../model/tactic/Player"
import {ratioWithinBase} from "../geo/Pos"
import {ComponentId, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {overlaps} from "../geo/Box"
import {Action, ActionKind, moves} from "../model/tactic/Action"
import {removeBall, updateComponent} from "./TacticContentDomains"
import {
BallState,
Player,
PlayerLike,
PlayerPhantom,
} from "../model/tactic/Player"
import { ratioWithinBase } from "../geo/Pos"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains"
import {
areInSamePath,
changePlayerBallState,
@ -11,9 +20,9 @@ import {
getOrigin,
getPlayerNextTo,
isNextInPath,
removePlayer
removePlayer,
} from "./PlayerDomains"
import {BALL_TYPE} from "../model/tactic/CourtObjects"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
export function getActionKind(
target: TacticComponent | null,
@ -22,12 +31,12 @@ export function getActionKind(
switch (ballState) {
case BallState.HOLDS_ORIGIN:
return target
? {kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN}
: {kind: ActionKind.DRIBBLE, nextState: ballState}
? { kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN }
: { kind: ActionKind.DRIBBLE, nextState: ballState }
case BallState.HOLDS_BY_PASS:
return target
? {kind: ActionKind.SHOOT, nextState: BallState.PASSED}
: {kind: ActionKind.DRIBBLE, nextState: ballState}
? { kind: ActionKind.SHOOT, nextState: BallState.PASSED }
: { kind: ActionKind.DRIBBLE, nextState: ballState }
case BallState.PASSED_ORIGIN:
case BallState.PASSED:
case BallState.NONE:
@ -209,8 +218,11 @@ export function createAction(
* Creates a new phantom component.
* Be aware that this function will reassign the `content` parameter.
*/
function createPhantom(forceHasBall: boolean, attachedTo?: ComponentId): ComponentId {
const {x, y} = ratioWithinBase(arrowHead, courtBounds)
function createPhantom(
forceHasBall: boolean,
attachedTo?: ComponentId,
): ComponentId {
const { x, y } = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number
let originPlayer: Player
@ -262,14 +274,14 @@ export function createAction(
id: phantomId,
pos: attachedTo
? {
type: "follows",
attach: attachedTo
}
type: "follows",
attach: attachedTo,
}
: {
type: "fixed",
x,
y
},
type: "fixed",
x,
y,
},
originPlayerId: originPlayer.id,
ballState: phantomState,
actions: [],
@ -308,17 +320,16 @@ export function createAction(
action = {
target: toId,
type: actionKind,
segments: [{next: toId}],
segments: [{ next: toId }],
}
} else {
action = {
target: toId,
type: actionKind,
segments: [{next: toId}],
segments: [{ next: toId }],
}
}
return {
newContent: updateComponent(
{
@ -335,15 +346,16 @@ export function createAction(
const actionKind = getActionKind(null, origin.ballState).kind
if (actionKind === ActionKind.SCREEN)
throw new Error("Attempted to create a screen action with nothing targeted")
throw new Error(
"Attempted to create a screen action with nothing targeted",
)
const phantomId = createPhantom(false)
const action: Action = {
target: phantomId,
type: actionKind,
segments: [{next: phantomId}],
segments: [{ next: phantomId }],
}
return {
newContent: updateComponent(
@ -429,7 +441,10 @@ export function removeAction(
}
// if the action type is a screen over a player, remove the phantom bound to the target
if (action.type === ActionKind.SCREEN && (origin.type === "phantom" || origin.type === "player")) {
if (
action.type === ActionKind.SCREEN &&
(origin.type === "phantom" || origin.type === "player")
) {
const screenPhantom = getPlayerNextTo(origin, 1, content.components)!
content = removePlayer(screenPhantom, content)
}
@ -466,7 +481,10 @@ export function spreadNewStateFromOriginStateChange(
continue
}
const actionTarget: PlayerLike = getComponent(action.target, content.components)
const actionTarget: PlayerLike = getComponent(
action.target,
content.components,
)
let targetState: BallState = actionTarget.ballState
let deleteAction = false
@ -496,8 +514,15 @@ export function spreadNewStateFromOriginStateChange(
action.type === ActionKind.SCREEN
) {
targetState = BallState.HOLDS_BY_PASS
const screenPhantom = getPlayerNextTo(origin, 1, content.components)!
if (screenPhantom.type === "phantom" && screenPhantom.pos.type === "follows") {
const screenPhantom = getPlayerNextTo(
origin,
1,
content.components,
)!
if (
screenPhantom.type === "phantom" &&
screenPhantom.pos.type === "follows"
) {
content = removePlayer(screenPhantom, content)
origin = getComponent(origin.id, content.components)
}
@ -509,7 +534,7 @@ export function spreadNewStateFromOriginStateChange(
i-- // step back
} else {
// do not change the action type if it is a shoot action
const {kind, nextState} = getActionKindBetween(
const { kind, nextState } = getActionKindBetween(
origin,
actionTarget,
newState,

@ -1,10 +1,30 @@
import {BallState, Player, PlayerLike, PlayerPhantom,} from "../model/tactic/Player"
import {ComponentId, TacticComponent, TacticContent} from "../model/tactic/Tactic"
import {removeComponent, updateComponent} from "./TacticContentDomains"
import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange,} from "./ActionsDomains"
import {ActionKind} from "../model/tactic/Action"
import {add, minus, norm, Pos, posWithinBase, ratioWithinBase, relativeTo} from "../geo/Pos.ts"
import {PLAYER_RADIUS_PIXELS} from "../components/editor/CourtPlayer.tsx";
import {
BallState,
Player,
PlayerLike,
PlayerPhantom,
} from "../model/tactic/Player"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { removeComponent, updateComponent } from "./TacticContentDomains"
import {
removeAllActionsTargeting,
spreadNewStateFromOriginStateChange,
} from "./ActionsDomains"
import { ActionKind } from "../model/tactic/Action"
import {
add,
minus,
norm,
Pos,
posWithinBase,
ratioWithinBase,
relativeTo,
} from "../geo/Pos.ts"
import { PLAYER_RADIUS_PIXELS } from "../components/editor/CourtPlayer.tsx"
export function getOrigin(
pathItem: PlayerPhantom,
@ -14,8 +34,13 @@ export function getOrigin(
return components.find((c) => c.id == pathItem.originPlayerId)! as Player
}
export function getPlayerNextTo(player: PlayerLike, n: number, components: TacticComponent[]): PlayerLike | undefined {
const playerOrigin = player.type === "player" ? player : getOrigin(player, components)
export function getPlayerNextTo(
player: PlayerLike,
n: number,
components: TacticComponent[],
): PlayerLike | undefined {
const playerOrigin =
player.type === "player" ? player : getOrigin(player, components)
const pathItems = playerOrigin.path?.items!
// add one as there is a shifting because a Player is never at the head of its own path
@ -24,20 +49,24 @@ export function getPlayerNextTo(player: PlayerLike, n: number, components: Tacti
const targetIdx = idx + n
// remove the screen phantom
const result = targetIdx == 0 ? playerOrigin : getComponent<PlayerLike>(pathItems[targetIdx - 1], components)
const result =
targetIdx == 0
? playerOrigin
: getComponent<PlayerLike>(pathItems[targetIdx - 1], components)
return result
}
//FIXME this function can be a bottleneck if the phantom's position is
// following another phantom and / or the origin of the phantom is another
export function computePhantomPositioning(phantom: PlayerPhantom,
content: TacticContent,
area: DOMRect): Pos {
export function computePhantomPositioning(
phantom: PlayerPhantom,
content: TacticContent,
area: DOMRect,
): Pos {
const positioning = phantom.pos
// If the position is already known and fixed, return the pos
if (positioning.type === "fixed")
return positioning
if (positioning.type === "fixed") return positioning
// If the position is to determine (positioning.type = "follows"), determine the phantom's pos
// by calculating it from the referent position, and the action that targets the referent.
@ -46,44 +75,56 @@ export function computePhantomPositioning(phantom: PlayerPhantom,
// Get the referent from the components
const referent: PlayerLike = getComponent(positioning.attach, components)
const referentPos = referent.type === "player"
? referent.pos
: computePhantomPositioning(referent, content, area)
const referentPos =
referent.type === "player"
? referent.pos
: computePhantomPositioning(referent, content, area)
// Get the origin
const origin = getOrigin(phantom, components)
const originPathItems = origin.path!.items
const phantomIdx = originPathItems.indexOf(phantom.id)
const playerBeforePhantom: PlayerLike = phantomIdx == 0 ? origin : getComponent(originPathItems[phantomIdx - 1], components)
const action = playerBeforePhantom.actions.find(a => a.target === positioning.attach)!
const playerBeforePhantom: PlayerLike =
phantomIdx == 0
? origin
: getComponent(originPathItems[phantomIdx - 1], components)
const action = playerBeforePhantom.actions.find(
(a) => a.target === positioning.attach,
)!
const segments = action.segments
const lastSegment = segments[segments.length - 1]
const lastSegmentStart = segments[segments.length - 2]?.next
const pivotPoint = lastSegment.controlPoint ?? (lastSegmentStart
? typeof lastSegmentStart === "string"
? document.getElementById(lastSegmentStart)!.getBoundingClientRect()
: lastSegmentStart
: playerBeforePhantom.type === "phantom"
? computePhantomPositioning(playerBeforePhantom, content, area)
: playerBeforePhantom.pos)
const pivotPoint =
lastSegment.controlPoint ??
(lastSegmentStart
? typeof lastSegmentStart === "string"
? document
.getElementById(lastSegmentStart)!
.getBoundingClientRect()
: lastSegmentStart
: playerBeforePhantom.type === "phantom"
? computePhantomPositioning(playerBeforePhantom, content, area)
: playerBeforePhantom.pos)
const segment = posWithinBase(relativeTo(referentPos, pivotPoint), area)
const segmentLength = norm(segment)
const phantomDistanceFromReferent = PLAYER_RADIUS_PIXELS //TODO Place this in constants
const segmentProjection = minus(area, {
x: (segment.x / segmentLength) * phantomDistanceFromReferent,
y: (segment.y / segmentLength) * phantomDistanceFromReferent
y: (segment.y / segmentLength) * phantomDistanceFromReferent,
})
const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area)
return add(referentPos, segmentProjectionRatio)
}
export function getComponent<T extends TacticComponent>(id: string, components: TacticComponent[]): T {
return components.find(c => c.id === id)! as T
export function getComponent<T extends TacticComponent>(
id: string,
components: TacticComponent[],
): T {
return components.find((c) => c.id === id)! as T
}
export function areInSamePath(a: PlayerLike, b: PlayerLike) {
@ -141,13 +182,18 @@ export function clearPlayerPath(
)
}
function removeAllPhantomsAttached(to: ComponentId, content: TacticContent): TacticContent {
function removeAllPhantomsAttached(
to: ComponentId,
content: TacticContent,
): TacticContent {
let i = 0
while (i < content.components.length) {
const component = content.components[i]
if (component.type === "phantom") {
if (component.pos.type === "follows" && component.pos.attach === to) {
if (
component.pos.type === "follows" &&
component.pos.attach === to
) {
content = removePlayer(component, content)
continue
}
@ -165,16 +211,24 @@ export function removePlayer(
content = removeAllPhantomsAttached(player.id, content)
if (player.type === "phantom") {
const pos = player.pos
// if the phantom was attached to another player, remove the action that symbolizes the attachment
if (pos.type === "follows") {
const playerBefore = getPlayerNextTo(player, -1, content.components)!
const actionIdx = playerBefore.actions.findIndex(a => a.target === pos.attach)
content = updateComponent({
...playerBefore,
actions: playerBefore.actions.toSpliced(actionIdx, 1)
}, content)
const playerBefore = getPlayerNextTo(
player,
-1,
content.components,
)!
const actionIdx = playerBefore.actions.findIndex(
(a) => a.target === pos.attach,
)
content = updateComponent(
{
...playerBefore,
actions: playerBefore.actions.toSpliced(actionIdx, 1),
},
content,
)
}
const origin = getOrigin(player, content.components)
@ -229,9 +283,9 @@ export function truncatePlayerPath(
truncateStartIdx == 0
? null
: {
...path,
items: path.items.toSpliced(truncateStartIdx),
},
...path,
items: path.items.toSpliced(truncateStartIdx),
},
},
content,
)

@ -1,11 +1,26 @@
import {Pos, ratioWithinBase} from "../geo/Pos"
import {BallState, Player, PlayerInfo, PlayerLike, PlayerTeam,} from "../model/tactic/Player"
import {Ball, BALL_ID, BALL_TYPE, CourtObject,} from "../model/tactic/CourtObjects"
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
import {overlaps} from "../geo/Box"
import {RackedCourtObject, RackedPlayer} from "./RackedItems"
import {changePlayerBallState, getComponent, getOrigin} from "./PlayerDomains"
import {ActionKind} from "../model/tactic/Action.ts";
import { Pos, ratioWithinBase } from "../geo/Pos"
import {
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import {
Ball,
BALL_ID,
BALL_TYPE,
CourtObject,
} from "../model/tactic/CourtObjects"
import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts"
export function placePlayerAt(
refBounds: DOMRect,
@ -167,20 +182,37 @@ export function moveComponent(
const originPathItems = origin.path!.items
const phantomIdx = originPathItems.indexOf(component.id)
const playerBeforePhantom: PlayerLike = phantomIdx == 0 ? origin : getComponent(originPathItems[phantomIdx - 1], content.components)
const playerBeforePhantom: PlayerLike =
phantomIdx == 0
? origin
: getComponent(
originPathItems[phantomIdx - 1],
content.components,
)
// detach the action from the screen target and transform it to a regular move action to the phantom.
content = updateComponent({
...playerBeforePhantom,
actions: playerBeforePhantom.actions.map(a => a.target === referent ? {
...a,
segments: a.segments.toSpliced(a.segments.length - 2, 1, {
...a.segments[a.segments.length - 1],
next: component.id,
}),
target: component.id,
type: ActionKind.MOVE
} : a),
}, content)
content = updateComponent(
{
...playerBeforePhantom,
actions: playerBeforePhantom.actions.map((a) =>
a.target === referent
? {
...a,
segments: a.segments.toSpliced(
a.segments.length - 2,
1,
{
...a.segments[a.segments.length - 1],
next: component.id,
},
),
target: component.id,
type: ActionKind.MOVE,
}
: a,
),
},
content,
)
}
content = updateComponent(
@ -188,10 +220,10 @@ export function moveComponent(
...component,
pos: isPhantom
? {
type: "fixed",
...newPos
}
: newPos
type: "fixed",
...newPos,
}
: newPos,
},
content,
)
@ -253,5 +285,5 @@ export function getRackPlayers(
c.type == "player" && c.team == team && c.role == role,
) == -1,
)
.map((key) => ({team, key}))
.map((key) => ({ team, key }))
}

@ -34,7 +34,7 @@ export interface PlayerInfo {
*/
readonly ballState: BallState
readonly pos: Pos,
readonly pos: Pos
}
export enum BallState {
@ -61,24 +61,26 @@ export interface MovementPath {
/**
* The position of the phantom is known and fixed
*/
export type FixedPhantomPositioning = ({ type: "fixed" } & Pos)
export type FixedPhantomPositioning = { type: "fixed" } & Pos
/**
* The position of the phantom is constrained to a given component.
* The actual position of the phantom is to determine given its environment.
*/
export type FollowsPhantomPositioning = { type: "follows", attach: ComponentId }
export type FollowsPhantomPositioning = { type: "follows"; attach: ComponentId }
/**
* Defines the different kind of positioning a phantom can have
*/
export type PhantomPositioning = FixedPhantomPositioning | FollowsPhantomPositioning
export type PhantomPositioning =
| FixedPhantomPositioning
| FollowsPhantomPositioning
/**
* A player phantom is a kind of component that represents the future state of a player
* according to the court's step information
*/
export interface PlayerPhantom extends Component<"phantom", PhantomPositioning> {
export interface PlayerPhantom
extends Component<"phantom", PhantomPositioning> {
readonly originPlayerId: ComponentId
readonly ballState: BallState

@ -28,7 +28,7 @@ export interface Component<T, Positioning> {
*/
readonly id: ComponentId
readonly pos: Positioning,
readonly pos: Positioning
readonly actions: Action[]
}

@ -403,7 +403,11 @@ function EditorView({
id: component.id,
team: origin.team,
role: origin.role,
pos: computePhantomPositioning(component, content, courtBounds()),
pos: computePhantomPositioning(
component,
content,
courtBounds(),
),
ballState: component.ballState,
}
} else {

@ -57,10 +57,10 @@ export default function HomePage() {
}
function Home({
lastTactics,
allTactics,
teams,
}: {
lastTactics,
allTactics,
teams,
}: {
lastTactics: Tactic[]
allTactics: Tactic[]
teams: Team[]
@ -77,10 +77,10 @@ function Home({
}
function Body({
lastTactics,
allTactics,
teams,
}: {
lastTactics,
allTactics,
teams,
}: {
lastTactics: Tactic[]
allTactics: Tactic[]
teams: Team[]
@ -100,10 +100,10 @@ function Body({
}
function SideMenu({
width,
lastTactics,
teams,
}: {
width,
lastTactics,
teams,
}: {
width: number
lastTactics: Tactic[]
teams: Team[]
@ -123,9 +123,9 @@ function SideMenu({
}
function PersonalSpace({
width,
allTactics,
}: {
width,
allTactics,
}: {
width: number
allTactics: Tactic[]
}) {
@ -198,17 +198,15 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
return (
<div id="body-personal-space">
{
allTactics.length == 0
? <p>Aucune tactique créée !</p>
:
<table>
<tbody key="tbody">
{allTactics.length == 0 ? (
<p>Aucune tactique créée !</p>
) : (
<table>
<tbody key="tbody">
<TableData allTactics={allTactics} />
</tbody>
</table>
}
</tbody>
</table>
)}
</div>
)
}

Loading…
Cancel
Save