drain changes on all children
continuous-integration/drone/push Build is passing Details

maxime 1 year ago
parent 4a9414edf6
commit e9d6640171

@ -1,4 +1,10 @@
import React, { KeyboardEventHandler, ReactNode, RefObject, useCallback, useRef } from "react"
import React, {
KeyboardEventHandler,
ReactNode,
RefObject,
useCallback,
useRef,
} from "react"
import "../../style/player.css"
import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece"
@ -26,7 +32,6 @@ export function CourtPlayer({
className,
availableActions,
}: CourtPlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null)
return courtPlayerPiece({
@ -52,7 +57,6 @@ export function EditableCourtPlayer({
const pieceRef = useRef<HTMLDivElement>(null)
const { x, y } = playerInfo.pos
return (
<Draggable
handle=".player-piece"
@ -67,10 +71,10 @@ export function EditableCourtPlayer({
if (
Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY ||
Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY
)
) {
onPositionValidated(pos)
}
}, [courtRef, onPositionValidated, x, y])}>
{courtPlayerPiece({
playerInfo,
className,
@ -87,7 +91,9 @@ export function EditableCourtPlayer({
)
}
interface CourtPlayerPieceProps extends CourtPlayerProps {
interface CourtPlayerPieceProps {
playerInfo: PlayerInfo
className?: string
pieceRef?: RefObject<HTMLDivElement>
availableActions?: () => ReactNode[]
onKeyUp?: KeyboardEventHandler<HTMLDivElement>
@ -103,7 +109,6 @@ function courtPlayerPiece({
const usesBall = playerInfo.ballState != BallState.NONE
const { x, y } = playerInfo.pos
return (
<div
ref={pieceRef}
@ -114,18 +119,10 @@ function courtPlayerPiece({
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<div
tabIndex={0}
className="player-content"
onKeyUp={onKeyUp}
>
{
availableActions && (
<div className="player-actions">
{availableActions()}
</div>
)
}
<div tabIndex={0} className="player-content" onKeyUp={onKeyUp}>
{availableActions && (
<div className="player-actions">{availableActions()}</div>
)}
<PlayerPiece
team={playerInfo.team}
text={playerInfo.role}

@ -1,12 +1,33 @@
import { equals, Pos, ratioWithinBase } from "../geo/Pos"
import { BallState, Player, PlayerInfo, PlayerLike, PlayerPhantom, PlayerTeam } from "../model/tactic/Player"
import { Ball, BALL_ID, BALL_TYPE, CourtObject } from "../model/tactic/CourtObjects"
import { ComponentId, StepContent, TacticComponent } from "../model/tactic/Tactic"
import {
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerPhantom,
PlayerTeam,
} from "../model/tactic/Player"
import {
Ball,
BALL_ID,
BALL_TYPE,
CourtObject,
} from "../model/tactic/CourtObjects"
import {
ComponentId,
StepContent,
TacticComponent,
} from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems"
import { getComponent, getOrigin, getPrecomputedPosition, tryGetComponent } from "./PlayerDomains"
import {
getComponent,
getOrigin,
getPrecomputedPosition,
tryGetComponent,
} from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts"
import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts"
@ -26,7 +47,7 @@ export function placePlayerAt(
ballState: BallState.NONE,
path: null,
actions: [],
frozen: false
frozen: false,
}
}
@ -365,7 +386,7 @@ function getPlayerTerminalState(
ballState: stateAfter(lastPhantom.ballState),
id: player.id,
pos,
frozen: true
frozen: true,
}
}
@ -395,8 +416,9 @@ export function drainTerminalStateOnChildContent(
if (
parentComponent.type !== "player" ||
childComponent.type !== "player"
)
) {
continue
}
const newContentResult = spreadNewStateFromOriginStateChange(
childComponent,

@ -19,10 +19,20 @@ import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent, TacticInfo } from "../model/tactic/Tactic"
import {
ComponentId,
CourtType,
StepContent,
StepInfoNode,
TacticComponent,
TacticInfo,
} from "../model/tactic/Tactic"
import { fetchAPI, fetchAPIGet } from "../Fetcher"
import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "../components/editor/CourtAction"
@ -43,10 +53,19 @@ import {
updateComponent,
} from "../editor/TacticContentDomains"
import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player"
import {
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx"
import {
CourtPlayer,
EditableCourtPlayer,
} from "../components/editor/CourtPlayer.tsx"
import {
createAction,
getActionKind,
@ -58,11 +77,21 @@ import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction"
import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains"
import {
computePhantomPositioning,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import { useNavigate, useParams } from "react-router-dom"
import StepsTree from "../components/editor/StepsTree"
import { addStepNode, getAvailableId, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain"
import {
addStepNode,
getAvailableId,
getParent,
getStepNode,
removeStepNode,
} from "../editor/StepsDomain"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@ -105,13 +134,10 @@ function GuestModeEditor() {
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0",
)
const stepInitialContent: ComputedStepContent = {
content: {
const stepInitialContent: StepContent = {
...(storageContent == null
? { components: [] }
: JSON.parse(storageContent)),
},
relativePositions: new Map(),
}
const rootStepNode: StepInfoNode = JSON.parse(
@ -130,6 +156,7 @@ function GuestModeEditor() {
)
}
const courtRef = useRef<HTMLDivElement>(null)
const [stepId, setStepId] = useState(ROOT_STEP_ID)
const [stepContent, setStepContent, saveState] = useContentState(
stepInitialContent,
@ -137,46 +164,40 @@ function GuestModeEditor() {
useMemo(
() =>
debounceAsync(
async ({
content,
relativePositions,
}: ComputedStepContent) => {
async (content: StepContent) => {
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
JSON.stringify(content),
)
const terminalState = computeTerminalState(
content,
relativePositions,
)
const currentStepNode = getStepNode(
rootStepNode,
stepId,
)!
for (const child of currentStepNode.children) {
const childCurrentContent = getStepContent(child.id)
const childUpdatedContent =
drainTerminalStateOnChildContent(
terminalState,
childCurrentContent,
const stepsTree: StepInfoNode = JSON.parse(
localStorage.getItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
)!,
)
if (childUpdatedContent) {
localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY +
await updateStepContents(
stepId,
JSON.stringify(childUpdatedContent),
stepsTree,
async (stepId) => {
const content = JSON.parse(localStorage.getItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
)!)
const courtBounds = courtRef.current!.getBoundingClientRect()
const relativePositions = computeRelativePositions(courtBounds, content)
return { content, relativePositions }
},
async (stepId, content) => localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
JSON.stringify(content),
),
)
}
}
return SaveStates.Guest
},
250,
),
[rootStepNode, stepId],
[stepId],
),
)
@ -196,6 +217,7 @@ function GuestModeEditor() {
"Nouvelle Tactique",
courtType: "PLAIN",
}}
courtRef={courtRef}
currentStepContent={stepContent}
setCurrentStepContent={(content) => setStepContent(content, true)}
saveState={saveState}
@ -207,13 +229,7 @@ function GuestModeEditor() {
selectStep={useCallback(
(step) => {
setStepId(step)
setStepContent(
() => ({
content: getStepContent(step),
relativePositions: new Map(),
}),
false,
)
setStepContent(getStepContent(step), false)
return
},
[setStepContent],
@ -264,38 +280,41 @@ function UserModeEditor() {
const tacticId = parseInt(idStr!)
const navigation = useNavigate()
const courtRef = useRef<HTMLDivElement>(null)
const [stepId, setStepId] = useState(1)
const saveContent = useCallback(
async ({ content, relativePositions }: ComputedStepContent) => {
async (content: StepContent) => {
const response = await fetchAPI(
`tactics/${tacticId}/steps/${stepId}`,
{ content },
"PUT",
)
const terminalStateContent = computeTerminalState(
content,
relativePositions,
)
const currentNode = getStepNode(stepsTree!, stepId)!
const tasks = currentNode.children.map(async (child) => {
await updateStepContents(
stepId,
stepsTree,
async (id) => {
const response = await fetchAPIGet(
`tactics/${tacticId}/steps/${child.id}`,
`tactics/${tacticId}/steps/${id}`,
)
if (!response.ok)
throw new Error("Error when retrieving children content")
const childContent: StepContent = await response.json()
const childUpdatedContent = drainTerminalStateOnChildContent(
terminalStateContent,
childContent,
)
if (childUpdatedContent) {
const content = await response.json()
const courtBounds = courtRef.current!.getBoundingClientRect()
const relativePositions = computeRelativePositions(courtBounds, content)
return {
content,
relativePositions,
}
},
async (id, content) => {
const response = await fetchAPI(
`tactics/${tacticId}/steps/${child.id}`,
{ content: childUpdatedContent },
`tactics/${tacticId}/steps/${id}`,
{ content },
"PUT",
)
if (!response.ok) {
@ -303,12 +322,8 @@ function UserModeEditor() {
"Error when updated new children content",
)
}
}
})
for (const task of tasks) {
await task
}
},
)
return response.ok ? SaveStates.Ok : SaveStates.Err
},
@ -316,11 +331,8 @@ function UserModeEditor() {
)
const [stepContent, setStepContent, saveState] =
useContentState<ComputedStepContent>(
{
content: { components: [] },
relativePositions: new Map(),
},
useContentState<StepContent>(
{ components: [] },
SaveStates.Ok,
useMemo(() => debounceAsync(saveContent, 250), [saveContent]),
)
@ -352,7 +364,7 @@ function UserModeEditor() {
setTactic({ id: tacticId, name, courtType })
setStepsTree(root)
setStepContent({ content, relativePositions: new Map() }, false)
setStepContent(content, false)
}
if (tactic === null) initialize()
@ -374,10 +386,7 @@ function UserModeEditor() {
if (!response.ok) return
setStepId(step)
setStepContent(
{
content: await response.json(),
relativePositions: new Map(),
},
await response.json(),
false,
)
},
@ -422,6 +431,7 @@ function UserModeEditor() {
rootStepNode: stepsTree,
courtType: tactic?.courtType,
}}
courtRef={courtRef}
currentStepId={stepId}
currentStepContent={stepContent}
setCurrentStepContent={(content) => setStepContent(content, true)}
@ -440,10 +450,12 @@ function EditorLoadingScreen() {
export interface EditorViewProps {
tactic: TacticInfo
currentStepContent: ComputedStepContent
currentStepContent: StepContent
currentStepId: number
saveState: SaveState
setCurrentStepContent: Dispatch<SetStateAction<ComputedStepContent>>
setCurrentStepContent: Dispatch<SetStateAction<StepContent>>
courtRef: RefObject<HTMLDivElement>
selectStep: (stepId: number) => void
onNameChange: (name: string) => Promise<boolean>
@ -457,13 +469,15 @@ export interface EditorViewProps {
function EditorPage({
tactic: { name, rootStepNode: initialStepsNode, courtType },
currentStepId,
setCurrentStepContent,
currentStepContent: { content, relativePositions },
setCurrentStepContent: setContent,
currentStepContent: content,
saveState,
onNameChange,
selectStep,
onRemoveStep,
onAddStep,
courtRef,
}: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -486,33 +500,34 @@ function EditorPage({
const [isStepsTreeVisible, setStepsTreeVisible] = useState(false)
const courtRef = useRef<HTMLDivElement>(null)
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const setContent = useCallback(
(newState: SetStateAction<StepContent>) => {
setCurrentStepContent((c) => {
const state =
typeof newState === "function"
? newState(c.content)
: newState
const relativePositions = useMemo(() => {
const courtBounds = courtRef.current?.getBoundingClientRect()
const relativePositions: ComputedRelativePositions = courtBounds
? computeRelativePositions(courtBounds, state)
: new Map()
return {
content: state,
relativePositions,
}
})
},
[setCurrentStepContent],
)
return courtBounds ? computeRelativePositions(courtBounds, content) : new Map()
}, [content, courtRef])
// const setContent = useCallback(
// (newState: SetStateAction<StepContent>) => {
// setCurrentStepContent((c) => {
// const state =
// typeof newState === "function"
// ? newState(c.content)
// : newState
//
// const courtBounds = courtRef.current?.getBoundingClientRect()
// const relativePositions: ComputedRelativePositions = courtBounds
// ? computeRelativePositions(courtBounds, state)
// : new Map()
//
// return state
// })
// },
// [setCurrentStepContent],
// )
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
setContent((c) => ({
@ -638,7 +653,8 @@ function EditorPage({
setContent={setContent}
/>
),
!isFrozen && (info.ballState === BallState.HOLDS_ORIGIN ||
!isFrozen &&
(info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction
key={2}
@ -674,11 +690,16 @@ function EditorPage({
info = component
if (component.frozen) {
return <CourtPlayer
return (
<CourtPlayer
key={component.id}
playerInfo={info}
className={"player"}
availableActions={() => renderAvailablePlayerActions(info, component)}
availableActions={() =>
renderAvailablePlayerActions(info, component)
}
/>
)
}
}
@ -1213,3 +1234,40 @@ function computeRelativePositions(courtBounds: DOMRect, content: StepContent) {
return relativePositionsCache
}
async function updateStepContents(stepId: number,
stepsTree: StepInfoNode,
getStepContent: (stepId: number) => Promise<ComputedStepContent>,
setStepContent: (stepId: number, content: StepContent) => Promise<void>,
) {
async function updateSteps(step: StepInfoNode, content: StepContent, relativePositions: ComputedRelativePositions) {
const terminalStateContent = computeTerminalState(
content,
relativePositions,
)
const tasks = step.children.map(async (child) => {
const { content: childContent, relativePositions: childRelativePositions } = await getStepContent(child.id)
const childUpdatedContent = drainTerminalStateOnChildContent(
terminalStateContent,
childContent,
)
if (childUpdatedContent) {
await setStepContent(child.id, childUpdatedContent)
await updateSteps(child, childUpdatedContent, childRelativePositions)
}
})
for (const task of tasks) {
await task
}
}
const { content, relativePositions } = await getStepContent(stepId)
const startNode = getStepNode(stepsTree!, stepId)!
await updateSteps(startNode, content, relativePositions)
}
Loading…
Cancel
Save