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.
247 lines
8.3 KiB
247 lines
8.3 KiB
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
|
|
const result = targetIdx == 0 ? playerOrigin : getComponent<PlayerLike>(pathItems[targetIdx - 1], components)
|
|
return result
|
|
}
|
|
|
|
//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<T extends TacticComponent>(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 actionIdx = playerBefore.actions.findIndex(a => a.target === pos.attach)
|
|
content = updateComponent({
|
|
...playerBefore,
|
|
actions: playerBefore.actions.toSpliced(actionIdx, 1)
|
|
}, content)
|
|
}
|
|
|
|
const origin = getOrigin(player, content.components)
|
|
content = truncatePlayerPath(origin, player, content)
|
|
console.log(content)
|
|
return 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)
|
|
}
|