import {BallState, Player, PlayerLike, PlayerPhantom,} from "../model/tactic/Player" import {ComponentId, TacticComponent, TacticContent,} 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) } //FIXME this function can be a bottleneck if the phantom's position is // following another phantom and / or the origin of the phantom is another export function computePhantomPositioning( phantom: PlayerPhantom, content: TacticContent, area: DOMRect, ): Pos { const positioning = phantom.pos // If the position is already known and fixed, return the pos if (positioning.type === "fixed") return positioning // 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, 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 const pivotPoint = lastSegment.controlPoint ?? (lastSegmentStart ? typeof lastSegmentStart === "string" ? document .getElementById(lastSegmentStart)! .getBoundingClientRect() : lastSegmentStart : playerBeforePhantom.type === "phantom" ? computePhantomPositioning(playerBeforePhantom, content, area) : playerBeforePhantom.pos) const segment = posWithinBase(relativeTo(referentPos, pivotPoint), area) const segmentLength = norm(segment) const phantomDistanceFromReferent = PLAYER_RADIUS_PIXELS //TODO Place this in constants const segmentProjection = minus(area, { x: (segment.x / segmentLength) * phantomDistanceFromReferent, y: (segment.y / segmentLength) * phantomDistanceFromReferent, }) const segmentProjectionRatio: Pos = ratioWithinBase(segmentProjection, area) return add(referentPos, segmentProjectionRatio) } export function getComponent( id: string, components: TacticComponent[], ): T { 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: TacticContent, ): TacticContent { 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: TacticContent, ): TacticContent { 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: TacticContent, ): TacticContent { 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 } const actionTarget = content.components.find( (c) => c.id === action.target, )! as PlayerLike return spreadNewStateFromOriginStateChange( actionTarget, BallState.NONE, content, ) } return content } export function truncatePlayerPath( player: Player, phantom: PlayerPhantom, content: TacticContent, ): TacticContent { 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, ) } export function changePlayerBallState( player: PlayerLike, newState: BallState, content: TacticContent, ): TacticContent { return spreadNewStateFromOriginStateChange(player, newState, content) }