parent
8d444c38b4
commit
852f163e4a
@ -0,0 +1,214 @@
|
||||
import {BallState, Player, PlayerPhantom} from "../model/tactic/Player"
|
||||
import {middlePos, ratioWithinBase} from "../geo/Pos"
|
||||
import {ComponentId, TacticComponent, TacticContent,} from "../model/tactic/Tactic"
|
||||
import {overlaps} from "../geo/Box"
|
||||
import {Action, ActionKind} from "../model/tactic/Action"
|
||||
import {removeBall, updateComponent} from "./TacticContentDomains"
|
||||
import {getOrigin} from "./PlayerDomains"
|
||||
|
||||
export function refreshAllActions(
|
||||
actions: Action[],
|
||||
components: TacticComponent[],
|
||||
) {
|
||||
return actions.map((action) => ({
|
||||
...action,
|
||||
type: getActionKindFrom(action.fromId, action.toId, components),
|
||||
}))
|
||||
}
|
||||
|
||||
export function getActionKindFrom(
|
||||
originId: ComponentId,
|
||||
targetId: ComponentId | null,
|
||||
components: TacticComponent[],
|
||||
): ActionKind {
|
||||
const origin = components.find((p) => p.id == originId)!
|
||||
const target = components.find(p => p.id == targetId)
|
||||
|
||||
let ballState = BallState.NONE
|
||||
|
||||
if (origin.type == "player" || origin.type == "phantom") {
|
||||
ballState = origin.ballState
|
||||
}
|
||||
|
||||
let hasTarget = target ? (target.type != 'phantom' || target.originPlayerId != origin.id) : false
|
||||
|
||||
return getActionKind(hasTarget, ballState)
|
||||
}
|
||||
|
||||
export function getActionKind(hasTarget: boolean, ballState: BallState): ActionKind {
|
||||
switch (ballState) {
|
||||
case BallState.HOLDS:
|
||||
return hasTarget ? ActionKind.SHOOT : ActionKind.DRIBBLE
|
||||
case BallState.SHOOTED:
|
||||
return ActionKind.MOVE
|
||||
case BallState.NONE:
|
||||
return hasTarget ? ActionKind.SCREEN : ActionKind.MOVE
|
||||
}
|
||||
}
|
||||
|
||||
export function placeArrow(
|
||||
origin: Player | PlayerPhantom,
|
||||
courtBounds: DOMRect,
|
||||
arrowHead: DOMRect,
|
||||
content: TacticContent,
|
||||
): { createdAction: Action, newContent: TacticContent } {
|
||||
const originRef = document.getElementById(origin.id)!
|
||||
const start = ratioWithinBase(
|
||||
middlePos(originRef.getBoundingClientRect()),
|
||||
courtBounds,
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a new phantom component.
|
||||
* Be aware that this function will reassign the `content` parameter.
|
||||
* @param receivesBall
|
||||
*/
|
||||
function createPhantom(receivesBall: 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,
|
||||
)
|
||||
|
||||
const ballState = receivesBall
|
||||
? BallState.HOLDS
|
||||
: origin.ballState == BallState.HOLDS
|
||||
? BallState.HOLDS
|
||||
: BallState.NONE
|
||||
|
||||
const phantom: PlayerPhantom = {
|
||||
type: "phantom",
|
||||
id: phantomId,
|
||||
rightRatio: x,
|
||||
bottomRatio: y,
|
||||
originPlayerId: originPlayer.id,
|
||||
ballState
|
||||
}
|
||||
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)) {
|
||||
const targetPos = document
|
||||
.getElementById(component.id)!
|
||||
.getBoundingClientRect()
|
||||
|
||||
const end = ratioWithinBase(middlePos(targetPos), courtBounds)
|
||||
|
||||
let toId = component.id
|
||||
|
||||
if (component.type == "ball") {
|
||||
toId = createPhantom(true)
|
||||
content = removeBall(content)
|
||||
}
|
||||
|
||||
const action: Action = {
|
||||
fromId: originRef.id,
|
||||
toId,
|
||||
type: getActionKind(true, origin.ballState),
|
||||
moveFrom: start,
|
||||
segments: [{next: end}],
|
||||
}
|
||||
|
||||
return {
|
||||
newContent: {
|
||||
...content,
|
||||
actions: [...content.actions, action],
|
||||
},
|
||||
createdAction: action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const phantomId = createPhantom(origin.ballState == BallState.HOLDS)
|
||||
|
||||
const action: Action = {
|
||||
fromId: originRef.id,
|
||||
toId: phantomId,
|
||||
type: getActionKind(false, origin.ballState),
|
||||
moveFrom: ratioWithinBase(
|
||||
middlePos(originRef.getBoundingClientRect()),
|
||||
courtBounds,
|
||||
),
|
||||
segments: [
|
||||
{next: ratioWithinBase(middlePos(arrowHead), courtBounds)},
|
||||
],
|
||||
}
|
||||
return {
|
||||
newContent: {
|
||||
...content,
|
||||
actions: [...content.actions, action],
|
||||
},
|
||||
createdAction: action
|
||||
}
|
||||
}
|
||||
|
||||
export function repositionActionsRelatedTo(
|
||||
compId: ComponentId,
|
||||
courtBounds: DOMRect,
|
||||
actions: Action[],
|
||||
): Action[] {
|
||||
const posRect = document.getElementById(compId)?.getBoundingClientRect()
|
||||
const newPos = posRect != undefined
|
||||
? ratioWithinBase(middlePos(posRect), courtBounds)
|
||||
: undefined
|
||||
|
||||
return actions.flatMap((action) => {
|
||||
if (newPos == undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (action.fromId == compId) {
|
||||
return [{...action, moveFrom: newPos}]
|
||||
}
|
||||
|
||||
if (action.toId == compId) {
|
||||
const lastIdx = action.segments.length - 1
|
||||
const segments = action.segments.toSpliced(lastIdx, 1, {
|
||||
...action.segments[lastIdx],
|
||||
next: newPos!,
|
||||
})
|
||||
return [{...action, segments}]
|
||||
}
|
||||
|
||||
return action
|
||||
})
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { Player, PlayerPhantom } from "../model/tactic/Player"
|
||||
import { TacticComponent, TacticContent } from "../model/tactic/Tactic"
|
||||
import { removeComponent, updateComponent } from "./TacticContentDomains"
|
||||
|
||||
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 removePlayerPath(
|
||||
player: Player,
|
||||
content: TacticContent,
|
||||
): TacticContent {
|
||||
if (player.path == null) {
|
||||
return content
|
||||
}
|
||||
|
||||
for (const pathElement of player.path.items) {
|
||||
content = removeComponent(pathElement, content)
|
||||
}
|
||||
return updateComponent(
|
||||
{
|
||||
...player,
|
||||
path: null,
|
||||
},
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
export function removePlayer(
|
||||
player: Player | PlayerPhantom,
|
||||
content: TacticContent,
|
||||
): TacticContent {
|
||||
if (player.type == "phantom") {
|
||||
const origin = getOrigin(player, content.components)
|
||||
return truncatePlayerPath(origin, player, content)
|
||||
}
|
||||
|
||||
content = removePlayerPath(player, content)
|
||||
return removeComponent(player.id, content)
|
||||
}
|
||||
|
||||
export function truncatePlayerPath(
|
||||
player: Player,
|
||||
phantom: PlayerPhantom,
|
||||
content: TacticContent,
|
||||
): TacticContent {
|
||||
if (player.path == null) return content
|
||||
|
||||
const path = player.path!
|
||||
|
||||
let truncateStartIdx = -1
|
||||
|
||||
for (let j = 0; j < path.items.length; j++) {
|
||||
const pathPhantomId = path.items[j]
|
||||
if (truncateStartIdx != -1 || pathPhantomId == phantom.id) {
|
||||
if (truncateStartIdx == -1) truncateStartIdx = j
|
||||
|
||||
//remove the phantom from the tactic
|
||||
content = removeComponent(pathPhantomId, content)
|
||||
}
|
||||
}
|
||||
|
||||
return updateComponent(
|
||||
{
|
||||
...player,
|
||||
path:
|
||||
truncateStartIdx == 0
|
||||
? null
|
||||
: {
|
||||
...path,
|
||||
items: path.items.toSpliced(truncateStartIdx),
|
||||
},
|
||||
},
|
||||
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,299 @@
|
||||
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 {refreshAllActions} from "./ActionsDomains"
|
||||
import {getOrigin} 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,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
courtObject = {
|
||||
type: BALL_TYPE,
|
||||
id: BALL_ID,
|
||||
rightRatio: x,
|
||||
bottomRatio: y,
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error("unknown court object " + rackedObject.key)
|
||||
}
|
||||
|
||||
return {
|
||||
...content,
|
||||
components: [...content.components, courtObject],
|
||||
}
|
||||
}
|
||||
|
||||
export function dropBallOnComponent(
|
||||
targetedComponentIdx: number,
|
||||
content: TacticContent,
|
||||
): TacticContent {
|
||||
let components = content.components
|
||||
let component = components[targetedComponentIdx]
|
||||
|
||||
let origin
|
||||
let isPhantom: boolean
|
||||
|
||||
if (component.type == 'phantom') {
|
||||
isPhantom = true
|
||||
origin = getOrigin(component, components)
|
||||
} else if (component.type == 'player') {
|
||||
isPhantom = false
|
||||
origin = component
|
||||
} else {
|
||||
return content
|
||||
}
|
||||
|
||||
components = components.toSpliced(targetedComponentIdx, 1, {
|
||||
...component,
|
||||
ballState: BallState.HOLDS,
|
||||
})
|
||||
if (origin.path != null) {
|
||||
const phantoms = origin.path!.items
|
||||
const headingPhantoms = isPhantom ? phantoms.slice(phantoms.indexOf(component.id)) : phantoms
|
||||
components = components.map(c => headingPhantoms.indexOf(c.id) != -1 ? {
|
||||
...c,
|
||||
hasBall: true
|
||||
} : c)
|
||||
}
|
||||
|
||||
const ballObj = components.findIndex((p) => p.type == BALL_TYPE)
|
||||
|
||||
// Maybe the ball is not present on the court as an object component
|
||||
// if so, don't bother removing it from the court.
|
||||
// This can occur if the user drags and drop the ball from a player that already has the ball
|
||||
// to another component
|
||||
if (ballObj != -1) {
|
||||
components.splice(ballObj, 1)
|
||||
}
|
||||
return {
|
||||
...content,
|
||||
actions: refreshAllActions(content.actions, components),
|
||||
components,
|
||||
}
|
||||
}
|
||||
|
||||
export function removeBall(content: TacticContent): TacticContent {
|
||||
const ballObj = content.components.findIndex((o) => o.type == "ball")
|
||||
|
||||
const components = content.components.map((c) =>
|
||||
(c.type == 'player' || c.type == 'phantom')
|
||||
? {
|
||||
...c,
|
||||
hasBall: false,
|
||||
}
|
||||
: c,
|
||||
)
|
||||
|
||||
// if the ball is already not on the court, do nothing
|
||||
if (ballObj != -1) {
|
||||
components.splice(ballObj, 1)
|
||||
}
|
||||
|
||||
return {
|
||||
...content,
|
||||
actions: refreshAllActions(content.actions, components),
|
||||
components,
|
||||
}
|
||||
}
|
||||
|
||||
export function placeBallAt(
|
||||
refBounds: DOMRect,
|
||||
courtBounds: DOMRect,
|
||||
content: TacticContent,
|
||||
): {
|
||||
newContent: TacticContent
|
||||
removed: boolean
|
||||
} {
|
||||
if (!overlaps(courtBounds, refBounds)) {
|
||||
return {newContent: removeBall(content), removed: true}
|
||||
}
|
||||
const playerCollidedIdx = getComponentCollided(
|
||||
refBounds,
|
||||
content.components,
|
||||
BALL_ID,
|
||||
)
|
||||
if (playerCollidedIdx != -1) {
|
||||
return {
|
||||
newContent: dropBallOnComponent(playerCollidedIdx, {
|
||||
...content,
|
||||
components: content.components.map((c) =>
|
||||
c.type == "player" || c.type == 'phantom'
|
||||
? {
|
||||
...c,
|
||||
hasBall: false,
|
||||
}
|
||||
: c,
|
||||
),
|
||||
}),
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
|
||||
const ballIdx = content.components.findIndex((o) => o.type == "ball")
|
||||
|
||||
const {x, y} = ratioWithinBase(refBounds, courtBounds)
|
||||
|
||||
const components = content.components.map((c) =>
|
||||
c.type == "player" || c.type == "phantom"
|
||||
? {
|
||||
...c,
|
||||
hasBall: false,
|
||||
}
|
||||
: c,
|
||||
)
|
||||
|
||||
const ball: Ball = {
|
||||
type: BALL_TYPE,
|
||||
id: BALL_ID,
|
||||
rightRatio: x,
|
||||
bottomRatio: y,
|
||||
}
|
||||
if (ballIdx != -1) {
|
||||
components.splice(ballIdx, 1, ball)
|
||||
} else {
|
||||
components.push(ball)
|
||||
}
|
||||
|
||||
return {
|
||||
newContent: {
|
||||
...content,
|
||||
actions: refreshAllActions(content.actions, components),
|
||||
components,
|
||||
},
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
const componentIdx = content.components.findIndex(
|
||||
(c) => c.id == componentId,
|
||||
)
|
||||
|
||||
return {
|
||||
...content,
|
||||
components: content.components.toSpliced(componentIdx, 1),
|
||||
actions: content.actions.filter(
|
||||
(a) => a.toId !== componentId && a.fromId !== componentId,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export function updateComponent(
|
||||
component: TacticComponent,
|
||||
content: TacticContent,
|
||||
): TacticContent {
|
||||
const componentIdx = content.components.findIndex(
|
||||
(c) => c.id == component.id,
|
||||
)
|
||||
return {
|
||||
...content,
|
||||
components: content.components.toSpliced(componentIdx, 1, component),
|
||||
}
|
||||
}
|
||||
|
||||
export function getComponentCollided(
|
||||
bounds: DOMRect,
|
||||
components: TacticComponent[],
|
||||
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}))
|
||||
}
|
Loading…
Reference in new issue