diff --git a/front/components/editor/BallPiece.tsx b/front/components/editor/BallPiece.tsx index baaba70..b09f811 100644 --- a/front/components/editor/BallPiece.tsx +++ b/front/components/editor/BallPiece.tsx @@ -1,21 +1,8 @@ -import React, { RefObject } from "react" - import "../../style/ball.css" -import Ball from "../../assets/icon/ball.svg?react" -import Draggable from "react-draggable" - -export interface BallPieceProps { - onDrop: () => void - pieceRef: RefObject -} +import BallSvg from "../../assets/icon/ball.svg?react" +import { Ball } from "../../tactic/CourtObjects" -export function BallPiece({ onDrop, pieceRef }: BallPieceProps) { - return ( - -
- -
-
- ) +export function BallPiece() { + return } diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index c158d89..4545f9a 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,21 +1,32 @@ import "../../style/basket_court.css" -import { RefObject, useRef } from "react" +import { RefObject } from "react" import CourtPlayer from "./CourtPlayer" + import { Player } from "../../tactic/Player" +import { CourtObject } from "../../tactic/CourtObjects" +import { CourtBall } from "./CourtBall" export interface BasketCourtProps { players: Player[] + objects: CourtObject[] + onPlayerRemove: (p: Player) => void - onBallDrop: (ref: HTMLDivElement) => void onPlayerChange: (p: Player) => void + + onBallRemove: () => void + + onBallMoved: (ball: DOMRect) => void + courtImage: string courtRef: RefObject } export function BasketCourt({ players, + objects, onPlayerRemove, - onBallDrop, + onBallRemove, + onBallMoved, onPlayerChange, courtImage, courtRef, @@ -33,11 +44,25 @@ export function BasketCourt({ player={player} onChange={onPlayerChange} onRemove={() => onPlayerRemove(player)} - onBallDrop={onBallDrop} + onBallDrop={onBallMoved} parentRef={courtRef} /> ) })} + + {objects.map((object) => { + if (object.type == "ball") { + return ( + + ) + } + throw new Error("unknown court object", object.type) + })} ) } diff --git a/front/components/editor/CourtBall.tsx b/front/components/editor/CourtBall.tsx new file mode 100644 index 0000000..9ba5ae5 --- /dev/null +++ b/front/components/editor/CourtBall.tsx @@ -0,0 +1,38 @@ +import React, { useRef } from "react" +import Draggable from "react-draggable" +import { BallPiece } from "./BallPiece" +import { Ball } from "../../tactic/CourtObjects" + +export interface CourtBallProps { + onMoved: (rect: DOMRect) => void + onRemove: () => void + ball: Ball +} + +export function CourtBall({ onMoved, ball, onRemove }: CourtBallProps) { + const pieceRef = useRef(null) + + const x = ball.rightRatio + const y = ball.bottomRatio + + return ( + onMoved(pieceRef.current!.getBoundingClientRect())} + nodeRef={pieceRef}> +
{ + if (e.key == "Delete") onRemove() + }} + style={{ + position: "absolute", + left: `${x * 100}%`, + top: `${y * 100}%`, + }}> + +
+
+ ) +} diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 6aebdcb..5f152ed 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,6 +1,5 @@ -import { RefObject, useRef, useState } from "react" +import { RefObject, useRef } from "react" import "../../style/player.css" -import RemoveIcon from "../../assets/icon/remove.svg?react" import { BallPiece } from "./BallPiece" import Draggable from "react-draggable" import { PlayerPiece } from "./PlayerPiece" @@ -11,7 +10,7 @@ export interface PlayerProps { player: Player onChange: (p: Player) => void onRemove: () => void - onBallDrop: (ref: HTMLDivElement) => void + onBallDrop: (bounds: DOMRect) => void parentRef: RefObject } @@ -36,7 +35,6 @@ export default function CourtPlayer({ { const pieceBounds = pieceRef.current!.getBoundingClientRect() @@ -69,15 +67,19 @@ export default function CourtPlayer({ if (e.key == "Delete") onRemove() }}>
- {hasBall && ( - onBallDrop(ballPiece.current!)} - pieceRef={ballPiece} - /> + + onBallDrop( + ballPiece.current!.getBoundingClientRect(), + ) + } + position={{ x: 0, y: 0 }}> +
+ +
+
)}
p.hasBall) == undefined, + const [objects, setObjects] = useState( + isBallOnCourt(content) ? [] : [{ key: "ball" }], ) - const ballPiece = useRef(null) - const courtDivContentRef = useRef(null) - const canDetach = (ref: HTMLDivElement) => { - const refBounds = ref.getBoundingClientRect() + const isBoundsOnCourt = (bounds: DOMRect) => { const courtBounds = courtDivContentRef.current!.getBoundingClientRect() // check if refBounds overlaps courtBounds return !( - refBounds.top > courtBounds.bottom || - refBounds.right < courtBounds.left || - refBounds.bottom < courtBounds.top || - refBounds.left > courtBounds.right + bounds.top > courtBounds.bottom || + bounds.right < courtBounds.left || + bounds.bottom < courtBounds.top || + bounds.left > courtBounds.right ) } @@ -151,6 +154,7 @@ function EditorView({ setContent((content) => { return { + ...content, players: [ ...content.players, { @@ -166,39 +170,185 @@ function EditorView({ }) } - const onBallDrop = (ref: HTMLDivElement) => { - const ballBounds = ref.getBoundingClientRect() - let ballAssigned = false + const onObjectDetach = ( + ref: HTMLDivElement, + rackedObject: RackedCourtObject, + ) => { + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - setContent((content) => { - const players = content.players.map((player) => { - if (ballAssigned) { - return { ...player, hasBall: false } - } - const playerBounds = document - .getElementById(player.id)! - .getBoundingClientRect() - const doesOverlap = !( - ballBounds.top > playerBounds.bottom || - ballBounds.right < playerBounds.left || - ballBounds.bottom < playerBounds.top || - ballBounds.left > playerBounds.right + const { x, y } = calculateRatio(refBounds, courtBounds) + + let courtObject: CourtObject + + switch (rackedObject.key) { + case "ball": + const ballObj = content.objects.findIndex( + (o) => o.type == "ball", + ) + const playerCollidedIdx = getPlayerCollided( + refBounds, + content.players, ) - if (doesOverlap) { - ballAssigned = true + if (playerCollidedIdx != -1) { + onBallDropOnPlayer(playerCollidedIdx) + setContent((content) => { + return { + ...content, + objects: content.objects.toSpliced(ballObj, 1), + } + }) + return + } + + courtObject = { + type: "ball", + rightRatio: x, + bottomRatio: y, + } + break + + default: + throw new Error("unknown court object ", rackedObject.key) + } + + setContent((content) => { + return { + ...content, + objects: [...content.objects, courtObject], + } + }) + } + + const getPlayerCollided = ( + bounds: DOMRect, + players: Player[], + ): number | -1 => { + for (let i = 0; i < players.length; i++) { + const player = players[i] + const playerBounds = document + .getElementById(player.id)! + .getBoundingClientRect() + const doesOverlap = !( + bounds.top > playerBounds.bottom || + bounds.right < playerBounds.left || + bounds.bottom < playerBounds.top || + bounds.left > playerBounds.right + ) + if (doesOverlap) { + return i + } + } + return -1 + } + + const onBallDropOnPlayer = (playerCollidedIdx: number) => { + setContent((content) => { + const ballObj = content.objects.findIndex((o) => o.type == "ball") + let player = content.players.at(playerCollidedIdx) as Player + return { + ...content, + players: content.players.toSpliced(playerCollidedIdx, 1, { + ...player, + hasBall: true, + }), + objects: content.objects.toSpliced(ballObj, 1), + } + }) + } + + const onBallDrop = (refBounds: DOMRect) => { + if (!isBoundsOnCourt(refBounds)) { + removeCourtBall() + return + } + const playerCollidedIdx = getPlayerCollided(refBounds, content.players) + if (playerCollidedIdx != -1) { + setContent((content) => { + return { + ...content, + players: content.players.map((player) => ({ + ...player, + hasBall: false, + })), } - return { ...player, hasBall: doesOverlap } }) - setShowBall(!ballAssigned) - return { players: players } + onBallDropOnPlayer(playerCollidedIdx) + return + } + + if (content.objects.findIndex((o) => o.type == "ball") != -1) { + return + } + + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() + const { x, y } = calculateRatio(refBounds, courtBounds) + let courtObject: CourtObject + + courtObject = { + type: "ball", + rightRatio: x, + bottomRatio: y, + } + + setContent((content) => { + return { + ...content, + players: content.players.map((player) => ({ + ...player, + hasBall: false, + })), + objects: [...content.objects, courtObject], + } }) } + const removePlayer = (player: Player) => { + setContent((content) => ({ + ...content, + players: toSplicedPlayers(content.players, player, false), + objects: [...content.objects], + })) + let setter + switch (player.team) { + case Team.Opponents: + setter = setOpponents + break + case Team.Allies: + setter = setAllies + } + if (player.hasBall) { + setObjects([{ key: "ball" }]) + } + setter((players) => [ + ...players, + { + team: player.team, + pos: player.role, + key: player.role, + }, + ]) + } + + const removeCourtBall = () => { + setContent((content) => { + const ballObj = content.objects.findIndex((o) => o.type == "ball") + return { + ...content, + players: content.players.map((player) => ({ + ...player, + hasBall: false, + })), + objects: content.objects.toSpliced(ballObj, 1), + } + }) + setObjects([{ key: "ball" }]) + } + return (
-
@@ -220,7 +370,9 @@ function EditorView({ id="allies-rack" objects={allies} onChange={setAllies} - canDetach={canDetach} + canDetach={(div) => + isBoundsOnCourt(div.getBoundingClientRect()) + } onElementDetached={onPieceDetach} render={({ team, key }) => ( - {showBall && ( - onBallDrop(ballPiece.current!)} - pieceRef={ballPiece} - /> - )} + + isBoundsOnCourt(div.getBoundingClientRect()) + } + onElementDetached={onObjectDetach} + render={renderCourtObject} + /> + isBoundsOnCourt(div.getBoundingClientRect()) + } onElementDetached={onPieceDetach} render={({ team, key }) => ( { + const playerBounds = document + .getElementById(player.id)! + .getBoundingClientRect() + if (!isBoundsOnCourt(playerBounds)) { + removePlayer(player) + return + } setContent((content) => ({ + ...content, players: toSplicedPlayers( content.players, player, @@ -274,32 +441,10 @@ function EditorView({ })) }} onPlayerRemove={(player) => { - setContent((content) => ({ - players: toSplicedPlayers( - content.players, - player, - false, - ), - })) - let setter - switch (player.team) { - case Team.Opponents: - setter = setOpponents - break - case Team.Allies: - setter = setAllies - } - if (player.hasBall) { - setShowBall(true) - } - setter((players) => [ - ...players, - { - team: player.team, - pos: player.role, - key: player.role, - }, - ]) + removePlayer(player) + }} + onBallRemove={() => { + removeCourtBall() }} />
@@ -309,6 +454,20 @@ function EditorView({ ) } +function isBallOnCourt(content: TacticContent) { + if (content.players.findIndex((p) => p.hasBall) != -1) { + return true + } + return content.objects.findIndex((o) => o.type == "ball") != -1 +} + +function renderCourtObject(courtObject: RackedCourtObject) { + if (courtObject.key == "ball") { + return + } + throw new Error("unknown racked court object ", courtObject.key) +} + function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { return ["1", "2", "3", "4", "5"] .filter( diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index efb20c6..0cbe32b 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -19,7 +19,7 @@ CREATE TABLE Tactic name varchar NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, owner integer NOT NULL, - content varchar DEFAULT '{"players": []}' NOT NULL, + content varchar DEFAULT '{"players": [], "objects": []}' NOT NULL, court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL, FOREIGN KEY (owner) REFERENCES Account ); diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index 3bbbe61..8415cdb 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -42,7 +42,7 @@ class EditorController { return ViewHttpResponse::react("views/Editor.tsx", [ "id" => -1, //-1 id means that the editor will not support saves "name" => TacticModel::TACTIC_DEFAULT_NAME, - "content" => '{"players": []}', + "content" => '{"players": [], "objects": []}', "courtType" => $courtType->name(), ]); } diff --git a/src/Core/Model/AuthModel.php b/src/Core/Model/AuthModel.php index e8710c0..929eb99 100644 --- a/src/Core/Model/AuthModel.php +++ b/src/Core/Model/AuthModel.php @@ -64,7 +64,7 @@ class AuthModel { public function login(string $email, string $password, array &$failures): ?Account { $hash = $this->gateway->getHash($email); if ($hash == null or (!password_verify($password, $hash))) { - $failures[] = new ValidationFail("email","Adresse email ou mot de passe invalide"); + $failures[] = new ValidationFail("email", "Adresse email ou mot de passe invalide"); return null; } return $this->gateway->getAccountFromMail($email);