Can place ball on the court (#77)
continuous-integration/drone/push Build is passing Details

Also added player and ball remove by drag and drop them out of the court

Co-authored-by: vivien.dufour <vivien.dufour@etu.uca.fr>
Co-authored-by: maxime <maximebatista18@gmail.com>
Reviewed-on: #77
pull/82/head
Vivien DUFOUR 1 year ago
parent 7042b48ffe
commit eb744d0da4

@ -1,21 +1,8 @@
import React, { RefObject } from "react"
import "../../style/ball.css" import "../../style/ball.css"
import Ball from "../../assets/icon/ball.svg?react" import BallSvg from "../../assets/icon/ball.svg?react"
import Draggable from "react-draggable" import { Ball } from "../../tactic/CourtObjects"
export interface BallPieceProps {
onDrop: () => void
pieceRef: RefObject<HTMLDivElement>
}
export function BallPiece({ onDrop, pieceRef }: BallPieceProps) { export function BallPiece() {
return ( return <BallSvg className={"ball"} />
<Draggable onStop={onDrop} nodeRef={pieceRef} position={{ x: 0, y: 0 }}>
<div className={`ball-div`} ref={pieceRef}>
<Ball className={"ball"} />
</div>
</Draggable>
)
} }

@ -1,21 +1,32 @@
import "../../style/basket_court.css" import "../../style/basket_court.css"
import { RefObject, useRef } from "react" import { RefObject } from "react"
import CourtPlayer from "./CourtPlayer" import CourtPlayer from "./CourtPlayer"
import { Player } from "../../tactic/Player" import { Player } from "../../tactic/Player"
import { CourtObject } from "../../tactic/CourtObjects"
import { CourtBall } from "./CourtBall"
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
objects: CourtObject[]
onPlayerRemove: (p: Player) => void onPlayerRemove: (p: Player) => void
onBallDrop: (ref: HTMLDivElement) => void
onPlayerChange: (p: Player) => void onPlayerChange: (p: Player) => void
onBallRemove: () => void
onBallMoved: (ball: DOMRect) => void
courtImage: string courtImage: string
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
} }
export function BasketCourt({ export function BasketCourt({
players, players,
objects,
onPlayerRemove, onPlayerRemove,
onBallDrop, onBallRemove,
onBallMoved,
onPlayerChange, onPlayerChange,
courtImage, courtImage,
courtRef, courtRef,
@ -33,11 +44,25 @@ export function BasketCourt({
player={player} player={player}
onChange={onPlayerChange} onChange={onPlayerChange}
onRemove={() => onPlayerRemove(player)} onRemove={() => onPlayerRemove(player)}
onBallDrop={onBallDrop} onBallDrop={onBallMoved}
parentRef={courtRef} parentRef={courtRef}
/> />
) )
})} })}
{objects.map((object) => {
if (object.type == "ball") {
return (
<CourtBall
onMoved={onBallMoved}
ball={object}
onRemove={onBallRemove}
key="ball"
/>
)
}
throw new Error("unknown court object", object.type)
})}
</div> </div>
) )
} }

@ -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<HTMLDivElement>(null)
const x = ball.rightRatio
const y = ball.bottomRatio
return (
<Draggable
onStop={() => onMoved(pieceRef.current!.getBoundingClientRect())}
nodeRef={pieceRef}>
<div
className={"ball-div"}
ref={pieceRef}
tabIndex={0}
onKeyUp={(e) => {
if (e.key == "Delete") onRemove()
}}
style={{
position: "absolute",
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<BallPiece />
</div>
</Draggable>
)
}

@ -1,6 +1,5 @@
import { RefObject, useRef, useState } from "react" import { RefObject, useRef } from "react"
import "../../style/player.css" import "../../style/player.css"
import RemoveIcon from "../../assets/icon/remove.svg?react"
import { BallPiece } from "./BallPiece" import { BallPiece } from "./BallPiece"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
@ -11,7 +10,7 @@ export interface PlayerProps {
player: Player player: Player
onChange: (p: Player) => void onChange: (p: Player) => void
onRemove: () => void onRemove: () => void
onBallDrop: (ref: HTMLDivElement) => void onBallDrop: (bounds: DOMRect) => void
parentRef: RefObject<HTMLDivElement> parentRef: RefObject<HTMLDivElement>
} }
@ -36,7 +35,6 @@ export default function CourtPlayer({
<Draggable <Draggable
handle={".player-piece"} handle={".player-piece"}
nodeRef={pieceRef} nodeRef={pieceRef}
bounds="parent"
position={{ x, y }} position={{ x, y }}
onStop={() => { onStop={() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect() const pieceBounds = pieceRef.current!.getBoundingClientRect()
@ -69,15 +67,19 @@ export default function CourtPlayer({
if (e.key == "Delete") onRemove() if (e.key == "Delete") onRemove()
}}> }}>
<div className="player-selection-tab"> <div className="player-selection-tab">
<RemoveIcon
className="player-selection-tab-remove"
onClick={onRemove}
/>
{hasBall && ( {hasBall && (
<BallPiece <Draggable
onDrop={() => onBallDrop(ballPiece.current!)} nodeRef={ballPiece}
pieceRef={ballPiece} onStop={() =>
/> onBallDrop(
ballPiece.current!.getBoundingClientRect(),
)
}
position={{ x: 0, y: 0 }}>
<div ref={ballPiece}>
<BallPiece />
</div>
</Draggable>
)} )}
</div> </div>
<PlayerPiece <PlayerPiece

@ -32,6 +32,8 @@
#racks { #racks {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
height: 25px;
} }
.title-input { .title-input {
@ -43,8 +45,28 @@
height: 100%; height: 100%;
} }
#allies-rack .player-piece, #allies-rack,
#opponent-rack {
width: 125px;
min-width: 125px;
display: flex;
flex-direction: row;
align-items: flex-end;
}
#allies-rack {
justify-content: flex-start;
}
#opponent-rack {
justify-content: flex-end;
}
#opponent-rack .player-piece { #opponent-rack .player-piece {
margin-right: 5px;
}
#allies-rack .player-piece {
margin-left: 5px; margin-left: 5px;
} }

@ -1,11 +1,17 @@
export type CourtObject = { type: "ball" } & Ball
export interface Ball { export interface Ball {
/** /**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) * The ball is a "ball" court object
*/ */
bottom_percentage: number readonly type: "ball"
/**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/
readonly bottomRatio: number
/** /**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
right_percentage: number readonly rightRatio: number
} }

@ -1,26 +1,26 @@
import { Team } from "./Team" import { Team } from "./Team"
export interface Player { export interface Player {
id: string readonly id: string
/** /**
* the player's team * the player's team
* */ * */
team: Team readonly team: Team
/** /**
* player's role * player's role
* */ * */
role: string readonly role: string
/** /**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/ */
bottomRatio: number readonly bottomRatio: number
/** /**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
rightRatio: number readonly rightRatio: number
hasBall: boolean readonly hasBall: boolean
} }

@ -1,4 +1,5 @@
import { Player } from "./Player" import { Player } from "./Player"
import { CourtObject } from "./CourtObjects"
export interface Tactic { export interface Tactic {
id: number id: number
@ -8,4 +9,5 @@ export interface Tactic {
export interface TacticContent { export interface TacticContent {
players: Player[] players: Player[]
objects: CourtObject[]
} }

@ -13,20 +13,24 @@ import { BasketCourt } from "../components/editor/BasketCourt"
import plainCourt from "../assets/court/full_court.svg" import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg" import halfCourt from "../assets/court/half_court.svg"
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 { BallPiece } from "../components/editor/BallPiece"
import { Player } from "../tactic/Player" import { Player } from "../tactic/Player"
import { Tactic, TacticContent } from "../tactic/Tactic" import { Tactic, TacticContent } from "../tactic/Tactic"
import { fetchAPI } from "../Fetcher" import { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team" import { Team } from "../tactic/Team"
import { calculateRatio } from "../Utils" import { calculateRatio } from "../Utils"
import SavingState, { import SavingState, {
SaveState, SaveState,
SaveStates, SaveStates,
} from "../components/editor/SavingState" } from "../components/editor/SavingState"
import { CourtObject } from "../tactic/CourtObjects"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
} }
@ -56,6 +60,8 @@ interface RackedPlayer {
key: string key: string
} }
type RackedCourtObject = { key: "ball" }
export default function Editor({ id, name, courtType, content }: EditorProps) { export default function Editor({ id, name, courtType, content }: EditorProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
@ -122,24 +128,21 @@ function EditorView({
getRackPlayers(Team.Opponents, content.players), getRackPlayers(Team.Opponents, content.players),
) )
const [showBall, setShowBall] = useState( const [objects, setObjects] = useState<RackedCourtObject[]>(
content.players.find((p) => p.hasBall) == undefined, isBallOnCourt(content) ? [] : [{ key: "ball" }],
) )
const ballPiece = useRef<HTMLDivElement>(null)
const courtDivContentRef = useRef<HTMLDivElement>(null) const courtDivContentRef = useRef<HTMLDivElement>(null)
const canDetach = (ref: HTMLDivElement) => { const isBoundsOnCourt = (bounds: DOMRect) => {
const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
// check if refBounds overlaps courtBounds // check if refBounds overlaps courtBounds
return !( return !(
refBounds.top > courtBounds.bottom || bounds.top > courtBounds.bottom ||
refBounds.right < courtBounds.left || bounds.right < courtBounds.left ||
refBounds.bottom < courtBounds.top || bounds.bottom < courtBounds.top ||
refBounds.left > courtBounds.right bounds.left > courtBounds.right
) )
} }
@ -151,6 +154,7 @@ function EditorView({
setContent((content) => { setContent((content) => {
return { return {
...content,
players: [ players: [
...content.players, ...content.players,
{ {
@ -166,39 +170,185 @@ function EditorView({
}) })
} }
const onBallDrop = (ref: HTMLDivElement) => { const onObjectDetach = (
const ballBounds = ref.getBoundingClientRect() ref: HTMLDivElement,
let ballAssigned = false rackedObject: RackedCourtObject,
) => {
const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
setContent((content) => { const { x, y } = calculateRatio(refBounds, courtBounds)
const players = content.players.map((player) => {
if (ballAssigned) { let courtObject: CourtObject
return { ...player, hasBall: false }
} switch (rackedObject.key) {
const playerBounds = document case "ball":
.getElementById(player.id)! const ballObj = content.objects.findIndex(
.getBoundingClientRect() (o) => o.type == "ball",
const doesOverlap = !( )
ballBounds.top > playerBounds.bottom || const playerCollidedIdx = getPlayerCollided(
ballBounds.right < playerBounds.left || refBounds,
ballBounds.bottom < playerBounds.top || content.players,
ballBounds.left > playerBounds.right
) )
if (doesOverlap) { if (playerCollidedIdx != -1) {
ballAssigned = true 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) onBallDropOnPlayer(playerCollidedIdx)
return { players: players } 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 ( return (
<div id="main-div"> <div id="main-div">
<div id="topbar-div"> <div id="topbar-div">
<div id="topbar-left"> <div id="topbar-left">
<SavingState state={saveState} /> <SavingState state={saveState} />
</div> </div>
<div id="title-input-div"> <div id="title-input-div">
@ -220,7 +370,9 @@ function EditorView({
id="allies-rack" id="allies-rack"
objects={allies} objects={allies}
onChange={setAllies} onChange={setAllies}
canDetach={canDetach} canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect())
}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
@ -232,18 +384,24 @@ function EditorView({
)} )}
/> />
{showBall && ( <Rack
<BallPiece id={"objects"}
onDrop={() => onBallDrop(ballPiece.current!)} objects={objects}
pieceRef={ballPiece} onChange={setObjects}
/> canDetach={(div) =>
)} isBoundsOnCourt(div.getBoundingClientRect())
}
onElementDetached={onObjectDetach}
render={renderCourtObject}
/>
<Rack <Rack
id="opponent-rack" id="opponent-rack"
objects={opponents} objects={opponents}
onChange={setOpponents} onChange={setOpponents}
canDetach={canDetach} canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect())
}
onElementDetached={onPieceDetach} onElementDetached={onPieceDetach}
render={({ team, key }) => ( render={({ team, key }) => (
<PlayerPiece <PlayerPiece
@ -259,13 +417,22 @@ function EditorView({
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
players={content.players} players={content.players}
onBallDrop={onBallDrop} objects={content.objects}
onBallMoved={onBallDrop}
courtImage={ courtImage={
courtType == "PLAIN" ? plainCourt : halfCourt courtType == "PLAIN" ? plainCourt : halfCourt
} }
courtRef={courtDivContentRef} courtRef={courtDivContentRef}
onPlayerChange={(player) => { onPlayerChange={(player) => {
const playerBounds = document
.getElementById(player.id)!
.getBoundingClientRect()
if (!isBoundsOnCourt(playerBounds)) {
removePlayer(player)
return
}
setContent((content) => ({ setContent((content) => ({
...content,
players: toSplicedPlayers( players: toSplicedPlayers(
content.players, content.players,
player, player,
@ -274,32 +441,10 @@ function EditorView({
})) }))
}} }}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setContent((content) => ({ removePlayer(player)
players: toSplicedPlayers( }}
content.players, onBallRemove={() => {
player, removeCourtBall()
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,
},
])
}} }}
/> />
</div> </div>
@ -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 <BallPiece />
}
throw new Error("unknown racked court object ", courtObject.key)
}
function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] {
return ["1", "2", "3", "4", "5"] return ["1", "2", "3", "4", "5"]
.filter( .filter(

@ -19,7 +19,7 @@ CREATE TABLE Tactic
name varchar NOT NULL, name varchar NOT NULL,
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
owner integer 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, court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL,
FOREIGN KEY (owner) REFERENCES Account FOREIGN KEY (owner) REFERENCES Account
); );

@ -42,7 +42,7 @@ class EditorController {
return ViewHttpResponse::react("views/Editor.tsx", [ return ViewHttpResponse::react("views/Editor.tsx", [
"id" => -1, //-1 id means that the editor will not support saves "id" => -1, //-1 id means that the editor will not support saves
"name" => TacticModel::TACTIC_DEFAULT_NAME, "name" => TacticModel::TACTIC_DEFAULT_NAME,
"content" => '{"players": []}', "content" => '{"players": [], "objects": []}',
"courtType" => $courtType->name(), "courtType" => $courtType->name(),
]); ]);
} }

@ -64,7 +64,7 @@ class AuthModel {
public function login(string $email, string $password, array &$failures): ?Account { public function login(string $email, string $password, array &$failures): ?Account {
$hash = $this->gateway->getHash($email); $hash = $this->gateway->getHash($email);
if ($hash == null or (!password_verify($password, $hash))) { 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 null;
} }
return $this->gateway->getAccountFromMail($email); return $this->gateway->getAccountFromMail($email);

Loading…
Cancel
Save