You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Application-Web/src/editor/ActionsDomains.ts

564 lines
16 KiB

import {
BallState,
Player,
PlayerLike,
PlayerPhantom,
} from "../model/tactic/Player"
import {ratioWithinBase} from "../geo/Pos"
import {
ComponentId,
TacticComponent,
StepContent,
} 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,
getComponent,
getOrigin,
getPlayerNextTo,
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 component = getComponent(targetOriginPath[i], components)
if (
component.actions.find(
(a) =>
typeof a.target === "string" &&
moves(a.type) &&
originOriginPath.indexOf(a.target) !== -1,
)
) {
return true
}
}
const originIdx = originOriginPath.indexOf(origin.id)
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: StepContent,
): { createdAction: Action; newContent: StepContent } {
/**
* 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)
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,
pos: attachedTo
? {
type: "follows",
attach: attachedTo,
}
: {
type: "fixed",
x,
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 actionKind = getActionKind(component, origin.ballState).kind
let action: Action
if (actionKind === ActionKind.SCREEN) {
createPhantom(false, toId)
action = {
target: toId,
type: actionKind,
segments: [{next: toId}],
}
} else {
action = {
target: toId,
type: actionKind,
segments: [{next: toId}],
}
}
return {
newContent: updateComponent(
{
...content.components.find((c) => c.id == origin.id)!,
actions: [...origin.actions, action],
},
content,
),
createdAction: action,
}
}
}
const actionKind = getActionKind(null, origin.ballState).kind
if (actionKind === ActionKind.SCREEN)
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}],
}
return {
newContent: updateComponent(
{
...content.components.find((c) => c.id == origin.id)!,
actions: [...origin.actions, action],
},
content,
),
createdAction: action,
}
}
export function removeAllActionsTargeting(
componentId: ComponentId,
content: StepContent,
): StepContent {
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,
actionIdx: number,
content: StepContent,
): StepContent {
const action = origin.actions[actionIdx]
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)
}
}
// 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")
) {
const screenPhantom = getPlayerNextTo(origin, 1, content.components)!
content = removePlayer(screenPhantom, 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: StepContent,
): StepContent {
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: PlayerLike = getComponent(
action.target,
content.components,
)
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
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)
}
}
if (deleteAction) {
content = removeAction(origin, i, content)
origin = getComponent(origin.id, content.components)
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
}