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, ): { 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: Player | PlayerPhantom, 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)) ) { console.log("a") 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)) ) { console.log("b") 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) ) { console.log("c") 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)) { console.log("e") 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" && 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 Player | PlayerPhantom 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: 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(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: 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)) { 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 }