(null)
return (
@@ -44,7 +47,8 @@ export default function CourtPlayer({
const pos = ratioWithinBase(pieceBounds, parentBounds)
- if (pos.x !== x || pos.y != y) 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])}>
c.id == pathItem.originPlayerId)! as Player
}
+//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 {
+ const positioning = phantom.pos
+
+ // If the position is already known and fixed, return the pos
+ 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.
+
+ const components = content.components
+
+ // Get the referent from the components
+ const referent: PlayerLike = getComponent(positioning.attach, components)
+ 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 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 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
+ })
+ const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area)
+
+ return add(referentPos, segmentProjectionRatio)
+}
+
+export function getComponent(id: string, components: TacticComponent[]): T {
+ return components.find(c => c.id === id)! as T
+}
+
export function areInSamePath(a: PlayerLike, b: PlayerLike) {
if (a.type === "phantom" && b.type === "phantom") {
return a.originPlayerId === b.originPlayerId
@@ -132,9 +185,9 @@ export function truncatePlayerPath(
truncateStartIdx == 0
? null
: {
- ...path,
- items: path.items.toSpliced(truncateStartIdx),
- },
+ ...path,
+ items: path.items.toSpliced(truncateStartIdx),
+ },
},
content,
)
diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts
index 5839bee..a8d02d3 100644
--- a/src/editor/TacticContentDomains.ts
+++ b/src/editor/TacticContentDomains.ts
@@ -1,39 +1,25 @@
-import { Pos, ratioWithinBase } from "../geo/Pos"
-import {
- BallState,
- Player,
- PlayerInfo,
- PlayerTeam,
-} from "../model/tactic/Player"
-import {
- Ball,
- BALL_ID,
- BALL_TYPE,
- CourtObject,
-} from "../model/tactic/CourtObjects"
-import {
- ComponentId,
- TacticComponent,
- TacticContent,
-} from "../model/tactic/Tactic"
-import { overlaps } from "../geo/Box"
-import { RackedCourtObject, RackedPlayer } from "./RackedItems"
-import { changePlayerBallState } from "./PlayerDomains"
+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,
courtBounds: DOMRect,
element: RackedPlayer,
): Player {
- const { x, y } = ratioWithinBase(refBounds, courtBounds)
+ const pos = ratioWithinBase(refBounds, courtBounds)
return {
type: "player",
id: "player-" + element.key + "-" + element.team,
team: element.team,
role: element.key,
- rightRatio: x,
- bottomRatio: y,
+ pos,
ballState: BallState.NONE,
path: null,
actions: [],
@@ -46,7 +32,7 @@ export function placeObjectAt(
rackedObject: RackedCourtObject,
content: TacticContent,
): TacticContent {
- const { x, y } = ratioWithinBase(refBounds, courtBounds)
+ const pos = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject
@@ -64,8 +50,7 @@ export function placeObjectAt(
courtObject = {
type: BALL_TYPE,
id: BALL_ID,
- rightRatio: x,
- bottomRatio: y,
+ pos,
actions: [],
}
break
@@ -134,13 +119,12 @@ export function placeBallAt(
const ballIdx = content.components.findIndex((o) => o.type == "ball")
- const { x, y } = ratioWithinBase(refBounds, courtBounds)
+ const pos = ratioWithinBase(refBounds, courtBounds)
const ball: Ball = {
type: BALL_TYPE,
id: BALL_ID,
- rightRatio: x,
- bottomRatio: y,
+ pos,
actions: [],
}
@@ -174,14 +158,44 @@ export function moveComponent(
if (!overlaps(playerBounds, courtBounds)) {
return removed(content)
}
- return updateComponent(
- {
+
+ const isPhantom = component.type === "phantom"
+
+ if (isPhantom && component.pos.type === "follows") {
+ const referent = component.pos.attach
+ const origin = getOrigin(component, content.components)
+ const originPathItems = origin.path!.items
+ const phantomIdx = originPathItems.indexOf(component.id)
+
+ 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(
+ {
...component,
- rightRatio: newPos.x,
- bottomRatio: newPos.y,
+ pos: isPhantom
+ ? {
+ type: "fixed",
+ ...newPos
+ }
+ : newPos
},
content,
)
+ return content
}
export function removeComponent(
@@ -239,5 +253,5 @@ export function getRackPlayers(
c.type == "player" && c.team == team && c.role == role,
) == -1,
)
- .map((key) => ({ team, key }))
+ .map((key) => ({team, key}))
}
diff --git a/src/model/tactic/Action.ts b/src/model/tactic/Action.ts
index c97cdd4..b2dca4f 100644
--- a/src/model/tactic/Action.ts
+++ b/src/model/tactic/Action.ts
@@ -9,9 +9,10 @@ export enum ActionKind {
SHOOT = "SHOOT",
}
-export type Action = { type: ActionKind } & MovementAction
+export type Action = MovementAction
export interface MovementAction {
+ type: ActionKind
target: ComponentId | Pos
segments: Segment[]
}
diff --git a/src/model/tactic/CourtObjects.ts b/src/model/tactic/CourtObjects.ts
index 96cde26..5f72199 100644
--- a/src/model/tactic/CourtObjects.ts
+++ b/src/model/tactic/CourtObjects.ts
@@ -1,4 +1,5 @@
import { Component } from "./Tactic"
+import { Pos } from "../../geo/Pos.ts"
export const BALL_ID = "ball"
export const BALL_TYPE = "ball"
@@ -6,4 +7,4 @@ export const BALL_TYPE = "ball"
//place here all different kinds of objects
export type CourtObject = Ball
-export type Ball = Component
+export type Ball = Component
diff --git a/src/model/tactic/Player.ts b/src/model/tactic/Player.ts
index a257103..ab4d116 100644
--- a/src/model/tactic/Player.ts
+++ b/src/model/tactic/Player.ts
@@ -1,4 +1,5 @@
import { Component, ComponentId } from "./Tactic"
+import { Pos } from "../../geo/Pos.ts"
export type PlayerId = string
@@ -9,7 +10,7 @@ export enum PlayerTeam {
Opponents = "opponents",
}
-export interface Player extends PlayerInfo, Component<"player"> {
+export interface Player extends PlayerInfo, Component<"player", Pos> {
readonly id: PlayerId
}
@@ -33,15 +34,7 @@ export interface PlayerInfo {
*/
readonly ballState: BallState
- /**
- * 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
+ readonly pos: Pos,
}
export enum BallState {
@@ -52,7 +45,7 @@ export enum BallState {
PASSED_ORIGIN,
}
-export interface Player extends Component<"player">, PlayerInfo {
+export interface Player extends Component<"player", Pos>, PlayerInfo {
/**
* True if the player has a basketball
*/
@@ -65,11 +58,32 @@ export interface MovementPath {
readonly items: ComponentId[]
}
+/**
+ * The position of the phantom is known and fixed
+ */
+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 }
+
+/**
+ * Defines the different kind of positioning a phantom can have
+ */
+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"> {
+export interface PlayerPhantom extends Component<"phantom", PhantomPositioning> {
readonly originPlayerId: ComponentId
readonly ballState: BallState
+
+ /**
+ * Defines a component this phantom will be attached to.
+ */
+ readonly attachedTo?: ComponentId
}
diff --git a/src/model/tactic/Tactic.ts b/src/model/tactic/Tactic.ts
index acce6f0..b22473f 100644
--- a/src/model/tactic/Tactic.ts
+++ b/src/model/tactic/Tactic.ts
@@ -18,7 +18,7 @@ export interface TacticContent {
export type TacticComponent = Player | CourtObject | PlayerPhantom
export type ComponentId = string
-export interface Component {
+export interface Component {
/**
* The component's type
*/
@@ -27,15 +27,8 @@ export interface Component {
* The component's identifier
*/
readonly id: ComponentId
- /**
- * Percentage of the component's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
- */
- readonly bottomRatio: number
- /**
- * Percentage of the component's position to the right (0 means left, 1 means right, 0.5 means middle)
- */
- readonly rightRatio: number
+ readonly pos: Positioning,
readonly actions: Action[]
}
diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx
index 7f2b797..f8d6d97 100644
--- a/src/pages/Editor.tsx
+++ b/src/pages/Editor.tsx
@@ -67,7 +67,7 @@ import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction"
import {
- changePlayerBallState,
+ changePlayerBallState, computePhantomPositioning,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
@@ -329,7 +329,7 @@ function EditorView({
content,
(content) => {
- if (player.type == "player") insertRackedPlayer(player)
+ if (player.type === "player") insertRackedPlayer(player)
return removePlayer(player, content)
},
),
@@ -402,8 +402,7 @@ function EditorView({
id: component.id,
team: origin.team,
role: origin.role,
- bottomRatio: component.bottomRatio,
- rightRatio: component.rightRatio,
+ pos: computePhantomPositioning(component, content, courtBounds()),
ballState: component.ballState,
}
} else {
diff --git a/src/style/steps_tree.css b/src/style/steps_tree.css
deleted file mode 100644
index eadeaf6..0000000
--- a/src/style/steps_tree.css
+++ /dev/null
@@ -1,87 +0,0 @@
-.step-piece {
- position: relative;
- font-family: monospace;
- pointer-events: all;
-
- background-color: var(--editor-tree-step-piece);
- color: var(--selected-team-secondarycolor);
-
- border-radius: 100px;
-
- width: 20px;
- height: 20px;
-
- display: flex;
-
- align-items: center;
- justify-content: center;
-
- user-select: none;
- cursor: pointer;
-
- border: 2px solid var(--editor-tree-background);
-}
-
-.step-piece-selected {
- border: 2px solid var(--selection-color-light);
-}
-
-.step-piece-selected,
-.step-piece:focus,
-.step-piece:hover {
- background-color: var(--editor-tree-step-piece-hovered);
-}
-
-.step-piece-actions {
- display: none;
- position: absolute;
- column-gap: 5px;
- top: -140%;
-}
-
-.step-piece-selected .step-piece-actions {
- display: flex;
-}
-
-.add-icon,
-.remove-icon {
- background-color: white;
- border-radius: 100%;
-}
-
-.add-icon {
- fill: var(--add-icon-fill);
-}
-
-.remove-icon {
- fill: var(--remove-icon-fill);
-}
-
-.step-children {
- margin-top: 10vh;
- display: flex;
- flex-direction: row;
- width: 100%;
- height: 100%;
-}
-
-.step-group {
- position: relative;
-
- display: flex;
- flex-direction: column;
- align-items: center;
-
- width: 100%;
- height: 100%;
-}
-
-.steps-tree {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- padding-top: 10%;
-
- height: 100%;
-}