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 Ball from "../../assets/icon/ball.svg?react"
import Draggable from "react-draggable"
export interface BallPieceProps {
onDrop: () => void
pieceRef: RefObject<HTMLDivElement>
}
import BallSvg from "../../assets/icon/ball.svg?react"
import { Ball } from "../../tactic/CourtObjects"
export function BallPiece({ onDrop, pieceRef }: BallPieceProps) {
return (
<Draggable onStop={onDrop} nodeRef={pieceRef} position={{ x: 0, y: 0 }}>
<div className={`ball-div`} ref={pieceRef}>
<Ball className={"ball"} />
</div>
</Draggable>
)
export function BallPiece() {
return <BallSvg className={"ball"} />
}

@ -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<HTMLDivElement>
}
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 (
<CourtBall
onMoved={onBallMoved}
ball={object}
onRemove={onBallRemove}
key="ball"
/>
)
}
throw new Error("unknown court object", object.type)
})}
</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 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<HTMLDivElement>
}
@ -36,7 +35,6 @@ export default function CourtPlayer({
<Draggable
handle={".player-piece"}
nodeRef={pieceRef}
bounds="parent"
position={{ x, y }}
onStop={() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect()
@ -69,15 +67,19 @@ export default function CourtPlayer({
if (e.key == "Delete") onRemove()
}}>
<div className="player-selection-tab">
<RemoveIcon
className="player-selection-tab-remove"
onClick={onRemove}
/>
{hasBall && (
<BallPiece
onDrop={() => onBallDrop(ballPiece.current!)}
pieceRef={ballPiece}
/>
<Draggable
nodeRef={ballPiece}
onStop={() =>
onBallDrop(
ballPiece.current!.getBoundingClientRect(),
)
}
position={{ x: 0, y: 0 }}>
<div ref={ballPiece}>
<BallPiece />
</div>
</Draggable>
)}
</div>
<PlayerPiece

@ -32,6 +32,8 @@
#racks {
display: flex;
justify-content: space-between;
align-items: center;
height: 25px;
}
.title-input {
@ -43,8 +45,28 @@
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 {
margin-right: 5px;
}
#allies-rack .player-piece {
margin-left: 5px;
}

@ -1,11 +1,17 @@
export type CourtObject = { type: "ball" } & 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)
*/
right_percentage: number
readonly rightRatio: number
}

@ -1,26 +1,26 @@
import { Team } from "./Team"
export interface Player {
id: string
readonly id: string
/**
* the player's team
* */
team: Team
readonly team: Team
/**
* 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)
*/
bottomRatio: number
readonly bottomRatio: number
/**
* 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 { CourtObject } from "./CourtObjects"
export interface Tactic {
id: number
@ -8,4 +9,5 @@ export interface Tactic {
export interface TacticContent {
players: Player[]
objects: CourtObject[]
}

@ -13,20 +13,24 @@ import { BasketCourt } from "../components/editor/BasketCourt"
import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg"
import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import { BallPiece } from "../components/editor/BallPiece"
import { Player } from "../tactic/Player"
import { Tactic, TacticContent } from "../tactic/Tactic"
import { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team"
import { calculateRatio } from "../Utils"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { CourtObject } from "../tactic/CourtObjects"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
}
@ -56,6 +60,8 @@ interface RackedPlayer {
key: string
}
type RackedCourtObject = { key: "ball" }
export default function Editor({ id, name, courtType, content }: EditorProps) {
const isInGuestMode = id == -1
@ -122,24 +128,21 @@ function EditorView({
getRackPlayers(Team.Opponents, content.players),
)
const [showBall, setShowBall] = useState(
content.players.find((p) => p.hasBall) == undefined,
const [objects, setObjects] = useState<RackedCourtObject[]>(
isBallOnCourt(content) ? [] : [{ key: "ball" }],
)
const ballPiece = useRef<HTMLDivElement>(null)
const courtDivContentRef = useRef<HTMLDivElement>(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 (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={saveState} />
</div>
<div id="title-input-div">
@ -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 }) => (
<PlayerPiece
@ -232,18 +384,24 @@ function EditorView({
)}
/>
{showBall && (
<BallPiece
onDrop={() => onBallDrop(ballPiece.current!)}
pieceRef={ballPiece}
/>
)}
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect())
}
onElementDetached={onObjectDetach}
render={renderCourtObject}
/>
<Rack
id="opponent-rack"
objects={opponents}
onChange={setOpponents}
canDetach={canDetach}
canDetach={(div) =>
isBoundsOnCourt(div.getBoundingClientRect())
}
onElementDetached={onPieceDetach}
render={({ team, key }) => (
<PlayerPiece
@ -259,13 +417,22 @@ function EditorView({
<div id="court-div-bounds">
<BasketCourt
players={content.players}
onBallDrop={onBallDrop}
objects={content.objects}
onBallMoved={onBallDrop}
courtImage={
courtType == "PLAIN" ? plainCourt : halfCourt
}
courtRef={courtDivContentRef}
onPlayerChange={(player) => {
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()
}}
/>
</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[] {
return ["1", "2", "3", "4", "5"]
.filter(

@ -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
);

@ -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(),
]);
}

@ -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);

Loading…
Cancel
Save