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 "../../style/player.css"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
@ -22,11 +28,10 @@ const MOVE_AREA_SENSIBILITY = 0.001
export const PLAYER_RADIUS_PIXELS = 20 export const PLAYER_RADIUS_PIXELS = 20
export function CourtPlayer({ export function CourtPlayer({
playerInfo, playerInfo,
className, className,
availableActions, availableActions,
}: CourtPlayerProps) { }: CourtPlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
return courtPlayerPiece({ return courtPlayerPiece({
@ -41,18 +46,17 @@ export function CourtPlayer({
* A player that is placed on the court, which can be selected, and moved in the associated bounds * A player that is placed on the court, which can be selected, and moved in the associated bounds
* */ * */
export function EditableCourtPlayer({ export function EditableCourtPlayer({
playerInfo, playerInfo,
className, className,
courtRef, courtRef,
onPositionValidated, onPositionValidated,
onRemove, onRemove,
availableActions, availableActions,
}: EditableCourtPlayerProps) { }: EditableCourtPlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null) const pieceRef = useRef<HTMLDivElement>(null)
const { x, y } = playerInfo.pos const { x, y } = playerInfo.pos
return ( return (
<Draggable <Draggable
handle=".player-piece" handle=".player-piece"
@ -67,10 +71,10 @@ export function EditableCourtPlayer({
if ( if (
Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY || Math.abs(pos.x - x) >= MOVE_AREA_SENSIBILITY ||
Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY Math.abs(pos.y - y) >= MOVE_AREA_SENSIBILITY
) ) {
onPositionValidated(pos) onPositionValidated(pos)
}
}, [courtRef, onPositionValidated, x, y])}> }, [courtRef, onPositionValidated, x, y])}>
{courtPlayerPiece({ {courtPlayerPiece({
playerInfo, playerInfo,
className, className,
@ -87,23 +91,24 @@ export function EditableCourtPlayer({
) )
} }
interface CourtPlayerPieceProps extends CourtPlayerProps { interface CourtPlayerPieceProps {
playerInfo: PlayerInfo
className?: string
pieceRef?: RefObject<HTMLDivElement> pieceRef?: RefObject<HTMLDivElement>
availableActions?: () => ReactNode[] availableActions?: () => ReactNode[]
onKeyUp?: KeyboardEventHandler<HTMLDivElement> onKeyUp?: KeyboardEventHandler<HTMLDivElement>
} }
function courtPlayerPiece({ function courtPlayerPiece({
playerInfo, playerInfo,
className, className,
pieceRef, pieceRef,
onKeyUp, onKeyUp,
availableActions, availableActions,
}: CourtPlayerPieceProps) { }: CourtPlayerPieceProps) {
const usesBall = playerInfo.ballState != BallState.NONE const usesBall = playerInfo.ballState != BallState.NONE
const { x, y } = playerInfo.pos const { x, y } = playerInfo.pos
return ( return (
<div <div
ref={pieceRef} ref={pieceRef}
@ -114,18 +119,10 @@ function courtPlayerPiece({
left: `${x * 100}%`, left: `${x * 100}%`,
top: `${y * 100}%`, top: `${y * 100}%`,
}}> }}>
<div <div tabIndex={0} className="player-content" onKeyUp={onKeyUp}>
tabIndex={0} {availableActions && (
className="player-content" <div className="player-actions">{availableActions()}</div>
onKeyUp={onKeyUp} )}
>
{
availableActions && (
<div className="player-actions">
{availableActions()}
</div>
)
}
<PlayerPiece <PlayerPiece
team={playerInfo.team} team={playerInfo.team}
text={playerInfo.role} text={playerInfo.role}

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

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