import {BallState, Player, PlayerPhantom} 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, ): ActionKind { switch (ballState) { case BallState.HOLDS_ORIGIN: case BallState.HOLDS_BY_PASS: return target ? ActionKind.SHOOT : ActionKind.DRIBBLE case BallState.PASSED_ORIGIN: case BallState.PASSED: case BallState.NONE: return target && target.type != BALL_TYPE ? ActionKind.SCREEN : ActionKind.MOVE } } export function getActionKindBetween(origin: Player | PlayerPhantom, target: TacticComponent | null, state: BallState): ActionKind { //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.actions.find(a => moves(a.type)) && origin.ballState != BallState.HOLDS_BY_PASS) { 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: Player | PlayerPhantom, target: Player | PlayerPhantom, 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 (Player | PlayerPhantom) if (phantom.actions.find(a => typeof a.target === "string" && (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 (Player | PlayerPhantom) if (phantom.actions.find(a => typeof a.target === "string" && targetOriginPath.indexOf(a.target) > targetIdx)) { return true; } } return false; } export function createAction( origin: Player | PlayerPhantom, 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(originState: BallState): 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 switch (originState) { case BallState.HOLDS_ORIGIN: phantomState = BallState.HOLDS_BY_PASS break case BallState.PASSED: case BallState.PASSED_ORIGIN: phantomState = BallState.NONE break default: phantomState = originState } 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), segments: [{next: toId}], } return { newContent: updateComponent( { ...content.components.find((c) => c.id == origin.id)!, actions: [...origin.actions, action], }, content, ), createdAction: action, } } } const phantomId = createPhantom(origin.ballState) const action: Action = { target: phantomId, type: getActionKind(null, origin.ballState), 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 (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 === "player" || target.type === "phantom") content = changePlayerBallState(target, BallState.NONE, 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: Player | PlayerPhantom, 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 Player | PlayerPhantom; let targetState: BallState = actionTarget.ballState let deleteAction = false if (isNextInPath(origin, actionTarget, content.components)) { /// If the target is the next phantom from the origin, its state is propagated. targetState = (newState === BallState.PASSED || newState === BallState.PASSED_ORIGIN) ? BallState.NONE : 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 type = action.type == ActionKind.SHOOT ? ActionKind.SHOOT : getActionKindBetween(origin, actionTarget, newState) origin = { ...origin, actions: origin.actions.toSpliced(i, 1, { ...action, type }) } content = updateComponent(origin, content) } content = spreadNewStateFromOriginStateChange(actionTarget, targetState, content) } return content }