You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Application-Web/src/editor/PlayerDomains.ts

330 lines
9.2 KiB

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<PlayerLike>(pathItems[targetIdx - 1], components)
}
export function getPrecomputedPosition(
phantom: PlayerPhantom,
computedPositions: Map<string, Pos>,
): 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<string, Pos>,
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<T extends TacticComponent>(
id: string,
components: TacticComponent[],
): T {
return tryGetComponent<T>(id, components)!
}
export function tryGetComponent<T extends TacticComponent>(
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
}
const actionTarget = content.components.find(
(c) => c.id === action.target,
)! as PlayerLike
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,
)
}