import { Pos, ratioWithinBase } from "../geo/Pos" import { BallState, Player, PlayerInfo, PlayerLike, PlayerPhantom, PlayerTeam, } from "../model/tactic/Player" import { Ball, BALL_ID, BALL_TYPE, CourtObject, } from "../model/tactic/CourtObjects" import { ComponentId, StepContent, TacticComponent, } from "../model/tactic/Tactic" import { overlaps } from "../geo/Box" import { RackedCourtObject, RackedPlayer } from "./RackedItems" import { changePlayerBallState, computePhantomPositioning, getComponent, getOrigin, } from "./PlayerDomains" import { ActionKind } from "../model/tactic/Action.ts" export function placePlayerAt( refBounds: DOMRect, courtBounds: DOMRect, element: RackedPlayer, ): Player { const pos = ratioWithinBase(refBounds, courtBounds) return { type: "player", id: "player-" + element.key + "-" + element.team, team: element.team, role: element.key, pos, ballState: BallState.NONE, path: null, actions: [], } } export function placeObjectAt( refBounds: DOMRect, courtBounds: DOMRect, rackedObject: RackedCourtObject, content: StepContent, ): StepContent { const pos = 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, pos, actions: [], } break } default: throw new Error("unknown court object " + rackedObject.key) } return { ...content, components: [...content.components, courtObject], } } export function dropBallOnComponent( targetedComponentIdx: number, content: StepContent, setAsOrigin: boolean, ): StepContent { 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: StepContent): StepContent { 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: StepContent, ): StepContent { 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 pos = ratioWithinBase(refBounds, courtBounds) const ball: Ball = { type: BALL_TYPE, id: BALL_ID, pos, 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: StepContent, removed: (content: StepContent) => StepContent, ): StepContent { 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) } const isPhantom = component.type === "phantom" if (isPhantom && component.pos.type === "follows") { const referent = component.pos.attach const origin = getOrigin(component, content.components) const originPathItems = origin.path!.items const phantomIdx = originPathItems.indexOf(component.id) const playerBeforePhantom: PlayerLike = phantomIdx == 0 ? origin : getComponent( originPathItems[phantomIdx - 1], content.components, ) // detach the action from the screen target and transform it to a regular move action to the phantom. content = updateComponent( { ...playerBeforePhantom, actions: playerBeforePhantom.actions.map((a) => a.target === referent ? { ...a, segments: a.segments.toSpliced( a.segments.length - 2, 1, { ...a.segments[a.segments.length - 1], next: component.id, }, ), target: component.id, type: ActionKind.MOVE, } : a, ), }, content, ) } content = updateComponent( { ...component, pos: isPhantom ? { type: "fixed", ...newPos, } : newPos, }, content, ) return content } export function removeComponent( componentId: ComponentId, content: StepContent, ): StepContent { return { ...content, components: content.components.filter((c) => c.id !== componentId), } } export function updateComponent( component: TacticComponent, content: StepContent, ): StepContent { return { ...content, components: content.components.map((c) => c.id === component.id ? component : c, ), } } 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 })) } /** * Returns a step content that only contains the terminal state of each components inside the given content * @param content * @param courtArea */ export function getTerminalState( content: StepContent, courtArea: DOMRect, ): StepContent { const nonPhantomComponents: (Player | CourtObject)[] = content.components.filter((c) => c.type !== "phantom") as ( | Player | CourtObject )[] const componentsTargetedState = nonPhantomComponents.map((comp) => comp.type === "player" ? getPlayerTerminalState(comp, content, courtArea) : comp, ) return { components: componentsTargetedState, } } function getPlayerTerminalState( player: Player, content: StepContent, area: DOMRect, ): Player { function stateAfter(state: BallState): BallState { switch (state) { case BallState.HOLDS_ORIGIN: return BallState.HOLDS_ORIGIN case BallState.PASSED_ORIGIN: case BallState.PASSED: return BallState.NONE case BallState.HOLDS_BY_PASS: return BallState.HOLDS_ORIGIN case BallState.NONE: return BallState.NONE } } function getTerminalPos(component: PlayerLike): Pos { return component.type === "phantom" ? computePhantomPositioning(component, content, area) : component.pos } const phantoms = player.path?.items if (!phantoms || phantoms.length === 0) { const pos = getTerminalPos(player) return { ...player, ballState: stateAfter(player.ballState), actions: [], pos, } } const lastPhantomId = phantoms[phantoms.length - 1] const lastPhantom = content.components.find( (c) => c.id === lastPhantomId, )! as PlayerPhantom const pos = getTerminalPos(lastPhantom) return { type: "player", path: { items: [] }, role: player.role, team: player.team, actions: [], ballState: stateAfter(lastPhantom.ballState), id: player.id, pos, } }