@@ -109,17 +104,19 @@ interface StepPieceProps {
}
function StepPiece({
- id,
- isSelected,
- onAddButtonClicked,
- onRemoveButtonClicked,
- onSelected,
- }: StepPieceProps) {
+ id,
+ isSelected,
+ onAddButtonClicked,
+ onRemoveButtonClicked,
+ onSelected,
+}: StepPieceProps) {
return (
{onAddButtonClicked && (
diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts
index 835b147..bb92dd4 100644
--- a/src/editor/StepsDomain.ts
+++ b/src/editor/StepsDomain.ts
@@ -40,13 +40,16 @@ export function removeStepNode(
* @param root
*/
export function getAvailableId(root: StepInfoNode): number {
- const acc = (root: StepInfoNode): number => Math.max(root.id, ...root.children.map(acc))
+ const acc = (root: StepInfoNode): number =>
+ Math.max(root.id, ...root.children.map(acc))
return acc(root) + 1
}
-export function getParent(root: StepInfoNode, node: StepInfoNode): StepInfoNode | null {
- if (root.children.find(n => n.id === node.id))
- return root
+export function getParent(
+ root: StepInfoNode,
+ node: StepInfoNode,
+): StepInfoNode | null {
+ if (root.children.find((n) => n.id === node.id)) return root
for (const child of root.children) {
const result = getParent(child, node)
@@ -55,4 +58,4 @@ export function getParent(root: StepInfoNode, node: StepInfoNode): StepInfoNode
}
}
return null
-}
\ No newline at end of file
+}
diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts
index 2c682f5..c6ac64f 100644
--- a/src/editor/TacticContentDomains.ts
+++ b/src/editor/TacticContentDomains.ts
@@ -5,6 +5,7 @@ import {
Player,
PlayerInfo,
PlayerLike,
+ PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import {
@@ -15,13 +16,18 @@ import {
} from "../model/tactic/CourtObjects"
import {
ComponentId,
- TacticComponent,
StepContent,
+ TacticComponent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems"
-import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains"
+import {
+ changePlayerBallState,
+ computePhantomPositioning,
+ getComponent,
+ getOrigin,
+} from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts"
export function placePlayerAt(
@@ -289,3 +295,85 @@ export function getRackPlayers(
)
.map((key) => ({ team, key }))
}
+
+/**
+ * Returns a step content that only contains the terminal state of each components inside the given content
+ * @param content
+ * @param courtArea
+ */
+export function getTerminalState(
+ content: StepContent,
+ courtArea: DOMRect,
+): StepContent {
+ const nonPhantomComponents: (Player | CourtObject)[] =
+ content.components.filter((c) => c.type !== "phantom") as (
+ | Player
+ | CourtObject
+ )[]
+
+ const componentsTargetedState = nonPhantomComponents.map((comp) =>
+ comp.type === "player"
+ ? getPlayerTerminalState(comp, content, courtArea)
+ : comp,
+ )
+
+ return {
+ components: componentsTargetedState,
+ }
+}
+
+function getPlayerTerminalState(
+ player: Player,
+ content: StepContent,
+ area: DOMRect,
+): Player {
+ function stateAfter(state: BallState): BallState {
+ switch (state) {
+ case BallState.HOLDS_ORIGIN:
+ return BallState.HOLDS_ORIGIN
+ case BallState.PASSED_ORIGIN:
+ case BallState.PASSED:
+ return BallState.NONE
+ case BallState.HOLDS_BY_PASS:
+ return BallState.HOLDS_ORIGIN
+ case BallState.NONE:
+ return BallState.NONE
+ }
+ }
+
+ function getTerminalPos(component: PlayerLike): Pos {
+ return component.type === "phantom"
+ ? computePhantomPositioning(component, content, area)
+ : component.pos
+ }
+
+ const phantoms = player.path?.items
+ if (!phantoms || phantoms.length === 0) {
+ const pos = getTerminalPos(player)
+
+ return {
+ ...player,
+ ballState: stateAfter(player.ballState),
+ actions: [],
+ pos,
+ }
+ }
+ const lastPhantomId = phantoms[phantoms.length - 1]
+ const lastPhantom = content.components.find(
+ (c) => c.id === lastPhantomId,
+ )! as PlayerPhantom
+
+ const pos = getTerminalPos(lastPhantom)
+
+ return {
+ type: "player",
+ path: { items: [] },
+ role: player.role,
+ team: player.team,
+
+ actions: [],
+ ballState: stateAfter(lastPhantom.ballState),
+ id: player.id,
+ pos,
+ }
+}
diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx
index 02b81cd..5068f85 100644
--- a/src/pages/Editor.tsx
+++ b/src/pages/Editor.tsx
@@ -42,6 +42,7 @@ import {
dropBallOnComponent,
getComponentCollided,
getRackPlayers,
+ getTerminalState,
moveComponent,
placeBallAt,
placeObjectAt,
@@ -79,7 +80,12 @@ import {
import { CourtBall } from "../components/editor/CourtBall"
import { useNavigate, useParams } from "react-router-dom"
import StepsTree from "../components/editor/StepsTree"
-import { addStepNode, getAvailableId, getParent, removeStepNode } from "../editor/StepsDomain"
+import {
+ addStepNode,
+ getAvailableId,
+ getParent,
+ removeStepNode,
+} from "../editor/StepsDomain"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@@ -96,7 +102,6 @@ interface TacticDto {
id: number
name: string
courtType: CourtType
- content: { components: TacticComponent[] }
root: StepInfoNode
}
@@ -108,78 +113,124 @@ export default function Editor({ guestMode }: EditorPageProps) {
return
}
-
function EditorPortal({ guestMode }: EditorPageProps) {
return guestMode ? :
}
function GuestModeEditor() {
- const storageContent = localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0")
+ const storageContent = localStorage.getItem(
+ GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0",
+ )
- const stepInitialContent = ({
- ...(storageContent == null ? { components: [] } : JSON.parse(storageContent)),
+ const stepInitialContent = {
+ ...(storageContent == null
+ ? { components: [] }
+ : JSON.parse(storageContent)),
stepId: 0,
- })
+ }
// initialize local storage if we launch in guest mode
if (storageContent == null) {
- localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify({ id: 0, children: [] }))
+ localStorage.setItem(
+ GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
+ JSON.stringify({ id: 0, children: [] }),
+ )
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepInitialContent.stepId,
JSON.stringify(stepInitialContent),
)
}
-
const [stepId, setStepId] = useState(DEFAULT_STEP_ID)
const [stepContent, setStepContent, saveState] = useContentState(
stepInitialContent,
SaveStates.Guest,
- useMemo(() => debounceAsync(async (content: StepContent) => {
- localStorage.setItem(
- GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
- JSON.stringify(content),
- )
- return SaveStates.Guest
- }, 250), [stepId]),
+ useMemo(
+ () =>
+ debounceAsync(async (content: StepContent) => {
+ localStorage.setItem(
+ GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
+ JSON.stringify(content),
+ )
+ return SaveStates.Guest
+ }, 250),
+ [stepId],
+ ),
)
- return setStepContent(content, true)}
- saveState={saveState}
- currentStepId={stepId}
- onNameChange={useCallback(async name => {
- localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
- return true //simulate that the name has been changed
- }, [])}
- selectStep={useCallback(step => {
- setStepId(step)
- setStepContent({ ...JSON.parse(localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step)!) }, false)
- return
- }, [setStepContent])}
- onAddStep={useCallback(async (parent, content) => {
- const root: StepInfoNode = JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!)
-
- const nodeId = getAvailableId(root)
- const node = { id: nodeId, children: [] }
-
- localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify(addStepNode(root, parent, node)))
- localStorage.setItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id, JSON.stringify(content))
- return node
- }, [])}
- onRemoveStep={useCallback(async step => {
- const root: StepInfoNode = JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!)
- localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify(removeStepNode(root, step)))
- return true
- }, [])}
- />
+ return (
+ setStepContent(content, true)}
+ saveState={saveState}
+ currentStepId={stepId}
+ onNameChange={useCallback(async (name) => {
+ localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
+ return true //simulate that the name has been changed
+ }, [])}
+ selectStep={useCallback(
+ (step) => {
+ setStepId(step)
+ setStepContent(
+ {
+ ...JSON.parse(
+ localStorage.getItem(
+ GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step,
+ )!,
+ ),
+ },
+ false,
+ )
+ return
+ },
+ [setStepContent],
+ )}
+ onAddStep={useCallback(async (parent, content) => {
+ const root: StepInfoNode = JSON.parse(
+ localStorage.getItem(
+ GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
+ )!,
+ )
+
+ const nodeId = getAvailableId(root)
+ const node = { id: nodeId, children: [] }
+
+ localStorage.setItem(
+ GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
+ JSON.stringify(addStepNode(root, parent, node)),
+ )
+ localStorage.setItem(
+ GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id,
+ JSON.stringify(content),
+ )
+ return node
+ }, [])}
+ onRemoveStep={useCallback(async (step) => {
+ const root: StepInfoNode = JSON.parse(
+ localStorage.getItem(
+ GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
+ )!,
+ )
+ localStorage.setItem(
+ GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
+ JSON.stringify(removeStepNode(root, step)),
+ )
+ return true
+ }, [])}
+ />
+ )
}
function UserModeEditor() {
@@ -188,12 +239,36 @@ function UserModeEditor() {
const id = parseInt(idStr!)
const navigation = useNavigate()
- useEffect(() => {
+ const [stepId, setStepId] = useState(1)
+ const [stepContent, setStepContent, saveState] =
+ useContentState(
+ { components: [] },
+ SaveStates.Ok,
+ useMemo(
+ () =>
+ debounceAsync(async (content: StepContent) => {
+ const response = await fetchAPI(
+ `tactics/${id}/steps/${stepId}`,
+ {
+ content: {
+ components: content.components,
+ },
+ },
+ "PUT",
+ )
+ return response.ok ? SaveStates.Ok : SaveStates.Err
+ }, 250),
+ [id, stepId],
+ ),
+ )
+ useEffect(() => {
async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${id}`)
const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`)
- const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/${DEFAULT_STEP_ID}`)
+ const contentResponsePromise = fetchAPIGet(
+ `tactics/${id}/steps/${DEFAULT_STEP_ID}`,
+ )
const infoResponse = await infoResponsePromise
const treeResponse = await treeResponsePromise
@@ -212,82 +287,70 @@ function UserModeEditor() {
const content = await contentResponse.json()
const { root } = await treeResponse.json()
- setTactic({ id, name, courtType, content, root })
+ setTactic({ id, name, courtType, root })
+ setStepContent(content, false)
}
initialize()
}, [id, idStr, navigation])
+ const onNameChange = useCallback(
+ (name: string) =>
+ fetchAPI(`tactics/${id}/name`, { name }, "PUT").then((r) => r.ok),
+ [id],
+ )
- const [stepId, setStepId] = useState(1)
- const [stepContent, setStepContent, saveState] = useContentState(
- tactic?.content ?? { components: [] },
- SaveStates.Ok,
- useMemo(() => debounceAsync(async (content: StepContent) => {
- const response = await fetchAPI(
- `tactics/${id}/steps/${stepId}`,
- {
- content: {
- components: content.components,
- },
- },
- "PUT",
- )
- return response.ok ? SaveStates.Ok : SaveStates.Err
- }, 250), [id, stepId]),
+ const selectStep = useCallback(
+ async (step: number) => {
+ const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
+ if (!response.ok) return
+ setStepId(step)
+ setStepContent({ ...(await response.json()) }, false)
+ },
+ [id, setStepContent],
+ )
+
+ const onAddStep = useCallback(
+ async (parent: StepInfoNode, content: StepContent) => {
+ const response = await fetchAPI(`tactics/${id}/steps`, {
+ parentId: parent.id,
+ content,
+ })
+ if (!response.ok) return null
+ const { stepId } = await response.json()
+ return { id: stepId, children: [] }
+ },
+ [id],
+ )
+
+ const onRemoveStep = useCallback(
+ (step: StepInfoNode) =>
+ fetchAPI(`tactics/${id}/steps/${step.id}`, {}, "DELETE").then(
+ (r) => r.ok,
+ ),
+ [id],
+ )
+
+ if (!tactic) return
+
+ return (
+ setStepContent(content, true)}
+ saveState={saveState}
+ onNameChange={onNameChange}
+ selectStep={selectStep}
+ onAddStep={onAddStep}
+ onRemoveStep={onRemoveStep}
+ />
)
- const onNameChange = useCallback((name: string) =>
- fetchAPI(`tactics/${id}/name`, { name }, "PUT")
- .then((r) => r.ok)
- , [id])
-
- const selectStep = useCallback(async (step: number) => {
- const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
- if (!response.ok)
- return
- setStepId(step)
- setStepContent({ ...await response.json() }, false)
- }, [id, setStepContent])
-
- const onAddStep = useCallback(async (parent: StepInfoNode, content: StepContent) => {
- const response = await fetchAPI(`tactics/${id}/steps`, {
- parentId: parent.id,
- content
- })
- if (!response.ok)
- return null
- const { stepId } = await response.json()
- return { id: stepId, children: [] }
- }, [id])
-
- const onRemoveStep = useCallback((step: StepInfoNode) =>
- fetchAPI(
- `tactics/${id}/steps/${step.id}`,
- {},
- "DELETE",
- ).then(r => r.ok)
- , [id])
-
-
- if (!tactic)
- return
-
- return setStepContent(content, true)}
- saveState={saveState}
- onNameChange={onNameChange}
- selectStep={selectStep}
- onAddStep={onAddStep}
- onRemoveStep={onRemoveStep}
- />
}
function EditorLoadingScreen() {
@@ -304,26 +367,23 @@ export interface EditorViewProps {
selectStep: (stepId: number) => void
onNameChange: (name: string) => Promise
onRemoveStep: (step: StepInfoNode) => Promise
- onAddStep: (parent: StepInfoNode, content: StepContent) => Promise
+ onAddStep: (
+ parent: StepInfoNode,
+ content: StepContent,
+ ) => Promise
}
-
-
function EditorPage({
- tactic: {
- name,
- rootStepNode: initialStepsNode,
- courtType,
- },
- setCurrentStepContent: setContent,
- currentStepContent: content,
- saveState,
- onNameChange,
- selectStep,
- onRemoveStep,
- onAddStep,
- }: EditorViewProps) {
-
+ tactic: { name, rootStepNode: initialStepsNode, courtType },
+ currentStepId,
+ setCurrentStepContent: setContent,
+ currentStepContent: content,
+ saveState,
+ onNameChange,
+ selectStep,
+ onRemoveStep,
+ onAddStep,
+}: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@@ -693,29 +753,35 @@ function EditorPage({
{
- const addedNode = await onAddStep(parent, content)
+ const addedNode = await onAddStep(
+ parent,
+ getTerminalState(content, courtBounds()),
+ )
if (addedNode == null) {
console.error(
"could not add step : onAddStep returned null node",
)
return
}
+ selectStep(addedNode.id)
setRootStepsNode((root) =>
addStepNode(root, parent, addedNode),
)
},
- [content, onAddStep],
+ [content, courtBounds, onAddStep, selectStep],
)}
onRemoveNode={useCallback(
async (removed) => {
const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id)
- if (isOk) setRootStepsNode(
- (root) => removeStepNode(root, removed)!,
- )
+ if (isOk)
+ setRootStepsNode(
+ (root) => removeStepNode(root, removed)!,
+ )
},
[rootStepsNode, onRemoveStep, selectStep],
)}
@@ -731,6 +797,7 @@ function EditorPage({
interface EditorStepsTreeProps {
isVisible: boolean
+ selectedStepId: number
root: StepInfoNode
onAddChildren: (parent: StepInfoNode) => void
onRemoveNode: (node: StepInfoNode) => void
@@ -739,6 +806,7 @@ interface EditorStepsTreeProps {
function EditorStepsTree({
isVisible,
+ selectedStepId,
root,
onAddChildren,
onRemoveNode,
@@ -752,6 +820,7 @@ function EditorStepsTree({
}}>
{
- (document.activeElement as HTMLElement).blur()
+ ;(document.activeElement as HTMLElement).blur()
setPreviewAction({
origin: playerInfo.id,
diff --git a/src/style/steps_tree.css b/src/style/steps_tree.css
index 4b58090..462b930 100644
--- a/src/style/steps_tree.css
+++ b/src/style/steps_tree.css
@@ -32,7 +32,9 @@
background-color: var(--editor-tree-step-piece-hovered);
}
-.step-piece-selected .step-piece-actions, .step-piece:hover .step-piece-actions, .step-piece:focus-within .step-piece-actions {
+.step-piece-selected .step-piece-actions,
+.step-piece:hover .step-piece-actions,
+.step-piece:focus-within .step-piece-actions {
visibility: visible;
}