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

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

@ -1,4 +1,10 @@
import {ReactElement, ReactNode, RefObject, useLayoutEffect, useState} from "react" import {
ReactElement,
ReactNode,
RefObject,
useLayoutEffect,
useState,
} from "react"
import { Action } from "../../model/tactic/Action" import { Action } from "../../model/tactic/Action"
import { CourtAction } from "./CourtAction.tsx" import { CourtAction } from "./CourtAction.tsx"
@ -30,12 +36,11 @@ export function BasketCourt({
courtImage, courtImage,
courtRef, courtRef,
}: BasketCourtProps) { }: BasketCourtProps) {
const [forceEmptyComponents, setForceEmptyComponents] = useState(true) const [forceEmptyComponents, setForceEmptyComponents] = useState(true)
useLayoutEffect(() => { useLayoutEffect(() => {
setForceEmptyComponents(false) setForceEmptyComponents(false)
}, [setForceEmptyComponents]); }, [setForceEmptyComponents])
const usedComponents = forceEmptyComponents ? [] : components const usedComponents = forceEmptyComponents ? [] : components
@ -56,10 +61,8 @@ export function BasketCourt({
origin={previewAction.origin} origin={previewAction.origin}
isInvalid={previewAction.isInvalid} isInvalid={previewAction.isInvalid}
//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={() => {
}}
/> />
)} )}
</div> </div>

@ -47,8 +47,11 @@ export default function CourtPlayer({
const pos = ratioWithinBase(pieceBounds, parentBounds) const pos = ratioWithinBase(pieceBounds, parentBounds)
if (
if (Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY || Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY) onPositionValidated(pos) Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY ||
Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY
)
onPositionValidated(pos)
}, [courtRef, onPositionValidated, x, y])}> }, [courtRef, onPositionValidated, x, y])}>
<div <div
id={playerInfo.id} id={playerInfo.id}

@ -1,6 +1,15 @@
import {BallState, Player, PlayerLike, PlayerPhantom} from "../model/tactic/Player" import {
BallState,
Player,
PlayerLike,
PlayerPhantom,
} from "../model/tactic/Player"
import { ratioWithinBase } from "../geo/Pos" import { ratioWithinBase } from "../geo/Pos"
import {ComponentId, TacticComponent, TacticContent} from "../model/tactic/Tactic" import {
ComponentId,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box" import { overlaps } from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action" import { Action, ActionKind, moves } from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains" import { removeBall, updateComponent } from "./TacticContentDomains"
@ -11,7 +20,7 @@ import {
getOrigin, getOrigin,
getPlayerNextTo, getPlayerNextTo,
isNextInPath, isNextInPath,
removePlayer removePlayer,
} from "./PlayerDomains" } from "./PlayerDomains"
import { BALL_TYPE } from "../model/tactic/CourtObjects" import { BALL_TYPE } from "../model/tactic/CourtObjects"
@ -209,7 +218,10 @@ export function createAction(
* 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.
*/ */
function createPhantom(forceHasBall: boolean, attachedTo?: ComponentId): ComponentId { function createPhantom(
forceHasBall: boolean,
attachedTo?: ComponentId,
): ComponentId {
const { x, y } = ratioWithinBase(arrowHead, courtBounds) const { x, y } = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number let itemIndex: number
@ -263,12 +275,12 @@ export function createAction(
pos: attachedTo pos: attachedTo
? { ? {
type: "follows", type: "follows",
attach: attachedTo attach: attachedTo,
} }
: { : {
type: "fixed", type: "fixed",
x, x,
y y,
}, },
originPlayerId: originPlayer.id, originPlayerId: originPlayer.id,
ballState: phantomState, ballState: phantomState,
@ -318,7 +330,6 @@ export function createAction(
} }
} }
return { return {
newContent: updateComponent( newContent: updateComponent(
{ {
@ -335,8 +346,9 @@ export function createAction(
const actionKind = getActionKind(null, origin.ballState).kind const actionKind = getActionKind(null, origin.ballState).kind
if (actionKind === ActionKind.SCREEN) 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 phantomId = createPhantom(false)
@ -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 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)! const screenPhantom = getPlayerNextTo(origin, 1, content.components)!
content = removePlayer(screenPhantom, content) content = removePlayer(screenPhantom, content)
} }
@ -466,7 +481,10 @@ export function spreadNewStateFromOriginStateChange(
continue continue
} }
const actionTarget: PlayerLike = getComponent(action.target, content.components) const actionTarget: PlayerLike = getComponent(
action.target,
content.components,
)
let targetState: BallState = actionTarget.ballState let targetState: BallState = actionTarget.ballState
let deleteAction = false let deleteAction = false
@ -496,8 +514,15 @@ export function spreadNewStateFromOriginStateChange(
action.type === ActionKind.SCREEN action.type === ActionKind.SCREEN
) { ) {
targetState = BallState.HOLDS_BY_PASS targetState = BallState.HOLDS_BY_PASS
const screenPhantom = getPlayerNextTo(origin, 1, content.components)! const screenPhantom = getPlayerNextTo(
if (screenPhantom.type === "phantom" && screenPhantom.pos.type === "follows") { origin,
1,
content.components,
)!
if (
screenPhantom.type === "phantom" &&
screenPhantom.pos.type === "follows"
) {
content = removePlayer(screenPhantom, content) content = removePlayer(screenPhantom, content)
origin = getComponent(origin.id, content.components) origin = getComponent(origin.id, content.components)
} }

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

@ -1,11 +1,26 @@
import { Pos, ratioWithinBase } from "../geo/Pos" import { Pos, ratioWithinBase } from "../geo/Pos"
import {BallState, Player, PlayerInfo, PlayerLike, 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,
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 { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems" import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains" import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains"
import {ActionKind} from "../model/tactic/Action.ts"; import { ActionKind } from "../model/tactic/Action.ts"
export function placePlayerAt( export function placePlayerAt(
refBounds: DOMRect, refBounds: DOMRect,
@ -167,20 +182,37 @@ export function moveComponent(
const originPathItems = origin.path!.items const originPathItems = origin.path!.items
const phantomIdx = originPathItems.indexOf(component.id) 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. // detach the action from the screen target and transform it to a regular move action to the phantom.
content = updateComponent({ content = updateComponent(
{
...playerBeforePhantom, ...playerBeforePhantom,
actions: playerBeforePhantom.actions.map(a => a.target === referent ? { actions: playerBeforePhantom.actions.map((a) =>
a.target === referent
? {
...a, ...a,
segments: a.segments.toSpliced(a.segments.length - 2, 1, { segments: a.segments.toSpliced(
a.segments.length - 2,
1,
{
...a.segments[a.segments.length - 1], ...a.segments[a.segments.length - 1],
next: component.id, next: component.id,
}), },
),
target: component.id, target: component.id,
type: ActionKind.MOVE type: ActionKind.MOVE,
} : a), }
}, content) : a,
),
},
content,
)
} }
content = updateComponent( content = updateComponent(
@ -189,9 +221,9 @@ export function moveComponent(
pos: isPhantom pos: isPhantom
? { ? {
type: "fixed", type: "fixed",
...newPos ...newPos,
} }
: newPos : newPos,
}, },
content, content,
) )

@ -34,7 +34,7 @@ export interface PlayerInfo {
*/ */
readonly ballState: BallState readonly ballState: BallState
readonly pos: Pos, readonly pos: Pos
} }
export enum BallState { export enum BallState {
@ -61,24 +61,26 @@ export interface MovementPath {
/** /**
* The position of the phantom is known and fixed * 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 position of the phantom is constrained to a given component.
* The actual position of the phantom is to determine given its environment. * 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 * 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 * A player phantom is a kind of component that represents the future state of a player
* according to the court's step information * 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 originPlayerId: ComponentId
readonly ballState: BallState readonly ballState: BallState

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

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

@ -198,17 +198,15 @@ function TableData({ allTactics }: { allTactics: Tactic[] }) {
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
return ( return (
<div id="body-personal-space"> <div id="body-personal-space">
{ {allTactics.length == 0 ? (
allTactics.length == 0 <p>Aucune tactique créée !</p>
? <p>Aucune tactique créée !</p> ) : (
:
<table> <table>
<tbody key="tbody"> <tbody key="tbody">
<TableData allTactics={allTactics} /> <TableData allTactics={allTactics} />
</tbody> </tbody>
</table> </table>
} )}
</div> </div>
) )
} }

Loading…
Cancel
Save