Replicate the parent's content to the visualisation of a step. #117

Merged
maxime.batista merged 4 commits from replicated-parent into master 1 year ago

@ -36,6 +36,7 @@ export interface BendableArrowProps {
onSegmentsChanges: (edges: Segment[]) => void onSegmentsChanges: (edges: Segment[]) => void
forceStraight: boolean forceStraight: boolean
wavy: boolean wavy: boolean
readOnly: boolean
startRadius?: number startRadius?: number
endRadius?: number endRadius?: number
@ -87,6 +88,7 @@ function constraintInCircle(center: Pos, reference: Pos, radius: number): Pos {
* @param segments * @param segments
* @param onSegmentsChanges * @param onSegmentsChanges
* @param wavy * @param wavy
* @param readOnly
* @param forceStraight * @param forceStraight
* @param style * @param style
* @param startRadius * @param startRadius
@ -103,6 +105,7 @@ export default function BendableArrow({
forceStraight, forceStraight,
wavy, wavy,
readOnly,
style, style,
startRadius = 0, startRadius = 0,
@ -531,7 +534,7 @@ export default function BendableArrow({
} }
fill="none" fill="none"
tabIndex={0} tabIndex={0}
onDoubleClick={addSegment} onDoubleClick={readOnly ? undefined : addSegment}
onKeyUp={(e) => { onKeyUp={(e) => {
if (onDeleteRequested && e.key == "Delete") if (onDeleteRequested && e.key == "Delete")
onDeleteRequested() onDeleteRequested()
@ -555,6 +558,7 @@ export default function BendableArrow({
{!forceStraight && {!forceStraight &&
isSelected && isSelected &&
!readOnly &&
computePoints(area.current!.getBoundingClientRect())} computePoints(area.current!.getBoundingClientRect())}
</div> </div>
) )

@ -6,10 +6,11 @@ import { ComponentId, TacticComponent } from "../../model/tactic/Tactic"
export interface BasketCourtProps { export interface BasketCourtProps {
components: TacticComponent[] components: TacticComponent[]
parentComponents: TacticComponent[] | null
previewAction: ActionPreview | null previewAction: ActionPreview | null
renderComponent: (comp: TacticComponent) => ReactNode renderComponent: (comp: TacticComponent, isFromParent: boolean) => ReactNode
renderActions: (comp: TacticComponent) => ReactNode[] renderActions: (comp: TacticComponent, isFromParent: boolean) => ReactNode[]
courtImage: ReactElement courtImage: ReactElement
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
@ -22,6 +23,7 @@ export interface ActionPreview extends Action {
export function BasketCourt({ export function BasketCourt({
components, components,
parentComponents,
previewAction, previewAction,
renderComponent, renderComponent,
@ -37,15 +39,23 @@ export function BasketCourt({
style={{ position: "relative" }}> style={{ position: "relative" }}>
{courtImage} {courtImage}
{courtRef.current && components.map(renderComponent)} {courtRef.current &&
{courtRef.current && components.flatMap(renderActions)} parentComponents?.map((i) => renderComponent(i, true))}
{courtRef.current &&
parentComponents?.flatMap((i) => renderActions(i, true))}
{courtRef.current &&
maxime.batista marked this conversation as resolved
Review
-                parentComponents &&
-                parentComponents.map((i) => renderComponent(i, true))}
+                parentComponents?.map((i) => renderComponent(i, true))}
             {courtRef.current &&
-                parentComponents &&
-                parentComponents.flatMap((i) => renderActions(i, true))}
+                parentComponents?.flatMap((i) => renderActions(i, true))}
```diff - parentComponents && - parentComponents.map((i) => renderComponent(i, true))} + parentComponents?.map((i) => renderComponent(i, true))} {courtRef.current && - parentComponents && - parentComponents.flatMap((i) => renderActions(i, true))} + parentComponents?.flatMap((i) => renderActions(i, true))} ```
components.map((i) => renderComponent(i, false))}
{courtRef.current &&
components.flatMap((i) => renderActions(i, false))}
{previewAction && ( {previewAction && (
<CourtAction <CourtAction
courtRef={courtRef} courtRef={courtRef}
action={previewAction} action={previewAction}
origin={previewAction.origin} origin={previewAction.origin}
isInvalid={previewAction.isInvalid} color={previewAction.isInvalid ? "red" : "black"}
isEditable={true}
//do nothing on interacted, not really possible as it's a preview arrow //do nothing on interacted, not really possible as it's a preview arrow
onActionDeleted={() => {}} onActionDeleted={() => {}}
onActionChanges={() => {}} onActionChanges={() => {}}

@ -7,22 +7,22 @@ import { ComponentId } from "../../model/tactic/Tactic"
export interface CourtActionProps { export interface CourtActionProps {
origin: ComponentId origin: ComponentId
action: Action action: Action
onActionChanges: (a: Action) => void color: string
onActionDeleted: () => void
courtRef: RefObject<HTMLElement> courtRef: RefObject<HTMLElement>
isInvalid: boolean isEditable: boolean
onActionChanges?: (a: Action) => void
onActionDeleted?: () => void
} }
export function CourtAction({ export function CourtAction({
origin, origin,
action, action,
color,
onActionChanges, onActionChanges,
onActionDeleted, onActionDeleted,
courtRef, courtRef,
isInvalid, isEditable,
}: CourtActionProps) { }: CourtActionProps) {
const color = isInvalid ? "red" : "black"
let head let head
switch (action.type) { switch (action.type) {
case ActionKind.DRIBBLE: case ActionKind.DRIBBLE:
@ -49,9 +49,11 @@ export function CourtAction({
startPos={origin} startPos={origin}
segments={action.segments} segments={action.segments}
onSegmentsChanges={(edges) => { onSegmentsChanges={(edges) => {
if (onActionChanges)
onActionChanges({ ...action, segments: edges }) onActionChanges({ ...action, segments: edges })
}} }}
wavy={action.type == ActionKind.DRIBBLE} wavy={action.type == ActionKind.DRIBBLE}
readOnly={!isEditable}
//TODO place those magic values in constants //TODO place those magic values in constants
endRadius={action.target ? 26 : 17} endRadius={action.target ? 26 : 17}
startRadius={10} startRadius={10}

@ -10,7 +10,9 @@ export interface PlayerPieceProps {
export function PlayerPiece({ team, text, hasBall }: PlayerPieceProps) { export function PlayerPiece({ team, text, hasBall }: PlayerPieceProps) {
let className = `player-piece ${team}` let className = `player-piece ${team}`
if (hasBall) { if (hasBall) {
className += ` player-piece-has-ball` className += " player-piece-has-ball"
} else {
className += " player-piece-has-no-ball"
} }
return ( return (

@ -70,6 +70,7 @@ function StepsTreeNode({
onSegmentsChanges={() => {}} onSegmentsChanges={() => {}}
forceStraight={true} forceStraight={true}
wavy={false} wavy={false}
readOnly={true}
//TODO remove magic constants //TODO remove magic constants
startRadius={10} startRadius={10}
endRadius={10} endRadius={10}

@ -279,9 +279,12 @@ export function removePlayer(
if (action.type !== ActionKind.SHOOT) { if (action.type !== ActionKind.SHOOT) {
continue continue
} }
const actionTarget = content.components.find( if (typeof action.target !== "string") continue
(c) => c.id === action.target, const actionTarget = tryGetComponent<PlayerLike>(
)! as PlayerLike action.target,
content.components,
)
if (actionTarget === undefined) continue //the target was maybe removed
return ( return (
spreadNewStateFromOriginStateChange( spreadNewStateFromOriginStateChange(
actionTarget, actionTarget,

@ -98,9 +98,9 @@ export function getAvailableId(root: StepInfoNode): number {
export function getParent( export function getParent(
root: StepInfoNode, root: StepInfoNode,
node: StepInfoNode, node: number,
): StepInfoNode | null { ): StepInfoNode | null {
if (root.children.find((n) => n.id === node.id)) return root if (root.children.find((n) => n.id === node)) return root
for (const child of root.children) { for (const child of root.children) {
const result = getParent(child, node) const result = getParent(child, node)

@ -26,9 +26,10 @@ import {
getComponent, getComponent,
getOrigin, getOrigin,
getPrecomputedPosition, getPrecomputedPosition,
removePlayer,
tryGetComponent, tryGetComponent,
} from "./PlayerDomains" } from "./PlayerDomains"
import { ActionKind } from "../model/tactic/Action.ts" import { Action, ActionKind } from "../model/tactic/Action.ts"
import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts" import { spreadNewStateFromOriginStateChange } from "./ActionsDomains.ts"
export function placePlayerAt( export function placePlayerAt(
@ -458,17 +459,73 @@ export function drainTerminalStateOnChildContent(
const initialChildCompsCount = childContent.components.length const initialChildCompsCount = childContent.components.length
//filter out all frozen components that are not present on the parent's terminal state anymore for (const component of childContent.components) {
if (
component.type !== "phantom" &&
component.frozen &&
!tryGetComponent(component.id, parentTerminalState.components)
) {
if (component.type === "player")
childContent = removePlayer(component, childContent)
else
childContent = { childContent = {
...childContent,
components: childContent.components.filter( components: childContent.components.filter(
(comp) => (c) => c.id !== component.id,
comp.type === "phantom" ||
!comp.frozen ||
tryGetComponent(comp.id, parentTerminalState.components),
), ),
} }
}
}
gotUpdated ||= childContent.components.length !== initialChildCompsCount gotUpdated ||= childContent.components.length !== initialChildCompsCount
return gotUpdated ? childContent : null return gotUpdated ? childContent : null
} }
export function mapToParentContent(content: StepContent): StepContent {
function mapToParentActions(actions: Action[]): Action[] {
return actions.map((a) => ({
...a,
target: a.target + "-parent",
segments: a.segments.map((s) => ({
...s,
next: typeof s.next === "string" ? s.next + "-parent" : s.next,
})),
}))
}
return {
...content,
components: content.components.map((p) => {
if (p.type == "ball") return p
if (p.type == "player") {
return {
...p,
id: p.id + "-parent",
actions: mapToParentActions(p.actions),
path: p.path && {
items: p.path.items.map((p) => p + "-parent"),
},
}
}
return {
...p,
pos:
p.pos.type == "follows"
? { ...p.pos, attach: p.pos.attach + "-parent" }
: p.pos,
id: p.id + "-parent",
originPlayerId: p.originPlayerId + "-parent",
actions: mapToParentActions(p.actions),
}
}),
}
}
export function selectContent(
id: string,
content: StepContent,
parentContent: StepContent | null,
): StepContent {
return parentContent && id.endsWith("-parent") ? parentContent : content
}

@ -42,11 +42,13 @@ import {
dropBallOnComponent, dropBallOnComponent,
getComponentCollided, getComponentCollided,
getRackPlayers, getRackPlayers,
mapToParentContent,
moveComponent, moveComponent,
placeBallAt, placeBallAt,
placeObjectAt, placeObjectAt,
placePlayerAt, placePlayerAt,
removeBall, removeBall,
selectContent,
updateComponent, updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
@ -202,6 +204,8 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
[stepsVersions, service, stepId, stepsTree], [stepsVersions, service, stepId, stepsTree],
) )
const [parentContent, setParentContent] = useState<StepContent | null>(null)
const [stepContent, setStepContent, saveState] = const [stepContent, setStepContent, saveState] =
useContentState<StepContent>( useContentState<StepContent>(
{ components: [] }, { components: [] },
@ -271,21 +275,30 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
if (isNotInit) init() if (isNotInit) init()
}, [isNotInit, service, setStepContent, stepsVersions]) }, [isNotInit, service, setStepContent, stepsVersions])
const editorService: EditorService = useMemo( const editorService: EditorService = useMemo(() => {
() => ({ let internalStepsTree = stepsTree
return {
async addStep( async addStep(
parent: StepInfoNode, parent: StepInfoNode,
content: StepContent, content: StepContent,
): Promise<StepInfoNode | ServiceError> { ): Promise<StepInfoNode | ServiceError> {
const result = await service.addStep(parent, content) const result = await service.addStep(parent, content)
if (typeof result !== "string") if (typeof result !== "string") {
setStepsTree(addStepNode(stepsTree!, parent, result)) internalStepsTree = addStepNode(
internalStepsTree!,
parent,
result,
)
setStepsTree(internalStepsTree)
}
return result return result
}, },
async removeStep(step: number): Promise<void | ServiceError> { async removeStep(step: number): Promise<void | ServiceError> {
const result = await service.removeStep(step) const result = await service.removeStep(step)
if (typeof result !== "string") if (typeof result !== "string") {
setStepsTree(removeStepNode(stepsTree!, step)) internalStepsTree = removeStepNode(internalStepsTree!, step)
setStepsTree(internalStepsTree)
}
stepsVersions.delete(step) stepsVersions.delete(step)
return result return result
}, },
@ -304,12 +317,19 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
async selectStep(step: number): Promise<void | ServiceError> { async selectStep(step: number): Promise<void | ServiceError> {
const result = await service.getContent(step) const result = await service.getContent(step)
if (typeof result === "string") return result if (typeof result === "string") return result
const stepParent = getParent(internalStepsTree!, step)?.id
if (stepParent) {
const parentResult = await service.getContent(stepParent)
if (typeof parentResult === "string") return parentResult
setParentContent(mapToParentContent(parentResult))
} else {
setParentContent(null)
}
setStepId(step) setStepId(step)
setStepContent(result, false) setStepContent(result, false)
}, },
}), }
[stepsVersions, service, setStepContent, stepsTree], }, [stepsVersions, service, setStepContent, stepsTree])
)
if (panicMessage) { if (panicMessage) {
return <p>{panicMessage}</p> return <p>{panicMessage}</p>
@ -326,6 +346,7 @@ function EditorPageWrapper({ service }: { service: TacticService }) {
stepId={stepId} stepId={stepId}
stepsTree={stepsTree} stepsTree={stepsTree}
contentSaveState={saveState} contentSaveState={saveState}
parentContent={parentContent}
content={stepContent} content={stepContent}
service={editorService} service={editorService}
courtRef={courtRef} courtRef={courtRef}
@ -337,10 +358,12 @@ export interface EditorViewProps {
stepsTree: StepInfoNode stepsTree: StepInfoNode
name: string name: string
courtType: CourtType courtType: CourtType
content: StepContent
contentSaveState: SaveState contentSaveState: SaveState
stepId: number stepId: number
parentContent: StepContent | null
content: StepContent
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
service: EditorService service: EditorService
@ -349,6 +372,7 @@ export interface EditorViewProps {
function EditorPage({ function EditorPage({
name, name,
courtType, courtType,
parentContent,
content, content,
stepId, stepId,
contentSaveState, contentSaveState,
@ -465,7 +489,10 @@ function EditorPage({
) == -1 ) == -1
isFrozen = player.frozen isFrozen = player.frozen
} else { } else {
const origin = getOrigin(player, content.components) const origin = getOrigin(
player,
selectContent(player.id, content, parentContent).components,
)
const path = origin.path! const path = origin.path!
// phantoms can only place other arrows if they are the head of the path // phantoms can only place other arrows if they are the head of the path
canPlaceArrows = canPlaceArrows =
@ -516,18 +543,23 @@ function EditorPage({
) )
const renderPlayer = useCallback( const renderPlayer = useCallback(
(component: PlayerLike) => { (component: PlayerLike, isFromParent: boolean) => {
let info: PlayerInfo let info: PlayerInfo
const isPhantom = component.type == "phantom" const isPhantom = component.type == "phantom"
let forceFreeze = isFromParent
const usedContent = isFromParent ? parentContent! : content
if (isPhantom) { if (isPhantom) {
const origin = getOrigin(component, content.components) const origin = getOrigin(component, usedContent.components)
info = { info = {
id: component.id, id: component.id,
team: origin.team, team: origin.team,
role: origin.role, role: origin.role,
pos: computePhantomPositioning( pos: computePhantomPositioning(
component, component,
content, usedContent,
relativePositions, relativePositions,
courtBounds(), courtBounds(),
), ),
@ -536,24 +568,31 @@ function EditorPage({
} else { } else {
info = component info = component
if (component.frozen) { forceFreeze ||= component.frozen
}
const className =
(isPhantom ? "phantom" : "player") +
" " +
(isFromParent ? "from-parent" : "")
if (forceFreeze) {
return ( return (
<CourtPlayer <CourtPlayer
key={component.id} key={component.id}
playerInfo={info} playerInfo={info}
className={"player"} className={className}
availableActions={() => availableActions={() =>
renderAvailablePlayerActions(info, component) renderAvailablePlayerActions(info, component)
} }
/> />
) )
} }
}
return ( return (
<EditableCourtPlayer <EditableCourtPlayer
key={component.id} key={component.id}
className={isPhantom ? "phantom" : "player"} className={className}
playerInfo={info} playerInfo={info}
onPositionValidated={(newPos) => onPositionValidated={(newPos) =>
validatePlayerPosition(component, info, newPos) validatePlayerPosition(component, info, newPos)
@ -604,11 +643,11 @@ function EditorPage({
) )
const renderComponent = useCallback( const renderComponent = useCallback(
(component: TacticComponent) => { (component: TacticComponent, isFromParent: boolean) => {
if (component.type === "player" || component.type === "phantom") { if (component.type === "player" || component.type === "phantom") {
return renderPlayer(component) return renderPlayer(component, isFromParent)
} }
if (component.type === BALL_TYPE) { if (component.type === BALL_TYPE && !isFromParent) {
return ( return (
<CourtBall <CourtBall
key="ball" key="ball"
@ -630,7 +669,7 @@ function EditorPage({
) )
const renderActions = useCallback( const renderActions = useCallback(
(component: TacticComponent) => (component: TacticComponent, isFromParent: boolean) =>
component.actions.map((action, i) => { component.actions.map((action, i) => {
return ( return (
<CourtAction <CourtAction
@ -638,13 +677,16 @@ function EditorPage({
action={action} action={action}
origin={component.id} origin={component.id}
courtRef={courtRef} courtRef={courtRef}
isInvalid={false} color={isFromParent ? "gray" : "black"}
isEditable={!isFromParent}
onActionDeleted={() => { onActionDeleted={() => {
if (!isFromParent)
doDeleteAction(action, i, component) doDeleteAction(action, i, component)
}} }}
onActionChanges={(action) => onActionChanges={(action) => {
if (!isFromParent)
doUpdateAction(component, action, i) doUpdateAction(component, action, i)
} }}
/> />
) )
}), }),
@ -698,6 +740,7 @@ function EditorPage({
<div id="court-div"> <div id="court-div">
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
parentComponents={parentContent?.components ?? null}
components={content.components} components={content.components}
courtImage={<Court courtType={courtType} />} courtImage={<Court courtType={courtType} />}
courtRef={courtRef} courtRef={courtRef}
@ -731,7 +774,9 @@ function EditorPage({
onRemoveNode={useCallback( onRemoveNode={useCallback(
async (removed) => { async (removed) => {
await service.removeStep(removed.id) await service.removeStep(removed.id)
await service.selectStep(getParent(stepsTree, removed)!.id) await service.selectStep(
getParent(stepsTree, removed.id)!.id,
)
}, },
[service, stepsTree], [service, stepsTree],
)} )}

@ -6,6 +6,11 @@
opacity: 50%; opacity: 50%;
} }
.from-parent .player-piece {
color: white;
background-color: var(--player-from-parent-color);
}
.player-content { .player-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -40,8 +45,13 @@
border-color: var(--player-piece-ball-border-color); border-color: var(--player-piece-ball-border-color);
} }
.player-piece-has-no-ball {
padding: 2px;
}
.player-actions { .player-actions {
display: flex; display: flex;
pointer-events: none;
position: absolute; position: absolute;
flex-direction: row; flex-direction: row;

@ -15,6 +15,7 @@
#img-account { #img-account {
cursor: pointer; cursor: pointer;
margin-right: 5px;
} }
#header-left, #header-left,
@ -28,6 +29,8 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: end; align-items: end;
color: white;
margin-right: 5px;
} }
#clickable-header-right:hover #username { #clickable-header-right:hover #username {

@ -9,6 +9,7 @@
--selected-team-secondarycolor: #000000; --selected-team-secondarycolor: #000000;
--player-allies-color: #64e4f5; --player-allies-color: #64e4f5;
--player-opponents-color: #f59264; --player-opponents-color: #f59264;
--player-from-parent-color: #494949;
--buttons-shadow-color: #a8a8a8; --buttons-shadow-color: #a8a8a8;

Loading…
Cancel
Save