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

@ -6,10 +6,11 @@ import { ComponentId, TacticComponent } from "../../model/tactic/Tactic"
export interface BasketCourtProps {
components: TacticComponent[]
parentComponents: TacticComponent[] | null
previewAction: ActionPreview | null
renderComponent: (comp: TacticComponent) => ReactNode
renderActions: (comp: TacticComponent) => ReactNode[]
renderComponent: (comp: TacticComponent, isFromParent: boolean) => ReactNode
renderActions: (comp: TacticComponent, isFromParent: boolean) => ReactNode[]
courtImage: ReactElement
courtRef: RefObject<HTMLDivElement>
@ -22,6 +23,7 @@ export interface ActionPreview extends Action {
export function BasketCourt({
components,
parentComponents,
previewAction,
renderComponent,
@ -37,15 +39,23 @@ export function BasketCourt({
style={{ position: "relative" }}>
{courtImage}
{courtRef.current && components.map(renderComponent)}
{courtRef.current && components.flatMap(renderActions)}
{courtRef.current &&
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 && (
<CourtAction
courtRef={courtRef}
action={previewAction}
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
onActionDeleted={() => {}}
onActionChanges={() => {}}

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

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

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

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

@ -98,9 +98,9 @@ export function getAvailableId(root: StepInfoNode): number {
export function getParent(
root: StepInfoNode,
node: StepInfoNode,
node: number,
): 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) {
const result = getParent(child, node)

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

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

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

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

Loading…
Cancel
Save