import { BallState, Player, PlayerLike, PlayerPhantom, } from "../model/tactic/Player" import { ComponentId, StepContent, TacticComponent, } from "../model/tactic/Tactic" import { removeComponent, updateComponent } from "./TacticContentDomains" import { removeAllActionsTargeting, spreadNewStateFromOriginStateChange, } from "./ActionsDomains" import { ActionKind } from "../model/tactic/Action" import { add, minus, norm, Pos, posWithinBase, ratioWithinBase, relativeTo, } from "../geo/Pos.ts" import { PLAYER_RADIUS_PIXELS } from "../components/editor/CourtPlayer.tsx" 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 getPlayerNextTo( player: PlayerLike, n: number, components: TacticComponent[], ): PlayerLike | undefined { const playerOrigin = player.type === "player" ? player : getOrigin(player, components) const pathItems = playerOrigin.path!.items // add one as there is a shifting because a Player is never at the head of its own path const idx = pathItems.indexOf(player.id) + 1 // is 0 if the player is the origin const targetIdx = idx + n // remove the screen phantom return targetIdx == 0 ? playerOrigin : getComponent(pathItems[targetIdx - 1], components) } export function getPrecomputedPosition( phantom: PlayerPhantom, computedPositions: Map, ): Pos | undefined { const pos = phantom.pos // If the position is already known and fixed, return the pos if (pos.type === "fixed") return pos return computedPositions.get(phantom.id) } export function computePhantomPositioning( phantom: PlayerPhantom, content: StepContent, computedPositions: Map, area: DOMRect, ): Pos { const positioning = phantom.pos // If the position is already known and fixed, return the pos if (positioning.type === "fixed") return positioning const storedPos = computedPositions.get(phantom.id) if (storedPos) return storedPos // If the position is to determine (positioning.type = "follows"), determine the phantom's pos // by calculating it from the referent position, and the action that targets the referent. const components = content.components // Get the referent from the components const referent: PlayerLike = getComponent(positioning.attach, components) const referentPos = referent.type === "player" ? referent.pos : computePhantomPositioning( referent, content, computedPositions, area, ) // Get the origin const origin = getOrigin(phantom, components) const originPathItems = origin.path!.items const phantomIdx = originPathItems.indexOf(phantom.id) const playerBeforePhantom: PlayerLike = phantomIdx == 0 ? origin : getComponent(originPathItems[phantomIdx - 1], components) const action = playerBeforePhantom.actions.find( (a) => a.target === positioning.attach, )! const segments = action.segments const lastSegment = segments[segments.length - 1] const lastSegmentStart = segments[segments.length - 2]?.next let pivotPoint = lastSegment.controlPoint if (!pivotPoint) { if (lastSegmentStart) { pivotPoint = typeof lastSegmentStart === "string" ? document .getElementById(lastSegmentStart)! .getBoundingClientRect() : lastSegmentStart } else { pivotPoint = playerBeforePhantom.type === "phantom" ? computePhantomPositioning( playerBeforePhantom, content, computedPositions, area, ) : playerBeforePhantom.pos } } const segment = posWithinBase(relativeTo(referentPos, pivotPoint), area) const segmentLength = norm(segment) const segmentProjection = minus(area, { x: (segment.x / segmentLength) * PLAYER_RADIUS_PIXELS, y: (segment.y / segmentLength) * PLAYER_RADIUS_PIXELS, }) const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area) const result = add(referentPos, segmentProjectionRatio) computedPositions.set(phantom.id, result) return result } export function getComponent( id: string, components: TacticComponent[], ): T { return tryGetComponent(id, components)! } export function tryGetComponent( id: string, components: TacticComponent[], ): T | undefined { return components.find((c) => c.id === id) as T } export function areInSamePath(a: PlayerLike, b: PlayerLike) { if (a.type === "phantom" && b.type === "phantom") { return a.originPlayerId === b.originPlayerId } if (a.type === "phantom") { return b.id === a.originPlayerId } if (b.type === "phantom") { return a.id === b.originPlayerId } return false } /** * @param origin * @param other * @param components * @returns true if the `other` player is the phantom next-to the origin's path. */ export function isNextInPath( origin: PlayerLike, other: PlayerLike, components: TacticComponent[], ): boolean { if (origin.type === "player") { return origin.path?.items[0] === other.id } const originPath = getOrigin(origin, components).path! return ( originPath.items!.indexOf(origin.id) === originPath.items!.indexOf(other.id) - 1 ) } export function clearPlayerPath( player: Player, content: StepContent, ): StepContent { if (player.path == null) { return content } for (const pathElement of player.path.items) { content = removeComponent(pathElement, content) content = removeAllActionsTargeting(pathElement, content) } return updateComponent( { ...player, path: null, }, content, ) } function removeAllPhantomsAttached( to: ComponentId, content: StepContent, ): StepContent { let i = 0 while (i < content.components.length) { const component = content.components[i] if (component.type === "phantom") { if ( component.pos.type === "follows" && component.pos.attach === to ) { content = removePlayer(component, content) continue } } i++ } return content } export function removePlayer( player: PlayerLike, content: StepContent, ): StepContent { content = removeAllActionsTargeting(player.id, content) content = removeAllPhantomsAttached(player.id, content) if (player.type === "phantom") { const pos = player.pos // if the phantom was attached to another player, remove the action that symbolizes the attachment if (pos.type === "follows") { const playerBefore = getPlayerNextTo( player, -1, content.components, )! const actions = playerBefore.actions.filter( (a) => a.target !== pos.attach, ) content = updateComponent( { ...playerBefore, actions, }, content, ) } const origin = getOrigin(player, content.components) return truncatePlayerPath(origin, player, content) } content = clearPlayerPath(player, content) content = removeComponent(player.id, content) for (const action of player.actions) { if (action.type !== ActionKind.SHOOT) { continue } if (typeof action.target !== "string") continue const actionTarget = tryGetComponent(action.target, content.components) if (actionTarget === undefined) continue //the target was maybe removed return ( spreadNewStateFromOriginStateChange( actionTarget, BallState.NONE, content, ) ?? content ) } return content } export function truncatePlayerPath( player: Player, phantom: PlayerPhantom, content: StepContent, ): StepContent { if (player.path == null) return content const path = player.path! const truncateStartIdx = path.items.indexOf(phantom.id) for (let i = truncateStartIdx; i < path.items.length; i++) { const pathPhantomId = path.items[i] //remove the phantom from the tactic content = removeComponent(pathPhantomId, content) content = removeAllActionsTargeting(pathPhantomId, content) } return updateComponent( { ...player, path: truncateStartIdx == 0 ? null : { ...path, items: path.items.toSpliced(truncateStartIdx), }, }, content, ) }