diff --git a/src/components/arrows/BendableArrow.tsx b/src/components/arrows/BendableArrow.tsx index 4b3615f..b730bbb 100644 --- a/src/components/arrows/BendableArrow.tsx +++ b/src/components/arrows/BendableArrow.tsx @@ -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())} ) diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index 68021f3..e3cddd2 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -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 @@ -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 && + components.map((i) => renderComponent(i, false))} + {courtRef.current && + components.flatMap((i) => renderActions(i, false))} {previewAction && ( {}} onActionChanges={() => {}} diff --git a/src/components/editor/CourtAction.tsx b/src/components/editor/CourtAction.tsx index 84f7fd5..54a6be2 100644 --- a/src/components/editor/CourtAction.tsx +++ b/src/components/editor/CourtAction.tsx @@ -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 - 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} diff --git a/src/components/editor/PlayerPiece.tsx b/src/components/editor/PlayerPiece.tsx index 1b52ff8..1a7067b 100644 --- a/src/components/editor/PlayerPiece.tsx +++ b/src/components/editor/PlayerPiece.tsx @@ -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 ( diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index 952b268..dde6534 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -70,6 +70,7 @@ function StepsTreeNode({ onSegmentsChanges={() => {}} forceStraight={true} wavy={false} + readOnly={true} //TODO remove magic constants startRadius={10} endRadius={10} diff --git a/src/editor/PlayerDomains.ts b/src/editor/PlayerDomains.ts index 1b3c918..2a7e28d 100644 --- a/src/editor/PlayerDomains.ts +++ b/src/editor/PlayerDomains.ts @@ -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( + action.target, + content.components, + ) + if (actionTarget === undefined) continue //the target was maybe removed return ( spreadNewStateFromOriginStateChange( actionTarget, diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts index e788a9c..a2b739a 100644 --- a/src/editor/StepsDomain.ts +++ b/src/editor/StepsDomain.ts @@ -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) diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 319e4a0..c7b2965 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -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 +} diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 03e8121..a9ece7d 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -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(null) + const [stepContent, setStepContent, saveState] = useContentState( { 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 { 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 { 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 { 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

{panicMessage}

@@ -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 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 ( - - renderAvailablePlayerActions(info, component) - } - /> - ) - } + forceFreeze ||= component.frozen + } + + const className = + (isPhantom ? "phantom" : "player") + + " " + + (isFromParent ? "from-parent" : "") + + if (forceFreeze) { + return ( + + renderAvailablePlayerActions(info, component) + } + /> + ) } return ( 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 ( + (component: TacticComponent, isFromParent: boolean) => component.actions.map((action, i) => { return ( { - 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({
} 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], )} diff --git a/src/style/player.css b/src/style/player.css index b03123b..8817474 100644 --- a/src/style/player.css +++ b/src/style/player.css @@ -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; diff --git a/src/style/template/header.css b/src/style/template/header.css index 07db237..dba7305 100644 --- a/src/style/template/header.css +++ b/src/style/template/header.css @@ -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 { diff --git a/src/style/theme/default.css b/src/style/theme/default.css index 19702b0..05ed963 100644 --- a/src/style/theme/default.css +++ b/src/style/theme/default.css @@ -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;