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.
330 lines
9.2 KiB
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,
|
|
)
|
|
}
|