@ -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/CourtObjects"
|
||||||
|
|
||||||
export function BallPiece() {
|
export function BallPiece() {
|
||||||
return <BallSvg className={"ball"} />
|
return <BallSvg id={BALL_ID} className={"ball"} />
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,510 @@
|
|||||||||||||
|
import {
|
||||||||||||
|
BallState,
|
||||||||||||
|
Player,
|
||||||||||||
|
PlayerPhantom,
|
||||||||||||
|
PlayerLike,
|
||||||||||||
|
} 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,
|
||||||||||||
|
getOrigin,
|
||||||||||||
|
isNextInPath,
|
||||||||||||
|
removePlayer,
|
||||||||||||
|
} from "./PlayerDomains"
|
||||||||||||
|
import { BALL_TYPE } from "../model/tactic/CourtObjects"
|
||||||||||||
|
|
||||||||||||
|
export function getActionKind(
|
||||||||||||
|
target: TacticComponent | null,
|
||||||||||||
|
ballState: BallState,
|
||||||||||||
|
): { kind: ActionKind; nextState: BallState } {
|
||||||||||||
|
switch (ballState) {
|
||||||||||||
|
case BallState.HOLDS_ORIGIN:
|
||||||||||||
|
return target
|
||||||||||||
|
? { 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 }
|
||||||||||||
|
case BallState.PASSED_ORIGIN:
|
||||||||||||
|
case BallState.PASSED:
|
||||||||||||
|
case BallState.NONE:
|
||||||||||||
|
return {
|
||||||||||||
|
kind:
|
||||||||||||
|
target && target.type != BALL_TYPE
|
||||||||||||
|
? ActionKind.SCREEN
|
||||||||||||
|
: ActionKind.MOVE,
|
||||||||||||
|
nextState: ballState,
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function getActionKindBetween(
|
||||||||||||
|
origin: PlayerLike,
|
||||||||||||
|
target: TacticComponent | null,
|
||||||||||||
|
state: BallState,
|
||||||||||||
|
): { kind: ActionKind; nextState: BallState } {
|
||||||||||||
|
//remove the target if the target is a phantom that is within the origin's path
|
||||||||||||
|
if (
|
||||||||||||
|
target != null &&
|
||||||||||||
|
target.type == "phantom" &&
|
||||||||||||
|
areInSamePath(origin, target)
|
||||||||||||
|
) {
|
||||||||||||
|
target = null
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return getActionKind(target, state)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function isActionValid(
|
||||||||||||
|
origin: TacticComponent,
|
||||||||||||
|
target: TacticComponent | null,
|
||||||||||||
|
components: TacticComponent[],
|
||||||||||||
|
): boolean {
|
||||||||||||
|
/// action is valid if the origin is neither a phantom nor a player
|
||||||||||||
|
if (origin.type != "phantom" && origin.type != "player") {
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
// action is invalid if the origin already moves (unless the origin holds a ball which will lead to a ball pass)
|
||||||||||||
|
if (
|
||||||||||||
|
origin.ballState != BallState.HOLDS_BY_PASS &&
|
||||||||||||
|
origin.ballState != BallState.HOLDS_ORIGIN &&
|
||||||||||||
|
origin.actions.find((a) => moves(a.type))
|
||||||||||||
|
) {
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
//Action is valid if the target is null
|
||||||||||||
|
if (target == null) {
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
// action is invalid if it targets its own origin
|
||||||||||||
|
if (origin.id === target.id) {
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
// action is invalid if the target already moves and is not indirectly bound with origin
|
||||||||||||
|
if (
|
||||||||||||
|
target.actions.find((a) => moves(a.type)) &&
|
||||||||||||
|
(hasBoundWith(target, origin, components) ||
|
||||||||||||
|
hasBoundWith(origin, target, components))
|
||||||||||||
|
) {
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
// Action is invalid if there is already an action between origin and target.
|
||||||||||||
|
if (
|
||||||||||||
|
origin.actions.find((a) => a.target === target?.id) ||
|
||||||||||||
|
target?.actions.find((a) => a.target === origin.id)
|
||||||||||||
|
) {
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
// Action is invalid if there is already an anterior action within the target's path
|
||||||||||||
|
if (target.type == "phantom" || target.type == "player") {
|
||||||||||||
|
// cant have an action with current path
|
||||||||||||
|
if (areInSamePath(origin, target)) return false
|
||||||||||||
|
|
||||||||||||
|
if (alreadyHasAnAnteriorActionWith(origin, target, components)) {
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
function hasBoundWith(
|
||||||||||||
|
origin: TacticComponent,
|
||||||||||||
|
target: TacticComponent,
|
||||||||||||
|
components: TacticComponent[],
|
||||||||||||
|
): boolean {
|
||||||||||||
|
const toVisit = [origin.id]
|
||||||||||||
|
const visited: string[] = []
|
||||||||||||
|
|
||||||||||||
|
let itemId: string | undefined
|
||||||||||||
|
while ((itemId = toVisit.pop())) {
|
||||||||||||
|
if (visited.indexOf(itemId) !== -1) continue
|
||||||||||||
|
|
||||||||||||
|
visited.push(itemId)
|
||||||||||||
|
|
||||||||||||
|
const item = components.find((c) => c.id === itemId)!
|
||||||||||||
|
|
||||||||||||
|
const itemBounds = item.actions.flatMap((a) =>
|
||||||||||||
|
typeof a.target == "string" ? [a.target] : [],
|
||||||||||||
|
)
|
||||||||||||
|
if (itemBounds.indexOf(target.id) !== -1) {
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
toVisit.push(...itemBounds)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
function alreadyHasAnAnteriorActionWith(
|
||||||||||||
|
origin: PlayerLike,
|
||||||||||||
|
target: PlayerLike,
|
||||||||||||
|
components: TacticComponent[],
|
||||||||||||
|
): boolean {
|
||||||||||||
|
const targetOrigin =
|
||||||||||||
|
target.type === "phantom" ? getOrigin(target, components) : target
|
||||||||||||
|
const targetOriginPath = [
|
||||||||||||
|
targetOrigin.id,
|
||||||||||||
|
...(targetOrigin.path?.items ?? []),
|
||||||||||||
|
]
|
||||||||||||
|
|
||||||||||||
|
const originOrigin =
|
||||||||||||
|
origin.type === "phantom" ? getOrigin(origin, components) : origin
|
||||||||||||
|
const originOriginPath = [
|
||||||||||||
|
originOrigin.id,
|
||||||||||||
|
...(originOrigin.path?.items ?? []),
|
||||||||||||
|
]
|
||||||||||||
|
|
||||||||||||
|
const targetIdx = targetOriginPath.indexOf(target.id)
|
||||||||||||
|
for (let i = targetIdx; i < targetOriginPath.length; i++) {
|
||||||||||||
|
const phantom = components.find(
|
||||||||||||
|
(c) => c.id === targetOriginPath[i],
|
||||||||||||
|
)! as PlayerLike
|
||||||||||||
|
if (
|
||||||||||||
|
phantom.actions.find(
|
||||||||||||
|
(a) =>
|
||||||||||||
|
typeof a.target === "string" &&
|
||||||||||||
|
moves(a.type) &&
|
||||||||||||
|
originOriginPath.indexOf(a.target) !== -1,
|
||||||||||||
|
)
|
||||||||||||
|
) {
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const originIdx = originOriginPath.indexOf(origin.id)
|
||||||||||||
clement.freville2
commented 1 year ago
Review
|
|||||||||||||
|
for (let i = 0; i <= originIdx; i++) {
|
||||||||||||
|
const phantom = components.find(
|
||||||||||||
|
(c) => c.id === originOriginPath[i],
|
||||||||||||
|
)! as PlayerLike
|
||||||||||||
|
if (
|
||||||||||||
|
phantom.actions.find(
|
||||||||||||
|
(a) =>
|
||||||||||||
|
typeof a.target === "string" &&
|
||||||||||||
|
moves(a.type) &&
|
||||||||||||
|
targetOriginPath.indexOf(a.target) > targetIdx,
|
||||||||||||
|
)
|
||||||||||||
|
) {
|
||||||||||||
|
return true
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return false
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function createAction(
|
||||||||||||
|
origin: PlayerLike,
|
||||||||||||
|
courtBounds: DOMRect,
|
||||||||||||
|
arrowHead: DOMRect,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): { createdAction: Action; newContent: TacticContent } {
|
||||||||||||
|
/**
|
||||||||||||
|
* Creates a new phantom component.
|
||||||||||||
|
* Be aware that this function will reassign the `content` parameter.
|
||||||||||||
|
*/
|
||||||||||||
|
function createPhantom(forceHasBall: boolean): ComponentId {
|
||||||||||||
|
const { x, y } = ratioWithinBase(arrowHead, courtBounds)
|
||||||||||||
|
|
||||||||||||
|
let itemIndex: number
|
||||||||||||
|
let originPlayer: Player
|
||||||||||||
|
|
||||||||||||
|
if (origin.type == "phantom") {
|
||||||||||||
|
// if we create a phantom from another phantom,
|
||||||||||||
|
// simply add it to the phantom's path
|
||||||||||||
|
const originPlr = getOrigin(origin, content.components)!
|
||||||||||||
|
itemIndex = originPlr.path!.items.length
|
||||||||||||
|
originPlayer = originPlr
|
||||||||||||
|
} else {
|
||||||||||||
|
// if we create a phantom directly from a player
|
||||||||||||
|
// create a new path and add it into
|
||||||||||||
|
itemIndex = 0
|
||||||||||||
|
originPlayer = origin
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const path = originPlayer.path
|
||||||||||||
|
|
||||||||||||
|
const phantomId = "phantom-" + itemIndex + "-" + originPlayer.id
|
||||||||||||
|
|
||||||||||||
|
content = updateComponent(
|
||||||||||||
|
{
|
||||||||||||
|
...originPlayer,
|
||||||||||||
|
path: {
|
||||||||||||
|
items: path ? [...path.items, phantomId] : [phantomId],
|
||||||||||||
|
},
|
||||||||||||
|
},
|
||||||||||||
|
content,
|
||||||||||||
|
)
|
||||||||||||
|
|
||||||||||||
|
let phantomState: BallState
|
||||||||||||
|
if (forceHasBall) phantomState = BallState.HOLDS_ORIGIN
|
||||||||||||
|
else
|
||||||||||||
|
switch (origin.ballState) {
|
||||||||||||
|
case BallState.HOLDS_ORIGIN:
|
||||||||||||
|
phantomState = BallState.HOLDS_BY_PASS
|
||||||||||||
|
break
|
||||||||||||
|
case BallState.PASSED:
|
||||||||||||
|
case BallState.PASSED_ORIGIN:
|
||||||||||||
|
phantomState = BallState.NONE
|
||||||||||||
|
break
|
||||||||||||
|
default:
|
||||||||||||
|
phantomState = origin.ballState
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const phantom: PlayerPhantom = {
|
||||||||||||
|
type: "phantom",
|
||||||||||||
|
id: phantomId,
|
||||||||||||
|
rightRatio: x,
|
||||||||||||
|
bottomRatio: y,
|
||||||||||||
|
originPlayerId: originPlayer.id,
|
||||||||||||
|
ballState: phantomState,
|
||||||||||||
|
actions: [],
|
||||||||||||
|
}
|
||||||||||||
|
content = {
|
||||||||||||
|
...content,
|
||||||||||||
|
components: [...content.components, phantom],
|
||||||||||||
|
}
|
||||||||||||
|
return phantom.id
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
for (const component of content.components) {
|
||||||||||||
|
if (component.id == origin.id) {
|
||||||||||||
|
continue
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const componentBounds = document
|
||||||||||||
|
.getElementById(component.id)!
|
||||||||||||
|
.getBoundingClientRect()
|
||||||||||||
|
|
||||||||||||
|
if (overlaps(componentBounds, arrowHead)) {
|
||||||||||||
|
let toId = component.id
|
||||||||||||
|
|
||||||||||||
|
if (component.type == "ball") {
|
||||||||||||
|
toId = createPhantom(true)
|
||||||||||||
|
content = removeBall(content)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const action: Action = {
|
||||||||||||
|
target: toId,
|
||||||||||||
|
type: getActionKind(component, origin.ballState).kind,
|
||||||||||||
|
segments: [{ next: toId }],
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
newContent: updateComponent(
|
||||||||||||
|
{
|
||||||||||||
|
...content.components.find((c) => c.id == origin.id)!,
|
||||||||||||
|
actions: [...origin.actions, action],
|
||||||||||||
|
},
|
||||||||||||
|
content,
|
||||||||||||
|
),
|
||||||||||||
|
createdAction: action,
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const phantomId = createPhantom(false)
|
||||||||||||
|
|
||||||||||||
|
const action: Action = {
|
||||||||||||
|
target: phantomId,
|
||||||||||||
|
type: getActionKind(null, origin.ballState).kind,
|
||||||||||||
|
segments: [{ next: phantomId }],
|
||||||||||||
|
}
|
||||||||||||
|
return {
|
||||||||||||
|
newContent: updateComponent(
|
||||||||||||
|
{
|
||||||||||||
|
...content.components.find((c) => c.id == origin.id)!,
|
||||||||||||
|
actions: [...origin.actions, action],
|
||||||||||||
|
},
|
||||||||||||
|
content,
|
||||||||||||
|
),
|
||||||||||||
|
createdAction: action,
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function removeAllActionsTargeting(
|
||||||||||||
|
componentId: ComponentId,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
const components = []
|
||||||||||||
|
for (let i = 0; i < content.components.length; i++) {
|
||||||||||||
|
const component = content.components[i]
|
||||||||||||
|
components.push({
|
||||||||||||
|
...component,
|
||||||||||||
|
actions: component.actions.filter((a) => a.target != componentId),
|
||||||||||||
|
})
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components,
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function removeAction(
|
||||||||||||
|
origin: TacticComponent,
|
||||||||||||
|
action: Action,
|
||||||||||||
|
actionIdx: number,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
origin = {
|
||||||||||||
|
...origin,
|
||||||||||||
|
actions: origin.actions.toSpliced(actionIdx, 1),
|
||||||||||||
|
}
|
||||||||||||
|
content = updateComponent(origin, content)
|
||||||||||||
|
|
||||||||||||
|
if (action.target == null) return content
|
||||||||||||
|
|
||||||||||||
|
const target = content.components.find((c) => action.target == c.id)!
|
||||||||||||
|
|
||||||||||||
|
// if the removed action is a shoot, set the origin as holding the ball
|
||||||||||||
|
if (
|
||||||||||||
|
action.type == ActionKind.SHOOT &&
|
||||||||||||
|
(origin.type === "player" || origin.type === "phantom")
|
||||||||||||
|
) {
|
||||||||||||
|
if (target.type === "player" || target.type === "phantom")
|
||||||||||||
|
content = changePlayerBallState(target, BallState.NONE, content)
|
||||||||||||
|
|
||||||||||||
|
if (origin.ballState === BallState.PASSED) {
|
||||||||||||
|
content = changePlayerBallState(
|
||||||||||||
|
origin,
|
||||||||||||
|
BallState.HOLDS_BY_PASS,
|
||||||||||||
|
content,
|
||||||||||||
|
)
|
||||||||||||
|
} else if (origin.ballState === BallState.PASSED_ORIGIN) {
|
||||||||||||
|
content = changePlayerBallState(
|
||||||||||||
|
origin,
|
||||||||||||
|
BallState.HOLDS_ORIGIN,
|
||||||||||||
|
content,
|
||||||||||||
|
)
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if (target.type === "phantom") {
|
||||||||||||
|
let path = null
|
||||||||||||
|
if (origin.type === "player") {
|
||||||||||||
|
path = origin.path
|
||||||||||||
|
} else if (origin.type === "phantom") {
|
||||||||||||
|
path = getOrigin(origin, content.components).path
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if (path != null && path.items.find((c) => c === target.id)) {
|
||||||||||||
|
content = removePlayer(target, content)
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return content
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
/**
|
||||||||||||
|
* Spreads the changes to others actions and components, directly or indirectly bound to the origin, implied by the change of the origin's actual state with
|
||||||||||||
|
* the given newState.
|
||||||||||||
|
* @param origin
|
||||||||||||
|
* @param newState
|
||||||||||||
|
* @param content
|
||||||||||||
|
*/
|
||||||||||||
|
export function spreadNewStateFromOriginStateChange(
|
||||||||||||
|
origin: PlayerLike,
|
||||||||||||
|
newState: BallState,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
if (origin.ballState === newState) {
|
||||||||||||
|
return content
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
origin = {
|
||||||||||||
|
...origin,
|
||||||||||||
|
ballState: newState,
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
content = updateComponent(origin, content)
|
||||||||||||
|
|
||||||||||||
|
for (let i = 0; i < origin.actions.length; i++) {
|
||||||||||||
|
const action = origin.actions[i]
|
||||||||||||
|
if (typeof action.target !== "string") {
|
||||||||||||
|
continue
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const actionTarget = content.components.find(
|
||||||||||||
|
(c) => action.target === c.id,
|
||||||||||||
|
)! as PlayerLike
|
||||||||||||
|
|
||||||||||||
|
let targetState: BallState = actionTarget.ballState
|
||||||||||||
|
let deleteAction = false
|
||||||||||||
|
|
||||||||||||
|
if (isNextInPath(origin, actionTarget, content.components)) {
|
||||||||||||
|
switch (newState) {
|
||||||||||||
|
case BallState.PASSED:
|
||||||||||||
|
case BallState.PASSED_ORIGIN:
|
||||||||||||
|
targetState = BallState.NONE
|
||||||||||||
|
break
|
||||||||||||
|
case BallState.HOLDS_ORIGIN:
|
||||||||||||
|
targetState = BallState.HOLDS_BY_PASS
|
||||||||||||
|
break
|
||||||||||||
|
default:
|
||||||||||||
|
targetState = newState
|
||||||||||||
|
}
|
||||||||||||
|
} else if (
|
||||||||||||
|
newState === BallState.NONE &&
|
||||||||||||
|
action.type === ActionKind.SHOOT
|
||||||||||||
|
) {
|
||||||||||||
|
/// if the new state removes the ball from the player, remove all actions that were meant to shoot the ball
|
||||||||||||
|
deleteAction = true
|
||||||||||||
|
targetState = BallState.NONE // Then remove the ball for the target as well
|
||||||||||||
|
} else if (
|
||||||||||||
|
(newState === BallState.HOLDS_BY_PASS ||
|
||||||||||||
|
newState === BallState.HOLDS_ORIGIN) &&
|
||||||||||||
|
action.type === ActionKind.SCREEN
|
||||||||||||
|
) {
|
||||||||||||
|
targetState = BallState.HOLDS_BY_PASS
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
if (deleteAction) {
|
||||||||||||
|
content = removeAction(origin, action, i, content)
|
||||||||||||
|
origin = content.components.find((c) => c.id === origin.id)! as
|
||||||||||||
|
| Player
|
||||||||||||
|
| PlayerPhantom
|
||||||||||||
|
i-- // step back
|
||||||||||||
|
} else {
|
||||||||||||
|
// do not change the action type if it is a shoot action
|
||||||||||||
|
const { kind, nextState } = getActionKindBetween(
|
||||||||||||
|
origin,
|
||||||||||||
|
actionTarget,
|
||||||||||||
|
newState,
|
||||||||||||
|
)
|
||||||||||||
|
|
||||||||||||
|
origin = {
|
||||||||||||
|
...origin,
|
||||||||||||
|
ballState: nextState,
|
||||||||||||
|
actions: origin.actions.toSpliced(i, 1, {
|
||||||||||||
|
...action,
|
||||||||||||
|
type: kind,
|
||||||||||||
|
}),
|
||||||||||||
|
}
|
||||||||||||
|
content = updateComponent(origin, content)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
content = spreadNewStateFromOriginStateChange(
|
||||||||||||
|
actionTarget,
|
||||||||||||
|
targetState,
|
||||||||||||
|
content,
|
||||||||||||
|
)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return content
|
||||||||||||
|
}
|
@ -0,0 +1,149 @@
|
|||||||
|
import {
|
||||||
|
BallState,
|
||||||
|
Player,
|
||||||
|
PlayerLike,
|
||||||
|
PlayerPhantom,
|
||||||
|
} from "../model/tactic/Player"
|
||||||
|
import { TacticComponent, TacticContent } from "../model/tactic/Tactic"
|
||||||
|
import { removeComponent, updateComponent } from "./TacticContentDomains"
|
||||||
|
import {
|
||||||
|
removeAllActionsTargeting,
|
||||||
|
spreadNewStateFromOriginStateChange,
|
||||||
|
} from "./ActionsDomains"
|
||||||
|
import { ActionKind } from "../model/tactic/Action"
|
||||||
|
|
||||||
|
export function getOrigin(
|
||||||
|
pathItem: PlayerPhantom,
|
||||||
|
components: TacticComponent[],
|
||||||
|
): Player {
|
||||||
|
// Trust the components to contains only phantoms with valid player origin identifiers
|
||||||
|
return components.find((c) => c.id == pathItem.originPlayerId)! as Player
|
||||||
|
}
|
||||||
|
|
||||||
|
export function areInSamePath(a: PlayerLike, b: PlayerLike) {
|
||||||
|
if (a.type === "phantom" && b.type === "phantom") {
|
||||||
|
return a.originPlayerId === b.originPlayerId
|
||||||
|
}
|
||||||
|
if (a.type === "phantom") {
|
||||||
|
return b.id === a.originPlayerId
|
||||||
|
}
|
||||||
|
if (b.type === "phantom") {
|
||||||
|
return a.id === b.originPlayerId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param origin
|
||||||
|
* @param other
|
||||||
|
* @param components
|
||||||
|
* @returns true if the `other` player is the phantom next-to the origin's path.
|
||||||
|
*/
|
||||||
|
export function isNextInPath(
|
||||||
|
origin: PlayerLike,
|
||||||
|
other: PlayerLike,
|
||||||
|
components: TacticComponent[],
|
||||||
|
): boolean {
|
||||||
|
if (origin.type === "player") {
|
||||||
|
return origin.path?.items[0] === other.id
|
||||||
|
}
|
||||||
|
const originPath = getOrigin(origin, components).path!
|
||||||
|
return (
|
||||||
|
originPath.items!.indexOf(origin.id) ===
|
||||||
|
originPath.items!.indexOf(other.id) - 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePlayerPath(
|
||||||
|
player: Player,
|
||||||
|
content: TacticContent,
|
||||||
|
): TacticContent {
|
||||||
|
if (player.path == null) {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pathElement of player.path.items) {
|
||||||
|
content = removeComponent(pathElement, content)
|
||||||
|
content = removeAllActionsTargeting(pathElement, content)
|
||||||
|
}
|
||||||
|
return updateComponent(
|
||||||
|
{
|
||||||
|
...player,
|
||||||
|
path: null,
|
||||||
|
},
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePlayer(
|
||||||
|
player: PlayerLike,
|
||||||
|
content: TacticContent,
|
||||||
|
): TacticContent {
|
||||||
|
content = removeAllActionsTargeting(player.id, content)
|
||||||
|
|
||||||
|
if (player.type == "phantom") {
|
||||||
|
const origin = getOrigin(player, content.components)
|
||||||
|
return truncatePlayerPath(origin, player, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
content = removePlayerPath(player, content)
|
||||||
|
content = removeComponent(player.id, content)
|
||||||
|
|
||||||
|
for (const action of player.actions) {
|
||||||
|
if (action.type !== ActionKind.SHOOT) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const actionTarget = content.components.find(
|
||||||
|
(c) => c.id === action.target,
|
||||||
|
)! as PlayerLike
|
||||||
|
return spreadNewStateFromOriginStateChange(
|
||||||
|
actionTarget,
|
||||||
|
BallState.NONE,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
export function truncatePlayerPath(
|
||||||
|
player: Player,
|
||||||
|
phantom: PlayerPhantom,
|
||||||
|
content: TacticContent,
|
||||||
|
): TacticContent {
|
||||||
|
if (player.path == null) return content
|
||||||
|
|
||||||
|
const path = player.path!
|
||||||
|
|
||||||
|
const truncateStartIdx = path.items.indexOf(phantom.id)
|
||||||
|
|
||||||
|
for (let i = truncateStartIdx; i < path.items.length; i++) {
|
||||||
|
const pathPhantomId = path.items[i]
|
||||||
|
|
||||||
|
//remove the phantom from the tactic
|
||||||
|
content = removeComponent(pathPhantomId, content)
|
||||||
|
content = removeAllActionsTargeting(pathPhantomId, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateComponent(
|
||||||
|
{
|
||||||
|
...player,
|
||||||
|
path:
|
||||||
|
truncateStartIdx == 0
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
...path,
|
||||||
|
items: path.items.toSpliced(truncateStartIdx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePlayerBallState(
|
||||||
|
player: PlayerLike,
|
||||||
|
newState: BallState,
|
||||||
|
content: TacticContent,
|
||||||
|
): TacticContent {
|
||||||
|
return spreadNewStateFromOriginStateChange(player, newState, content)
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* information about a player that is into a rack
|
||||||
|
*/
|
||||||
|
import { PlayerTeam } from "../model/tactic/Player"
|
||||||
|
|
||||||
|
export interface RackedPlayer {
|
||||||
|
team: PlayerTeam
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RackedCourtObject = { key: "ball" }
|
@ -0,0 +1,243 @@
|
|||||||||||||
|
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"
|
||||||||||||
|
|
||||||||||||
|
export function placePlayerAt(
|
||||||||||||
|
refBounds: DOMRect,
|
||||||||||||
|
courtBounds: DOMRect,
|
||||||||||||
|
element: RackedPlayer,
|
||||||||||||
|
): Player {
|
||||||||||||
|
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
type: "player",
|
||||||||||||
|
id: "player-" + element.key + "-" + element.team,
|
||||||||||||
|
team: element.team,
|
||||||||||||
|
role: element.key,
|
||||||||||||
|
rightRatio: x,
|
||||||||||||
|
bottomRatio: y,
|
||||||||||||
|
ballState: BallState.NONE,
|
||||||||||||
|
path: null,
|
||||||||||||
|
actions: [],
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function placeObjectAt(
|
||||||||||||
|
refBounds: DOMRect,
|
||||||||||||
|
courtBounds: DOMRect,
|
||||||||||||
|
rackedObject: RackedCourtObject,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
||||||||||||
|
|
||||||||||||
|
let courtObject: CourtObject
|
||||||||||||
|
|
||||||||||||
|
switch (rackedObject.key) {
|
||||||||||||
|
case BALL_TYPE:
|
||||||||||||
|
const playerCollidedIdx = getComponentCollided(
|
||||||||||||
|
refBounds,
|
||||||||||||
|
content.components,
|
||||||||||||
|
BALL_ID,
|
||||||||||||
|
)
|
||||||||||||
|
if (playerCollidedIdx != -1) {
|
||||||||||||
|
return dropBallOnComponent(playerCollidedIdx, content, true)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
courtObject = {
|
||||||||||||
|
type: BALL_TYPE,
|
||||||||||||
|
id: BALL_ID,
|
||||||||||||
|
rightRatio: x,
|
||||||||||||
|
bottomRatio: y,
|
||||||||||||
|
actions: [],
|
||||||||||||
|
}
|
||||||||||||
|
break
|
||||||||||||
|
|
||||||||||||
|
default:
|
||||||||||||
|
throw new Error("unknown court object " + rackedObject.key)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components: [...content.components, courtObject],
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function dropBallOnComponent(
|
||||||||||||
|
targetedComponentIdx: number,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
setAsOrigin: boolean,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
const component = content.components[targetedComponentIdx]
|
||||||||||||
|
|
||||||||||||
|
if (component.type === "player" || component.type === "phantom") {
|
||||||||||||
|
const newState =
|
||||||||||||
|
setAsOrigin ||
|
||||||||||||
|
component.ballState === BallState.PASSED_ORIGIN ||
|
||||||||||||
|
component.ballState === BallState.HOLDS_ORIGIN
|
||||||||||||
|
? BallState.HOLDS_ORIGIN
|
||||||||||||
|
: BallState.HOLDS_BY_PASS
|
||||||||||||
|
|
||||||||||||
|
content = changePlayerBallState(component, newState, content)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return removeBall(content)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function removeBall(content: TacticContent): TacticContent {
|
||||||||||||
|
const ballObjIdx = content.components.findIndex((o) => o.type == "ball")
|
||||||||||||
|
|
||||||||||||
|
if (ballObjIdx == -1) {
|
||||||||||||
|
return content
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components: content.components.toSpliced(ballObjIdx, 1),
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function placeBallAt(
|
||||||||||||
|
refBounds: DOMRect,
|
||||||||||||
|
courtBounds: DOMRect,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
if (!overlaps(courtBounds, refBounds)) {
|
||||||||||||
|
return removeBall(content)
|
||||||||||||
|
}
|
||||||||||||
|
const playerCollidedIdx = getComponentCollided(
|
||||||||||||
|
refBounds,
|
||||||||||||
|
content.components,
|
||||||||||||
|
BALL_ID,
|
||||||||||||
|
)
|
||||||||||||
|
|
||||||||||||
|
if (playerCollidedIdx != -1) {
|
||||||||||||
|
return dropBallOnComponent(playerCollidedIdx, content, true)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
const ballIdx = content.components.findIndex((o) => o.type == "ball")
|
||||||||||||
|
|
||||||||||||
|
const { x, y } = ratioWithinBase(refBounds, courtBounds)
|
||||||||||||
|
|
||||||||||||
|
const ball: Ball = {
|
||||||||||||
|
type: BALL_TYPE,
|
||||||||||||
|
id: BALL_ID,
|
||||||||||||
|
rightRatio: x,
|
||||||||||||
|
bottomRatio: y,
|
||||||||||||
|
actions: [],
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
let components = content.components
|
||||||||||||
|
|
||||||||||||
|
if (ballIdx != -1) {
|
||||||||||||
|
components = components.toSpliced(ballIdx, 1, ball)
|
||||||||||||
|
} else {
|
||||||||||||
|
components = components.concat(ball)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components,
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function moveComponent(
|
||||||||||||
|
newPos: Pos,
|
||||||||||||
|
component: TacticComponent,
|
||||||||||||
|
info: PlayerInfo,
|
||||||||||||
|
courtBounds: DOMRect,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
removed: (content: TacticContent) => TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
const playerBounds = document
|
||||||||||||
|
.getElementById(info.id)!
|
||||||||||||
|
.getBoundingClientRect()
|
||||||||||||
|
|
||||||||||||
|
// if the piece is no longer on the court, remove it
|
||||||||||||
|
if (!overlaps(playerBounds, courtBounds)) {
|
||||||||||||
|
return removed(content)
|
||||||||||||
|
}
|
||||||||||||
|
return updateComponent(
|
||||||||||||
|
{
|
||||||||||||
|
...component,
|
||||||||||||
|
rightRatio: newPos.x,
|
||||||||||||
|
bottomRatio: newPos.y,
|
||||||||||||
|
},
|
||||||||||||
|
content,
|
||||||||||||
|
)
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function removeComponent(
|
||||||||||||
|
componentId: ComponentId,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components: content.components.filter((c) => c.id !== componentId),
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function updateComponent(
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
component: TacticComponent,
|
||||||||||||
|
content: TacticContent,
|
||||||||||||
|
): TacticContent {
|
||||||||||||
|
return {
|
||||||||||||
|
...content,
|
||||||||||||
|
components: content.components.map((c) =>
|
||||||||||||
|
c.id === component.id ? component : c,
|
||||||||||||
|
),
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function getComponentCollided(
|
||||||||||||
|
bounds: DOMRect,
|
||||||||||||
|
components: TacticComponent[],
|
||||||||||||
maxime.batista marked this conversation as resolved
|
|||||||||||||
|
ignore?: ComponentId,
|
||||||||||||
|
): number | -1 {
|
||||||||||||
|
for (let i = 0; i < components.length; i++) {
|
||||||||||||
|
const component = components[i]
|
||||||||||||
|
|
||||||||||||
|
if (component.id == ignore) continue
|
||||||||||||
|
|
||||||||||||
|
const playerBounds = document
|
||||||||||||
|
.getElementById(component.id)!
|
||||||||||||
|
.getBoundingClientRect()
|
||||||||||||
|
|
||||||||||||
|
if (overlaps(playerBounds, bounds)) {
|
||||||||||||
|
return i
|
||||||||||||
|
}
|
||||||||||||
|
}
|
||||||||||||
|
return -1
|
||||||||||||
|
}
|
||||||||||||
|
|
||||||||||||
|
export function getRackPlayers(
|
||||||||||||
|
team: PlayerTeam,
|
||||||||||||
|
components: TacticComponent[],
|
||||||||||||
|
): RackedPlayer[] {
|
||||||||||||
|
return ["1", "2", "3", "4", "5"]
|
||||||||||||
|
.filter(
|
||||||||||||
|
(role) =>
|
||||||||||||
|
components.findIndex(
|
||||||||||||
|
(c) =>
|
||||||||||||
|
c.type == "player" && c.team == team && c.role == role,
|
||||||||||||
|
) == -1,
|
||||||||||||
|
)
|
||||||||||||
|
.map((key) => ({ team, key }))
|
||||||||||||
|
}
|
@ -1,17 +0,0 @@
|
|||||||
export type CourtObject = { type: "ball" } & Ball
|
|
||||||
|
|
||||||
export interface Ball {
|
|
||||||
/**
|
|
||||||
* The ball is a "ball" court object
|
|
||||||
*/
|
|
||||||
readonly type: "ball"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from "./Tactic"
|
||||||
|
|
||||||
|
export const BALL_ID = "ball"
|
||||||
|
export const BALL_TYPE = "ball"
|
||||||
|
|
||||||
|
//place here all different kinds of objects
|
||||||
|
export type CourtObject = Ball
|
||||||
|
|
||||||
|
export type Ball = Component<typeof BALL_TYPE>
|
Loading…
Reference in new issue