mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Cleaning
This commit is contained in:
13
doc/TODO.md
13
doc/TODO.md
@@ -34,18 +34,19 @@
|
|||||||
- [x] Pouvoir changer les paramètres du jeu pendant une partie.
|
- [x] Pouvoir changer les paramètres du jeu pendant une partie.
|
||||||
- [x] Implémenter les wireframes
|
- [x] Implémenter les wireframes
|
||||||
- [x] Ajouter une région par défaut si pas de position
|
- [x] Ajouter une région par défaut si pas de position
|
||||||
- [ ] Pouvoir faire pause dans la partie
|
|
||||||
- [ ] Voir les traces et évènements des teams
|
|
||||||
- [ ] Voir l'incertitude de position des teams
|
|
||||||
- [x] Focus une team cliquée
|
- [x] Focus une team cliquée
|
||||||
- [x] Refaire les flèches de chasse sur la map
|
- [x] Refaire les flèches de chasse sur la map
|
||||||
- [ ] Mettre en évidence le menu paramètre (configuration)
|
|
||||||
- [ ] Afficher un feedback quand un paramètre est sauvegardé
|
|
||||||
- [x] Pouvoir définir la zone de départ de chaque équipe
|
- [x] Pouvoir définir la zone de départ de chaque équipe
|
||||||
- [x] Nommer les polygons par des lettres de l'alphabet
|
- [x] Nommer les polygons par des lettres de l'alphabet
|
||||||
|
- [ ] Plein écran
|
||||||
|
- [ ] Pouvoir faire pause dans la partie
|
||||||
|
- [ ] Mettre en évidence le menu paramètre
|
||||||
|
- [ ] Afficher un feedback quand un paramètre est sauvegardé
|
||||||
|
- [ ] Améliorer le système de création zone (cercle et polygone)
|
||||||
|
- [ ] Voir les traces et évènements des teams
|
||||||
|
- [ ] Voir l'incertitude de position des teams
|
||||||
- [ ] Faire un menu quand on arrive sur la traque
|
- [ ] Faire un menu quand on arrive sur la traque
|
||||||
- [ ] Pouvoir load des paramètres enregistrés
|
- [ ] Pouvoir load des paramètres enregistrés
|
||||||
- [ ] Améliorer le système de création zone (cercle et polygone)
|
|
||||||
- [ ] Penser l'affichage en fin de traque
|
- [ ] Penser l'affichage en fin de traque
|
||||||
|
|
||||||
### Améliorations du jeu de la traque
|
### Améliorations du jeu de la traque
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ export default function Display() {
|
|||||||
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
|
const [bottomContainerHeight, setBottomContainerHeight] = useState(0);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {SERVER_URL} = useSocket();
|
const {SERVER_URL} = useSocket();
|
||||||
const {gameSettings, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
|
const {messages, zoneType, zoneExtremities, nextZoneDate, isShrinking, location, startLocationTracking, stopLocationTracking, gameState, startDate} = useTeamContext();
|
||||||
const {loggedIn, logout, loading} = useTeamConnexion();
|
const {loggedIn, logout, loading} = useTeamConnexion();
|
||||||
const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, startDate, finishDate, nCaptures, nSentLocation} = useGame();
|
const {sendCurrentPosition, capture, enemyLocation, enemyName, startingArea, captureCode, name, ready, captured, lastSentLocation, locationSendDeadline, teamId, outOfZone, outOfZoneDeadline, distance, finishDate, nCaptures, nSentLocation} = useGame();
|
||||||
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
|
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
|
||||||
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
|
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
|
||||||
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
|
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
|
||||||
@@ -225,7 +225,7 @@ export default function Display() {
|
|||||||
const CapturedMessage = () => {
|
const CapturedMessage = () => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.timersContainer, {height: 61}]}>
|
<View style={[styles.timersContainer, {height: 61}]}>
|
||||||
<Text style={{fontSize: 20}}>{gameSettings?.capturedMessage || "Vous avez été éliminé..."}</Text>
|
<Text style={{fontSize: 20}}>{messages?.captured || "Vous avez été éliminé..."}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -233,26 +233,28 @@ export default function Display() {
|
|||||||
const EndGameMessage = () => {
|
const EndGameMessage = () => {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.timersContainer, {height: 61}]}>
|
<View style={[styles.timersContainer, {height: 61}]}>
|
||||||
{captured && <Text style={{fontSize: 20}}>{gameSettings?.loserEndGameMessage || "Vous avez perdu..."}</Text>}
|
{captured && <Text style={{fontSize: 20}}>{messages?.loser || "Vous avez perdu..."}</Text>}
|
||||||
{!captured && <Text style={{fontSize: 20}}>{gameSettings?.winnerEndGameMessage || "Vous avez gagné !"}</Text>}
|
{!captured && <Text style={{fontSize: 20}}>{messages?.winner || "Vous avez gagné !"}</Text>}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Zones = () => {
|
const Zones = () => {
|
||||||
|
const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng});
|
||||||
|
|
||||||
switch (zoneType) {
|
switch (zoneType) {
|
||||||
case zoneTypes.circle:
|
case zoneTypes.circle:
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
|
{ zoneExtremities.begin && <Circle center={latToLatitude(zoneExtremities.begin.center)} radius={zoneExtremities.begin.radius} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} />}
|
||||||
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
|
{ zoneExtremities.end && <Circle center={latToLatitude(zoneExtremities.end.center)} radius={zoneExtremities.end.radius} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} />}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
case zoneTypes.polygon:
|
case zoneTypes.polygon:
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
{ zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.points} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
|
{ zoneExtremities.begin && <Polygon coordinates={zoneExtremities.begin.polygon.map(pos => latToLatitude(pos))} strokeColor="red" fillColor="rgba(255,0,0,0.1)" strokeWidth={2} /> }
|
||||||
{ zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.points} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
|
{ zoneExtremities.end && <Polygon coordinates={zoneExtremities.end.polygon.map(pos => latToLatitude(pos))} strokeColor="green" fillColor="rgba(0,255,0,0.1)" strokeWidth={2} /> }
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useLocation } from "../hook/useLocation";
|
import { useLocation } from "../hook/useLocation";
|
||||||
import { useSocketListener } from "../hook/useSocketListener";
|
import { useSocketListener } from "../hook/useSocketListener";
|
||||||
import { createContext, useContext, useMemo, useRef, useState } from "react";
|
import { createContext, useContext, useMemo, useState } from "react";
|
||||||
import { useSocket } from "./socketContext";
|
import { useSocket } from "./socketContext";
|
||||||
import { GameState } from "../util/gameState";
|
import { GameState } from "../util/gameState";
|
||||||
import useSendDeviceInfo from "../hook/useSendDeviceInfo";
|
import useSendDeviceInfo from "../hook/useSendDeviceInfo";
|
||||||
@@ -14,52 +14,52 @@ const zoneTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function TeamProvider({children}) {
|
function TeamProvider({children}) {
|
||||||
const { logout } = useTeamConnexion();
|
const {teamSocket} = useSocket();
|
||||||
|
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
|
||||||
|
// update_team
|
||||||
const [teamInfos, setTeamInfos] = useState({});
|
const [teamInfos, setTeamInfos] = useState({});
|
||||||
|
// game_state
|
||||||
const [gameState, setGameState] = useState(GameState.SETUP);
|
const [gameState, setGameState] = useState(GameState.SETUP);
|
||||||
const [gameSettings, setGameSettings] = useState(null);
|
const [startDate, setStartDate] = useState(null);
|
||||||
const [zoneType, setZoneType] = useState(null);
|
// current_zone
|
||||||
const [zoneExtremities, setZoneExtremities] = useState(null);
|
const [zoneExtremities, setZoneExtremities] = useState(null);
|
||||||
const [nextZoneDate, setNextZoneDate] = useState(null);
|
const [nextZoneDate, setNextZoneDate] = useState(null);
|
||||||
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
|
// settings
|
||||||
const {teamSocket} = useSocket();
|
const [messages, setMessages] = useState(null);
|
||||||
const teamInfosRef = useRef();
|
const [zoneType, setZoneType] = useState(null);
|
||||||
|
// logout
|
||||||
|
const { logout } = useTeamConnexion();
|
||||||
|
|
||||||
useSendDeviceInfo();
|
useSendDeviceInfo();
|
||||||
|
|
||||||
teamInfosRef.current = teamInfos;
|
useSocketListener(teamSocket, "update_team", (data) => {
|
||||||
|
setTeamInfos(teamInfos => ({...teamInfos, ...data}))
|
||||||
|
});
|
||||||
|
|
||||||
function setZone(data) {
|
useSocketListener(teamSocket, "game_state", (data) => {
|
||||||
setZoneType(data.type);
|
setGameState(data.state);
|
||||||
switch (data.type) {
|
setStartDate(data.date);
|
||||||
case zoneTypes.circle:
|
});
|
||||||
setZoneExtremities({
|
|
||||||
begin: {...data.begin, ...{center : {latitude: data.begin.center.lat, longitude: data.begin.center.lng} }},
|
useSocketListener(teamSocket, "settings", (data) => {
|
||||||
end: {...data.end, ...{center : {latitude: data.end.center.lat, longitude: data.end.center.lng} }}
|
setMessages(data.messages);
|
||||||
});
|
setZoneType(data.zone.type);
|
||||||
break;
|
//TODO
|
||||||
case zoneTypes.polygon:
|
//setSendPositionDelay(data.sendPositionDelay);
|
||||||
setZoneExtremities({
|
//setOutOfZoneDelay(data.outOfZoneDelay);
|
||||||
begin: {...data.begin, ...{points : data.begin.points.map( p => ({latitude: p.lat,longitude: p.lng}) )}},
|
});
|
||||||
end: {...data.end, ...{points : data.end.points.map( p => ({latitude: p.lat,longitude: p.lng}) )}}
|
|
||||||
});
|
useSocketListener(teamSocket, "current_zone", (data) => {
|
||||||
break;
|
setZoneExtremities({begin: data.begin, end: data.end});
|
||||||
default:
|
|
||||||
setZoneExtremities({begin: data.begin, end: data.end});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
setNextZoneDate(data.endDate);
|
setNextZoneDate(data.endDate);
|
||||||
}
|
});
|
||||||
|
|
||||||
useSocketListener(teamSocket, "update_team", (newTeamInfos) => {setTeamInfos({...teamInfosRef.current, ...newTeamInfos})});
|
|
||||||
useSocketListener(teamSocket, "game_state", setGameState);
|
|
||||||
useSocketListener(teamSocket, "zone", setZone);
|
|
||||||
useSocketListener(teamSocket, "game_settings", setGameSettings);
|
|
||||||
useSocketListener(teamSocket, "logout", logout);
|
useSocketListener(teamSocket, "logout", logout);
|
||||||
|
|
||||||
|
|
||||||
const value = useMemo(() => (
|
const value = useMemo(() => (
|
||||||
{teamInfos, gameState, zoneType, zoneExtremities, nextZoneDate, gameSettings, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
|
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
|
||||||
), [teamInfos, gameState, zoneType, zoneExtremities, nextZoneDate, gameSettings, location]);
|
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<teamContext.Provider value={value}>
|
<teamContext.Provider value={value}>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useTeamContext } from "../context/teamContext";
|
|||||||
export default function useGame() {
|
export default function useGame() {
|
||||||
const { teamSocket } = useSocket();
|
const { teamSocket } = useSocket();
|
||||||
const { teamId } = useTeamConnexion();
|
const { teamId } = useTeamConnexion();
|
||||||
const { teamInfos, gameState } = useTeamContext();
|
const { teamInfos } = useTeamContext();
|
||||||
|
|
||||||
function sendCurrentPosition() {
|
function sendCurrentPosition() {
|
||||||
console.log("Reveal position.")
|
console.log("Reveal position.")
|
||||||
teamSocket.emit("send_position");
|
teamSocket.emit("send_position");
|
||||||
@@ -29,5 +29,5 @@ export default function useGame() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {...teamInfos, sendCurrentPosition, capture, teamId, gameState};
|
return {...teamInfos, sendCurrentPosition, capture, teamId};
|
||||||
}
|
}
|
||||||
@@ -55,9 +55,9 @@ export function initAdminSocketHandler() {
|
|||||||
socket.emit("current_zone", {
|
socket.emit("current_zone", {
|
||||||
begin: zoneManager.getCurrentZone(),
|
begin: zoneManager.getCurrentZone(),
|
||||||
end: zoneManager.getNextZone(),
|
end: zoneManager.getNextZone(),
|
||||||
endDate: zoneManager.currentZoneEndDate,
|
endDate: zoneManager.currentZone.endDate,
|
||||||
});
|
});
|
||||||
socket.emit("settings", game.getSettings());
|
socket.emit("settings", game.getAdminSettings());
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("add_team", (teamName) => {
|
socket.on("add_team", (teamName) => {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkEndGame() {
|
checkEndGame() {
|
||||||
if (this.teams.filter(team => !team.captured) <= 2) this.setState(GameState.FINISHED);
|
if (this.teams.filter(team => !team.captured).length <= 2) this.setState(GameState.FINISHED);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateChasingChain() {
|
updateChasingChain() {
|
||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
// Update of lastSentLocation
|
// Update of lastSentLocation
|
||||||
for (const team of this.teams) {
|
for (const team of this.teams) {
|
||||||
team.lastSentLocation = team.currentLocation;
|
team.lastSentLocation = team.currentLocation;
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000;
|
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
||||||
sendPositionTimeouts.set(team.id);
|
sendPositionTimeouts.set(team.id);
|
||||||
sendUpdatedTeamInformations(team.id);
|
sendUpdatedTeamInformations(team.id);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ export default {
|
|||||||
|
|
||||||
/* ------------------------------- STATE AND SETTINGS FUNCTIONS ------------------------------- */
|
/* ------------------------------- STATE AND SETTINGS FUNCTIONS ------------------------------- */
|
||||||
|
|
||||||
getSettings() {
|
getAdminSettings() {
|
||||||
return {
|
return {
|
||||||
messages: this.messages,
|
messages: this.messages,
|
||||||
zone: zoneManager.settings,
|
zone: zoneManager.settings,
|
||||||
@@ -131,14 +131,23 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getPlayerSettings() {
|
||||||
|
return {
|
||||||
|
messages: this.messages,
|
||||||
|
zone: {type: zoneManager.settings.type},
|
||||||
|
sendPositionDelay: sendPositionTimeouts.delay,
|
||||||
|
outOfZoneDelay: outOfZoneTimeouts.delay
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
changeSettings(newSettings) {
|
changeSettings(newSettings) {
|
||||||
if ("messages" in newSettings) this.messages = {...this.messages, ...newSettings.messages};
|
if ("messages" in newSettings) this.messages = {...this.messages, ...newSettings.messages};
|
||||||
if ("zone" in newSettings) zoneManager.changeSettings(newSettings.zone);
|
if ("zone" in newSettings) zoneManager.changeSettings(newSettings.zone);
|
||||||
if ("sendPositionDelay" in newSettings) sendPositionTimeouts.setDelay(newSettings.sendPositionDelay);
|
if ("sendPositionDelay" in newSettings) sendPositionTimeouts.setDelay(newSettings.sendPositionDelay);
|
||||||
if ("outOfZoneDelay" in newSettings) outOfZoneTimeouts.setDelay(newSettings.outOfZoneDelay);
|
if ("outOfZoneDelay" in newSettings) outOfZoneTimeouts.setDelay(newSettings.outOfZoneDelay);
|
||||||
// Broadcast new infos
|
// Broadcast new infos
|
||||||
secureAdminBroadcast("settings", this.getSettings());
|
secureAdminBroadcast("settings", this.getAdminSettings());
|
||||||
playersBroadcast("game_settings", this.messages);
|
playersBroadcast("settings", this.getPlayerSettings());
|
||||||
},
|
},
|
||||||
|
|
||||||
setState(newState) {
|
setState(newState) {
|
||||||
@@ -153,7 +162,7 @@ export default {
|
|||||||
break;
|
break;
|
||||||
case GameState.PLACEMENT:
|
case GameState.PLACEMENT:
|
||||||
if (this.teams.length < 3) {
|
if (this.teams.length < 3) {
|
||||||
secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate});
|
secureAdminBroadcast("game_state", {state: this.state, date: this.stateDate});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
trajectory.stop();
|
trajectory.stop();
|
||||||
@@ -163,7 +172,7 @@ export default {
|
|||||||
break;
|
break;
|
||||||
case GameState.PLAYING:
|
case GameState.PLAYING:
|
||||||
if (this.teams.length < 3) {
|
if (this.teams.length < 3) {
|
||||||
secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate});
|
secureAdminBroadcast("game_state", {state: this.state, date: this.stateDate});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
trajectory.start();
|
trajectory.start();
|
||||||
@@ -172,7 +181,7 @@ export default {
|
|||||||
break;
|
break;
|
||||||
case GameState.FINISHED:
|
case GameState.FINISHED:
|
||||||
if (this.state != GameState.PLAYING) {
|
if (this.state != GameState.PLAYING) {
|
||||||
secureAdminBroadcast("game_state", {state: this.state, stateDate: this.stateDate});
|
secureAdminBroadcast("game_state", {state: this.state, date: this.stateDate});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
trajectory.stop();
|
trajectory.stop();
|
||||||
@@ -187,8 +196,8 @@ export default {
|
|||||||
this.state = newState;
|
this.state = newState;
|
||||||
this.stateDate = dateNow;
|
this.stateDate = dateNow;
|
||||||
// Broadcast new infos
|
// Broadcast new infos
|
||||||
secureAdminBroadcast("game_state", {state: newState, stateDate: this.stateDate});
|
secureAdminBroadcast("game_state", {state: newState, date: this.stateDate});
|
||||||
playersBroadcast("game_state", newState);
|
playersBroadcast("game_state", {state: newState, date: this.stateDate});
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -214,7 +223,7 @@ export default {
|
|||||||
// Variables
|
// Variables
|
||||||
const team = this.getTeam(teamId);
|
const team = this.getTeam(teamId);
|
||||||
// Remove the player and its data
|
// Remove the player and its data
|
||||||
if (this.isCapitain(teamId, socketId)) {
|
if (this.isPlayerCapitain(teamId, socketId)) {
|
||||||
team.battery = null;
|
team.battery = null;
|
||||||
team.phoneModel = null;
|
team.phoneModel = null;
|
||||||
team.phoneName = null;
|
team.phoneName = null;
|
||||||
@@ -297,6 +306,17 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateTeam(teamId, newInfos) {
|
||||||
|
// Test of parameters
|
||||||
|
if (!this.hasTeam(teamId)) return false;
|
||||||
|
// Update
|
||||||
|
this.teams = this.teams.map(team => team.id == teamId ? {...team, ...newInfos} : team);
|
||||||
|
// Broadcast new infos
|
||||||
|
secureAdminBroadcast("teams", this.teams);
|
||||||
|
sendUpdatedTeamInformations(teamId);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
captureTeam(teamId) {
|
captureTeam(teamId) {
|
||||||
// Test of parameters
|
// Test of parameters
|
||||||
if (!this.hasTeam(teamId)) return false;
|
if (!this.hasTeam(teamId)) return false;
|
||||||
@@ -369,7 +389,7 @@ export default {
|
|||||||
const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] })
|
const teamCurrentlyOutOfZone = !zoneManager.isInZone({ lat: location[0], lng: location[1] })
|
||||||
if (teamCurrentlyOutOfZone && !team.outOfZone) {
|
if (teamCurrentlyOutOfZone && !team.outOfZone) {
|
||||||
team.outOfZone = true;
|
team.outOfZone = true;
|
||||||
team.outOfZoneDeadline = dateNow + outOfZoneTimeouts.duration * 60 * 1000;
|
team.outOfZoneDeadline = dateNow + outOfZoneTimeouts.delay * 60 * 1000;
|
||||||
outOfZoneTimeouts.set(teamId);
|
outOfZoneTimeouts.set(teamId);
|
||||||
} else if (!teamCurrentlyOutOfZone && team.outOfZone) {
|
} else if (!teamCurrentlyOutOfZone && team.outOfZone) {
|
||||||
team.outOfZone = false;
|
team.outOfZone = false;
|
||||||
@@ -395,7 +415,7 @@ export default {
|
|||||||
team.nSentLocation++;
|
team.nSentLocation++;
|
||||||
team.lastSentLocation = team.currentLocation;
|
team.lastSentLocation = team.currentLocation;
|
||||||
team.enemyLocation = enemyTeam.lastSentLocation;
|
team.enemyLocation = enemyTeam.lastSentLocation;
|
||||||
team.locationSendDeadline = dateNow + sendPositionTimeouts.duration * 60 * 1000;
|
team.locationSendDeadline = dateNow + sendPositionTimeouts.delay * 60 * 1000;
|
||||||
sendPositionTimeouts.set(team.id);
|
sendPositionTimeouts.set(team.id);
|
||||||
// Update enemy
|
// Update enemy
|
||||||
enemyTeam.nObserved++;
|
enemyTeam.nObserved++;
|
||||||
|
|||||||
@@ -92,14 +92,16 @@ export function initTeamSocket() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendUpdatedTeamInformations(loginTeamId);
|
sendUpdatedTeamInformations(loginTeamId);
|
||||||
socket.emit("game_state", game.state);
|
socket.emit("game_state", {
|
||||||
socket.emit("game_settings", game.messages);
|
state: game.state,
|
||||||
socket.emit("zone", {
|
date: game.stateDate
|
||||||
type: zoneManager.settings.type,
|
});
|
||||||
|
socket.emit("current_zone", {
|
||||||
begin: zoneManager.getCurrentZone(),
|
begin: zoneManager.getCurrentZone(),
|
||||||
end: zoneManager.getNextZone(),
|
end: zoneManager.getNextZone(),
|
||||||
endDate: zoneManager.currentZoneEndDate,
|
endDate: zoneManager.currentZone.endDate,
|
||||||
});
|
});
|
||||||
|
socket.emit("settings", game.getPlayerSettings());
|
||||||
callback({ isLoggedIn : true, message: "Logged in"});
|
callback({ isLoggedIn : true, message: "Logged in"});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, red
|
|||||||
|
|
||||||
function circleZone(center, radius, duration) {
|
function circleZone(center, radius, duration) {
|
||||||
return {
|
return {
|
||||||
|
type: zoneTypes.circle,
|
||||||
center: center,
|
center: center,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
@@ -84,18 +85,19 @@ function circleSettingsToZones(settings) {
|
|||||||
|
|
||||||
const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []}
|
const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []}
|
||||||
|
|
||||||
function polygonZone(points, duration) {
|
function polygonZone(polygon, duration) {
|
||||||
return {
|
return {
|
||||||
points: points,
|
type: zoneTypes.polygon,
|
||||||
|
polygon: polygon,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
|
|
||||||
isInZone(location) {
|
isInZone(location) {
|
||||||
const {lat: x, lng: y} = location;
|
const {lat: x, lng: y} = location;
|
||||||
let inside = false;
|
let inside = false;
|
||||||
|
|
||||||
for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) {
|
for (let i = 0, j = this.polygon.length - 1; i < this.polygon.length; j = i++) {
|
||||||
const {lat: xi, lng: yi} = this.points[i];
|
const {lat: xi, lng: yi} = this.polygon[i];
|
||||||
const {lat: xj, lng: yj} = this.points[j];
|
const {lat: xj, lng: yj} = this.polygon[j];
|
||||||
|
|
||||||
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
||||||
|
|
||||||
@@ -156,7 +158,7 @@ function polygonSettingsToZones(settings) {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
zones.push(polygonZone(
|
zones.push(polygonZone(
|
||||||
mergePolygons(zones[length-1].points, polygon),
|
mergePolygons(zones[length-1].polygon, polygon),
|
||||||
duration
|
duration
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -233,12 +235,11 @@ export default {
|
|||||||
|
|
||||||
zoneBroadcast() {
|
zoneBroadcast() {
|
||||||
const zone = {
|
const zone = {
|
||||||
type: this.settings.type,
|
|
||||||
begin: this.getCurrentZone(),
|
begin: this.getCurrentZone(),
|
||||||
end: this.getNextZone(),
|
end: this.getNextZone(),
|
||||||
endDate:this.currentZone.endDate,
|
endDate:this.currentZone.endDate,
|
||||||
};
|
};
|
||||||
playersBroadcast("zone", zone);
|
playersBroadcast("current_zone", zone);
|
||||||
secureAdminBroadcast("current_zone", zone);
|
secureAdminBroadcast("current_zone", zone);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export function MapButton({ icon, ...props }) {
|
|
||||||
return (
|
|
||||||
<button className="w-16 h-16 bg-custom-light-blue rounded-full hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
|
||||||
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ControlButton({ icon, ...props }) {
|
|
||||||
return (
|
|
||||||
<button className="w-[4.5rem] h-[4.5rem] bg-custom-light-blue rounded-lg hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
|
||||||
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { Marker, Tooltip, Polygon, Circle } from "react-leaflet";
|
import { Arrow, CircleZone, PolygonZone, Position, Tag } from "@/components/layer";
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import 'leaflet-polylinedecorator';
|
|
||||||
import { Arrow } from "@/components/layer";
|
|
||||||
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
|
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import { GameState, ZoneTypes } from "@/util/types";
|
import { GameState, ZoneTypes } from "@/util/types";
|
||||||
import { mapZooms } from "@/util/configurations";
|
import { mapZooms } from "@/util/configurations";
|
||||||
|
|
||||||
const positionIcon = new L.Icon({
|
export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows }) {
|
||||||
iconUrl: '/icons/marker/blue.png',
|
|
||||||
iconSize: [30, 30],
|
|
||||||
iconAnchor: [15, 15],
|
|
||||||
popupAnchor: [0, -15],
|
|
||||||
shadowSize: [30, 30],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
|
|
||||||
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||||
|
|
||||||
// Remaining time before sending position
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nextZoneDate) {
|
if (nextZoneDate) {
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
@@ -36,25 +24,25 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
|
|||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
// time is in seconds
|
// time is in seconds
|
||||||
if (time < 0) return "00:00";
|
if (!time || time < 0) return "00:00";
|
||||||
const minutes = Math.floor(time / 60);
|
const minutes = Math.floor(time / 60);
|
||||||
const seconds = Math.floor(time % 60);
|
const seconds = Math.floor(time % 60);
|
||||||
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function Zones() {
|
function Zones() {
|
||||||
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
|
if (!(showZones && gameState == GameState.PLAYING)) return null;
|
||||||
|
|
||||||
switch (zoneType) {
|
switch (zoneType) {
|
||||||
case ZoneTypes.CIRCLE:
|
case ZoneTypes.CIRCLE:
|
||||||
return (<>
|
return (<>
|
||||||
{ zoneExtremities.begin && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color="red" fillColor="red" />}
|
<CircleZone circle={zoneExtremities.begin} color="red" />
|
||||||
{ zoneExtremities.end && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color="green" fillColor="green" />}
|
<CircleZone circle={zoneExtremities.end} color="green" />
|
||||||
</>);
|
</>);
|
||||||
case ZoneTypes.POLYGON:
|
case ZoneTypes.POLYGON:
|
||||||
return (<>
|
return (<>
|
||||||
{ zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
<PolygonZone polygon={zoneExtremities.begin?.polygon} color="red" />
|
||||||
{ zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
<PolygonZone polygon={zoneExtremities.end?.polygon} color="green" />
|
||||||
</>);
|
</>);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
@@ -68,17 +56,15 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
|
|||||||
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
|
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
|
||||||
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
|
<MapEventListener onDragStart={() => setIsFocusing(false)}/>
|
||||||
<Zones/>
|
<Zones/>
|
||||||
{teams.map((team) => gameState == GameState.PLACEMENT && team.startingArea &&
|
{teams.map((team) => team && <Fragment key={team.id}>
|
||||||
<Circle key={team.id} center={team.startingArea.center} radius={team.startingArea.radius} color="blue" fillColor="blue">
|
<CircleZone circle={team.startingArea} color="blue" display={gameState == GameState.PLACEMENT && showZones}>
|
||||||
<Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>
|
<Tag text={team.name} display={showNames} />
|
||||||
</Circle>
|
</CircleZone>
|
||||||
)}
|
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chased)?.currentLocation} display={showArrows}/>
|
||||||
{teams.map((team) => team.currentLocation && !team.captured && <>
|
<Position position={team.currentLocation} color={"blue"} onClick={() => onSelected(team.id)} display={!team.captured}>
|
||||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon} eventHandlers={{click: () => onSelected(team.id)}}>
|
<Tag text={team.name} display={showNames} />
|
||||||
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
</Position>
|
||||||
</Marker>
|
</Fragment>)}
|
||||||
{showArrows && <Arrow key={team.id} pos1={team.currentLocation} pos2={getTeam(team.chased).currentLocation}/>}
|
|
||||||
</>)}
|
|
||||||
</CustomMapContainer>
|
</CustomMapContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { env } from 'next-runtime-env';
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import { getStatus } from '@/util/functions';
|
import { getStatus } from '@/util/functions';
|
||||||
|
import { Colors } from '@/util/types';
|
||||||
|
import { teamStatus } from '@/util/configurations';
|
||||||
|
|
||||||
function DotLine({ label, value }) {
|
function DotLine({ label, value }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,7 +24,7 @@ function IconValue({ color, icon, value }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<img src={`/icons/${icon}/${color}.png`} className="w-6 h-6" />
|
<img src={`/icons/${icon}/${color}.png`} className="w-6 h-6" />
|
||||||
<p className={`text-custom-${color}`}>{value}</p>
|
<p style={{color: Colors[color]}}>{value}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -41,11 +43,10 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
|||||||
|
|
||||||
if (!team) return null;
|
if (!team) return null;
|
||||||
|
|
||||||
function formatTime(startDate) {
|
function formatTime(startDate, endDate) {
|
||||||
// startDate in milliseconds
|
// startDate in milliseconds
|
||||||
const date = team.captured ? team.finishDate : Date.now();
|
if (endDate == null || startDate == null || startDate < 0) return NO_VALUE + ":" + NO_VALUE;
|
||||||
if (date == null || startDate == null || startDate < 0) return NO_VALUE + ":" + NO_VALUE;
|
const seconds = Math.floor((endDate - startDate) / 1000);
|
||||||
const seconds = Math.floor((date - startDate) / 1000);
|
|
||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||||
@@ -57,11 +58,10 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
|||||||
return `${Math.floor(distance / 100) / 10}km`;
|
return `${Math.floor(distance / 100) / 10}km`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSpeed(distance, startDate) {
|
function formatSpeed(distance, startDate, endDate) {
|
||||||
// distance in meters | startDate in milliseconds
|
// distance in meters | startDate in milliseconds
|
||||||
const date = team.captured ? team.finishDate : Date.now();
|
if (distance == null || distance < 0 || endDate == null || startDate == null || endDate <= startDate) return NO_VALUE + "km/h";
|
||||||
if (distance == null || distance < 0 || date == null || startDate == null || date <= startDate) return NO_VALUE + "km/h";
|
const speed = distance / (endDate - startDate);
|
||||||
const speed = distance / (date - startDate);
|
|
||||||
return `${Math.floor(speed * 36000) / 10}km/h`;
|
return `${Math.floor(speed * 36000) / 10}km/h`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,31 +75,31 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full h-full relative flex flex-col p-3 gap-3">
|
<div className="w-full h-full relative flex flex-col p-3 gap-3">
|
||||||
<button className="absolute left-2 -top-1 text-black text-5xl" onClick={onClose} title="Fermer">x</button>
|
<button className="absolute left-2 -top-1 text-black text-5xl" onClick={onClose} title="Fermer">x</button>
|
||||||
<p className={`text-2xl font-bold text-center ${getStatus(team, gameState).color} font-bold`}>{getStatus(team, gameState).label}</p>
|
<p className="text-2xl font-bold text-center" style={{color: getStatus(team, gameState).color}}>{getStatus(team, gameState).label}</p>
|
||||||
<p className="text-4xl font-bold text-center">{team.name ?? NO_VALUE}</p>
|
<p className="text-4xl font-bold text-center">{team.name ?? NO_VALUE}</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<img src={imgSrc ? imgSrc : "/images/missing_image.jpg"} alt="Photo de l'équipe"/>
|
<img src={imgSrc ? imgSrc : "/images/missing_image.jpg"} alt="Photo de l'équipe"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between items-center">
|
<div className="flex flex-row justify-between items-center">
|
||||||
<IconValue color={team.sockets.length > 0 ? "green" : "red"} icon="user" value={team.sockets.length ?? NO_VALUE} />
|
<IconValue color={team.sockets.length > 0 ? "green" : "red"} icon="user" value={team.sockets?.length ?? NO_VALUE} />
|
||||||
<IconValue color={team.battery >= 20 ? "green" : "red"} icon="battery" value={(team.battery ?? NO_VALUE) + "%"} />
|
<IconValue color={team.battery >= 20 ? "green" : "red"} icon="battery" value={(team.battery ?? NO_VALUE) + "%"} />
|
||||||
<IconValue
|
<IconValue
|
||||||
color={team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}
|
color={team.lastCurrentLocationDate && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}
|
||||||
icon="location" value={formatDate(team.lastCurrentLocationDate)}
|
icon="location" value={formatDate(team.lastCurrentLocationDate)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DotLine label="ID d'équipe" value={String(selectedTeamId).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")} />
|
<DotLine label="ID d'équipe" value={String(selectedTeamId).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")} />
|
||||||
<DotLine label="ID de capture" value={String(team.captureCode).padStart(4, '0')} />
|
<DotLine label="ID de capture" value={team.captureCode ? String(team.captureCode).padStart(4, '0') : NO_VALUE} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DotLine label="Chasse" value={getTeam(team.chasing).name ?? NO_VALUE} />
|
<DotLine label="Chasse" value={getTeam(team.chasing)?.name ?? NO_VALUE} />
|
||||||
<DotLine label="Chassé par" value={getTeam(team.chased).name ?? NO_VALUE} />
|
<DotLine label="Chassé par" value={getTeam(team.chased)?.name ?? NO_VALUE} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DotLine label="Distance" value={formatDistance(team.distance)} />
|
<DotLine label="Distance" value={formatDistance(team.distance)} />
|
||||||
<DotLine label="Temps de survie" value={formatTime(startDate)} />
|
<DotLine label="Temps de survie" value={formatTime(startDate, team.captured ? team.finishDate : Date.now())} />
|
||||||
<DotLine label="Vitesse moyenne" value={formatSpeed(team.distance, startDate)} />
|
<DotLine label="Vitesse moyenne" value={formatSpeed(team.distance, startDate, team.captured ? team.finishDate : Date.now())} />
|
||||||
<DotLine label="Captures" value={team.nCaptures ?? NO_VALUE} />
|
<DotLine label="Captures" value={team.nCaptures ?? NO_VALUE} />
|
||||||
<DotLine label="Observations" value={team.nSentLocation ?? NO_VALUE} />
|
<DotLine label="Observations" value={team.nSentLocation ?? NO_VALUE} />
|
||||||
<DotLine label="Observé" value={team.nObserved ?? NO_VALUE} />
|
<DotLine label="Observé" value={team.nObserved ?? NO_VALUE} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getStatus } from '@/util/functions';
|
|||||||
function TeamViewerItem({ team }) {
|
function TeamViewerItem({ team }) {
|
||||||
const { gameState } = useAdmin();
|
const { gameState } = useAdmin();
|
||||||
const status = getStatus(team, gameState);
|
const status = getStatus(team, gameState);
|
||||||
|
const NO_VALUE = "XX";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'w-full flex flex-row gap-3 p-2 bg-white justify-between'}>
|
<div className={'w-full flex flex-row gap-3 p-2 bg-white justify-between'}>
|
||||||
@@ -12,11 +13,11 @@ function TeamViewerItem({ team }) {
|
|||||||
<div className='flex flex-row gap-1'>
|
<div className='flex flex-row gap-1'>
|
||||||
<img src={`/icons/user/${team.sockets.length > 0 ? "green" : "red"}.png`} className="w-4 h-4" />
|
<img src={`/icons/user/${team.sockets.length > 0 ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
<img src={`/icons/battery/${team.battery >= 20 ? "green" : "red"}.png`} className="w-4 h-4" />
|
<img src={`/icons/battery/${team.battery >= 20 ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
<img src={`/icons/location/${team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}.png`} className="w-4 h-4" />
|
<img src={`/icons/location/${team.lastCurrentLocationDate && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xl font-bold`}>{team.name}</p>
|
<p className="text-xl font-bold">{team.name ?? NO_VALUE}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xl font-bold ${status.color}`}>
|
<p className="text-xl font-bold" style={{color: status.color}}>
|
||||||
{status.label}
|
{status.label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { BlueButton } from "@/components/button";
|
|
||||||
import { TextInput } from "@/components/input";
|
|
||||||
|
|
||||||
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
|
|
||||||
const [value, setValue] = useState("");
|
|
||||||
|
|
||||||
function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
setValue("");
|
|
||||||
onSubmit(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form className="bg-white shadow-md max-w mx-auto p-5 mx-10 flex flex-col space-y-4" onSubmit={handleSubmit}>
|
|
||||||
<h1 className="text-2xl font-bold text-center text-gray-700">{title}</h1>
|
|
||||||
<TextInput inputMode="numeric" placeholder={placeholder} value={value} onChange={(e) => setValue(e.target.value)} name="team-id"/>
|
|
||||||
<BlueButton type="submit">{buttonText}</BlueButton>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { useState } from "react";
|
||||||
import { useAdminConnexion } from '@/context/adminConnexionContext';
|
import { useAdminConnexion } from '@/context/adminConnexionContext';
|
||||||
import LoginForm from './components/loginForm';
|
|
||||||
|
|
||||||
export default function AdminLoginPage() {
|
export default function AdminLoginPage() {
|
||||||
const {login, useProtect} = useAdminConnexion();
|
const {login, useProtect} = useAdminConnexion();
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
setValue("");
|
||||||
|
login(value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginForm title="Admin login" placeholder="Admin password" buttonText={"Login"} onSubmit={login} />
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<form className="flex flex-col items-center gap-3 bg-white p-8 rounded-lg ring-1 ring-black" onSubmit={handleSubmit}>
|
||||||
|
<h1 className="text-2xl font-bold text-center text-gray-700">Admin login</h1>
|
||||||
|
<input name="team-id" className="w-60 h-12 text-center rounded ring-1 ring-inset ring-black placeholder:text-gray-400" placeholder="Admin password" value={value} onChange={(e) => setValue(e.target.value)}/>
|
||||||
|
<button className=" w-40 h-12 bg-blue-600 hover:bg-blue-500 text-l text-white rounded ease-out duration-200" type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,35 @@ import { GameState } from "@/util/types";
|
|||||||
import { mapStyles } from '@/util/configurations';
|
import { mapStyles } from '@/util/configurations';
|
||||||
import TeamSidePanel from "./components/teamSidePanel";
|
import TeamSidePanel from "./components/teamSidePanel";
|
||||||
import TeamViewer from './components/teamViewer';
|
import TeamViewer from './components/teamViewer';
|
||||||
import { MapButton, ControlButton } from './components/buttons';
|
|
||||||
|
|
||||||
// Imported at runtime and not at compile time
|
// Imported at runtime and not at compile time
|
||||||
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
|
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
|
||||||
|
|
||||||
|
function MainTitle() {
|
||||||
|
return (
|
||||||
|
<div className='w-full bg-custom-light-blue gap-5 p-5 flex flex-row shadow-2xl'>
|
||||||
|
<img src="/icons/home.png" className="w-8 h-8" />
|
||||||
|
<h2 className="text-3xl font-bold">Page principale</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapButton({ icon, ...props }) {
|
||||||
|
return (
|
||||||
|
<button className="w-16 h-16 bg-custom-light-blue rounded-full hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
||||||
|
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlButton({ icon, ...props }) {
|
||||||
|
return (
|
||||||
|
<button className="w-[4.5rem] h-[4.5rem] bg-custom-light-blue rounded-lg hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
||||||
|
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const { changeState, getTeam } = useAdmin();
|
const { changeState, getTeam } = useAdmin();
|
||||||
@@ -53,12 +77,9 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full bg-gray-200 p-3 flex flex-row content-start gap-3'>
|
<div className='w-full h-full p-3 flex flex-row gap-3'>
|
||||||
<div className="h-full w-2/6 flex flex-col gap-3">
|
<div className="h-full w-2/6 flex flex-col gap-3">
|
||||||
<div className='w-full bg-custom-light-blue gap-5 p-5 flex flex-row shadow-2xl'>
|
<MainTitle/>
|
||||||
<img src="/icons/home.png" className="w-8 h-8" />
|
|
||||||
<h2 className="text-3xl font-bold">Page principale</h2>
|
|
||||||
</div>
|
|
||||||
<Section title="Contrôle" innerClassName='flex flex-row justify-between'>
|
<Section title="Contrôle" innerClassName='flex flex-row justify-between'>
|
||||||
<Link href="/admin/parameters">
|
<Link href="/admin/parameters">
|
||||||
<ControlButton icon="parameters" title="Accéder aux paramètres du jeu"/>
|
<ControlButton icon="parameters" title="Accéder aux paramètres du jeu"/>
|
||||||
@@ -72,7 +93,7 @@ export default function AdminPage() {
|
|||||||
<TeamViewer selectedTeamId={selectedTeamId} onSelected={onSelected}/>
|
<TeamViewer selectedTeamId={selectedTeamId} onSelected={onSelected}/>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
<div className='grow flex-1 flex flex-col bg-white p-3 gap-3 shadow-2xl'>
|
<Section outerClassName='h-full flex-1' innerClassName='flex flex-col gap-3'>
|
||||||
<div className="flex-1 flex flex-row gap-3">
|
<div className="flex-1 flex flex-row gap-3">
|
||||||
<div className="flex-1 h-full">
|
<div className="flex-1 h-full">
|
||||||
<LiveMap
|
<LiveMap
|
||||||
@@ -101,7 +122,7 @@ export default function AdminPage() {
|
|||||||
{false && <MapButton icon="path" title="Afficher/masquer la trace de l'équipe sélectionnée"/>}
|
{false && <MapButton icon="path" title="Afficher/masquer la trace de l'équipe sélectionnée"/>}
|
||||||
{false && <MapButton icon="informations" title="Afficher/masquer les évènements de l'équipe sélectionnée"/>}
|
{false && <MapButton icon="informations" title="Afficher/masquer les évènements de l'équipe sélectionnée"/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Circle } from "react-leaflet";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { CustomButton } from "@/components/button";
|
|
||||||
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
import { TextInput } from "@/components/input";
|
import { NumberInput } from "@/components/input";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import useMapCircleDraw from "@/hook/useCircleDraw";
|
import useMapCircleDraw from "@/hook/useCircleDraw";
|
||||||
import useLocalVariable from "@/hook/useLocalVariable";
|
import useLocalVariable from "@/hook/useLocalVariable";
|
||||||
import { defaultZoneSettings } from "@/util/configurations";
|
import { defaultZoneSettings } from "@/util/configurations";
|
||||||
import { ZoneTypes } from "@/util/types";
|
import { ZoneTypes } from "@/util/types";
|
||||||
|
import { CircleZone } from "@/components/layer";
|
||||||
|
|
||||||
const EditMode = {
|
const EditMode = {
|
||||||
MIN: 0,
|
MIN: 0,
|
||||||
@@ -16,8 +15,24 @@ const EditMode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
||||||
const { center: maxCenter, radius: maxRadius, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
const { drawingCircle: drawingMaxCircle, handleLeftClick: maxLeftClick, handleRightClick: maxRightClick, handleMouseMove: maxHover } = useMapCircleDraw(maxZone, setMaxZone);
|
||||||
const { center: minCenter, radius: minRadius, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
const { drawingCircle: drawingMinCircle, handleLeftClick: minLeftClick, handleRightClick: minRightClick, handleMouseMove: minHover } = useMapCircleDraw(minZone, setMinZone);
|
||||||
|
|
||||||
|
function MaxCircleZone() {
|
||||||
|
return (
|
||||||
|
drawingMaxCircle
|
||||||
|
? <CircleZone circle={drawingMaxCircle} color="blue" />
|
||||||
|
: <CircleZone circle={maxZone} color="blue" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MinCircleZone() {
|
||||||
|
return (
|
||||||
|
drawingMinCircle
|
||||||
|
? <CircleZone circle={drawingMinCircle} color="red" />
|
||||||
|
: <CircleZone circle={minZone} color="red" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<MapEventListener
|
<MapEventListener
|
||||||
@@ -25,33 +40,31 @@ function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
|
|||||||
onRightClick={editMode == EditMode.MAX ? maxRightClick : minRightClick}
|
onRightClick={editMode == EditMode.MAX ? maxRightClick : minRightClick}
|
||||||
onMouseMove={editMode == EditMode.MAX ? maxHover : minHover}
|
onMouseMove={editMode == EditMode.MAX ? maxHover : minHover}
|
||||||
/>
|
/>
|
||||||
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="red" fillColor="red" />}
|
<MaxCircleZone/>
|
||||||
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="blue" fillColor="blue" />}
|
<MinCircleZone/>
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CircleZoneSelector() {
|
export default function CircleZoneSelector({ display }) {
|
||||||
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
|
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
|
||||||
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
|
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
|
||||||
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
|
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
|
||||||
const [editMode, setEditMode] = useState(EditMode.MIN);
|
const [editMode, setEditMode] = useState(EditMode.MAX);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localZoneSettings.type != ZoneTypes.CIRCLE) {
|
if (!localZoneSettings || localZoneSettings.type != ZoneTypes.CIRCLE) {
|
||||||
setLocalZoneSettings(defaultZoneSettings.circle);
|
setLocalZoneSettings(defaultZoneSettings.circle);
|
||||||
}
|
}
|
||||||
}, [localZoneSettings]);
|
}, [localZoneSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setEditMode(editMode == EditMode.MIN ? EditMode.MAX : EditMode.MIN);
|
|
||||||
}, [localZoneSettings.min, localZoneSettings.max]);
|
|
||||||
|
|
||||||
function setMinZone(minZone) {
|
function setMinZone(minZone) {
|
||||||
setLocalZoneSettings({...localZoneSettings, min: minZone});
|
setLocalZoneSettings({...localZoneSettings, min: minZone});
|
||||||
|
setEditMode(EditMode.MAX);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMaxZone(maxZone) {
|
function setMaxZone(maxZone) {
|
||||||
setLocalZoneSettings({...localZoneSettings, max: maxZone});
|
setLocalZoneSettings({...localZoneSettings, max: maxZone});
|
||||||
|
setEditMode(EditMode.MIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateReductionCount(reductionCount) {
|
function updateReductionCount(reductionCount) {
|
||||||
@@ -67,44 +80,30 @@ export default function CircleZoneSelector() {
|
|||||||
applyLocalOutOfZoneDelay();
|
applyLocalOutOfZoneDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function customStringToInt(e) {
|
|
||||||
return parseInt(e, 10) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full gap-3 flex flex-row'>
|
<div className={display ? 'w-full h-full gap-3 flex flex-row' : "hidden"}>
|
||||||
{localZoneSettings.type == ZoneTypes.CIRCLE && <>
|
{localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE && <>
|
||||||
<div className="h-full flex-1">
|
<div className="h-full flex-1">
|
||||||
<CustomMapContainer>
|
<CustomMapContainer>
|
||||||
<Drawings minZone={localZoneSettings.min} setMinZone={setMinZone} maxZone={localZoneSettings.max} setMaxZone={setMaxZone} editMode={editMode} />
|
<Drawings minZone={localZoneSettings.min} setMinZone={setMinZone} maxZone={localZoneSettings.max} setMaxZone={setMaxZone} editMode={editMode} />
|
||||||
</CustomMapContainer>
|
</CustomMapContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-1/6 flex flex-col gap-3">
|
<div className="h-full w-1/6 flex flex-col gap-3">
|
||||||
<div className="w-full h-15">
|
{editMode == EditMode.MIN && <button className="w-full h-16 text-lg text-white rounded bg-blue-600 hover:bg-blue-500" onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</button>}
|
||||||
{editMode == EditMode.MIN && <CustomButton color="blue" onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</CustomButton>}
|
{editMode == EditMode.MAX && <button className="w-full h-16 text-lg text-white rounded bg-red-600 hover:bg-red-500" onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</button>}
|
||||||
{editMode == EditMode.MAX && <CustomButton color="red" onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</CustomButton>}
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Reduction number</p>
|
<p>Reduction number</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id="reduction-number" value={localZoneSettings.reductionCount ?? ""} onChange={updateReductionCount} />
|
||||||
<TextInput id="reduction-number" value={localZoneSettings.reductionCount ?? ""} onChange={(e) => updateReductionCount(customStringToInt(e.target.value))} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Zone duration</p>
|
<p>Zone duration</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id="duration" value={localZoneSettings.duration ?? ""} onChange={updateDuration} />
|
||||||
<TextInput id="duration" value={localZoneSettings.duration ?? ""} onChange={(e) => updateDuration(customStringToInt(e.target.value))} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Timeout</p>
|
<p>Timeout</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id="timeout-circle-selector" value={localOutOfZoneDelay ?? ""} onChange={setLocalOutOfZoneDelay} />
|
||||||
<TextInput id="timeout" value={localOutOfZoneDelay ?? ""} onChange={(e) => setLocalOutOfZoneDelay(customStringToInt(e.target.value))} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-15">
|
|
||||||
<CustomButton color="green" onClick={handleSubmit}>Apply</CustomButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button className="w-full h-16 text-lg text-white rounded bg-green-600 hover:bg-green-500" onClick={handleSubmit}>Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function MessageInput({title, ...props}) {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row gap-3 items-center">
|
<div className="w-full flex flex-row gap-3 items-center">
|
||||||
<p>{title}</p>
|
<p>{title}</p>
|
||||||
<input {...props} type="text" className="w-full h-8 p-2 rounded ring-1 ring-inset ring-gray-400 placeholder:text-gray-600" />
|
<input className="w-full p-1 rounded ring-1 ring-inset ring-gray-400 placeholder:text-gray-600" {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Circle, Tooltip } from "react-leaflet";
|
|
||||||
import { List } from "@/components/list";
|
import { List } from "@/components/list";
|
||||||
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import useMultipleCircleDraw from "@/hook/useMultipleCircleDraw";
|
import useMultipleCircleDraw from "@/hook/useMultipleCircleDraw";
|
||||||
|
import { CircleZone, Tag } from "@/components/layer";
|
||||||
|
|
||||||
function Drawings({ placementZones, addZone, removeZone, handleRightClick }) {
|
function Drawings({ placementZones, addZone, removeZone, handleRightClick }) {
|
||||||
const { handleLeftClick, handleRightClick: handleRightClickDrawing } = useMultipleCircleDraw(placementZones, addZone, removeZone, 30);
|
const { handleLeftClick, handleRightClick: handleRightClickDrawing } = useMultipleCircleDraw(placementZones, addZone, removeZone, 30);
|
||||||
@@ -16,14 +16,14 @@ function Drawings({ placementZones, addZone, removeZone, handleRightClick }) {
|
|||||||
return (<>
|
return (<>
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={modifiedHandleRightClick} />
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={modifiedHandleRightClick} />
|
||||||
{ placementZones.map(placementZone =>
|
{ placementZones.map(placementZone =>
|
||||||
<Circle key={placementZone.id} center={placementZone.center} radius={placementZone.radius} color="blue" fillColor="blue">
|
<CircleZone key={placementZone.id} circle={placementZone} color="blue">
|
||||||
<Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{placementZone.name}</Tooltip>
|
<Tag text={placementZone.name} />
|
||||||
</Circle>
|
</CircleZone>
|
||||||
)}
|
)}
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlacementZoneSelector() {
|
export default function PlacementZoneSelector({ display }) {
|
||||||
const { teams, getTeam, placementTeam } = useAdmin();
|
const { teams, getTeam, placementTeam } = useAdmin();
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState(null);
|
const [selectedTeamId, setSelectedTeamId] = useState(null);
|
||||||
const [placementZones, setPlacementZones] = useState([]);
|
const [placementZones, setPlacementZones] = useState([]);
|
||||||
@@ -46,7 +46,7 @@ export default function PlacementZoneSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full gap-3 flex flex-row'>
|
<div className={display ? 'w-full h-full gap-3 flex flex-row' : "hidden"}>
|
||||||
<div className="h-full flex-1">
|
<div className="h-full flex-1">
|
||||||
<CustomMapContainer>
|
<CustomMapContainer>
|
||||||
<Drawings placementZones={placementZones} addZone={addPlacementZone} removeZone={removePlacementZone} handleRightClick={() => setSelectedTeamId(null)} />
|
<Drawings placementZones={placementZones} addZone={addPlacementZone} removeZone={removePlacementZone} handleRightClick={() => setSelectedTeamId(null)} />
|
||||||
@@ -58,7 +58,7 @@ export default function PlacementZoneSelector() {
|
|||||||
</div>
|
</div>
|
||||||
<List array={teams} selectedId={selectedTeamId} onSelected={(id) => setSelectedTeamId(selectedTeamId != id ? id : null)}>
|
<List array={teams} selectedId={selectedTeamId} onSelected={(id) => setSelectedTeamId(selectedTeamId != id ? id : null)}>
|
||||||
{ (team) =>
|
{ (team) =>
|
||||||
<div className="w-full flex flex-row items-center justify-between gap-2 p-2 bg-white">
|
<div key={team.id} className="w-full flex flex-row items-center justify-between gap-2 p-2 bg-white">
|
||||||
<p className='text-xl font-bold'>{team.name}</p>
|
<p className='text-xl font-bold'>{team.name}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,23 +17,19 @@ function ZoneTypeButton({title, onClick, isSelected}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PlayingZoneSelector() {
|
export default function PlayingZoneSelector({ display }) {
|
||||||
const [zoneType, setZoneType] = useState(ZoneTypes.POLYGON);
|
const [zoneType, setZoneType] = useState(ZoneTypes.POLYGON);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col gap-3">
|
<div className={display ? 'w-full h-full gap-3 flex flex-col' : "hidden"}>
|
||||||
<div className="w-full flex flex-row gap-3 items-center">
|
<div className="w-full flex flex-row gap-3 items-center">
|
||||||
<p className="text-l">Type de zone :</p>
|
<p className="text-l">Type de zone :</p>
|
||||||
<ZoneTypeButton title="Cercles" onClick={() => setZoneType(ZoneTypes.CIRCLE)} isSelected={zoneType == ZoneTypes.CIRCLE} />
|
<ZoneTypeButton title="Cercles" onClick={() => setZoneType(ZoneTypes.CIRCLE)} isSelected={zoneType == ZoneTypes.CIRCLE} />
|
||||||
<ZoneTypeButton title="Polygones" onClick={() => setZoneType(ZoneTypes.POLYGON)} isSelected={zoneType == ZoneTypes.POLYGON} />
|
<ZoneTypeButton title="Polygones" onClick={() => setZoneType(ZoneTypes.POLYGON)} isSelected={zoneType == ZoneTypes.POLYGON} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1">
|
<div className="w-full flex-1">
|
||||||
{zoneType == ZoneTypes.CIRCLE &&
|
<CircleZoneSelector display={zoneType == ZoneTypes.CIRCLE} />
|
||||||
<CircleZoneSelector/>
|
<PolygonZoneSelector display={zoneType == ZoneTypes.POLYGON} />
|
||||||
}
|
|
||||||
{zoneType == ZoneTypes.POLYGON &&
|
|
||||||
<PolygonZoneSelector/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Polyline } from "react-leaflet";
|
import { Polyline } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import { CustomButton } from "@/components/button";
|
|
||||||
import { ReorderList } from "@/components/list";
|
import { ReorderList } from "@/components/list";
|
||||||
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
import { CustomMapContainer, MapEventListener } from "@/components/map";
|
||||||
import { TextInput } from "@/components/input";
|
import { NumberInput } from "@/components/input";
|
||||||
import { Node, LabeledPolygon } from "@/components/layer";
|
import { Node, PolygonZone, Label } from "@/components/layer";
|
||||||
import useAdmin from "@/hook/useAdmin";
|
import useAdmin from "@/hook/useAdmin";
|
||||||
import useMapPolygonDraw from "@/hook/usePolygonDraw";
|
import useMapPolygonDraw from "@/hook/usePolygonDraw";
|
||||||
import useLocalVariable from "@/hook/useLocalVariable";
|
import useLocalVariable from "@/hook/useLocalVariable";
|
||||||
@@ -22,25 +21,45 @@ function Drawings({ localZoneSettings, addZone, removeZone }) {
|
|||||||
}
|
}
|
||||||
}, [localZoneSettings]);
|
}, [localZoneSettings]);
|
||||||
|
|
||||||
|
function getLabelPosition(polygon) {
|
||||||
|
const sum = polygon.reduce(
|
||||||
|
(acc, coord) => ({
|
||||||
|
lat: acc.lat + coord.lat,
|
||||||
|
lng: acc.lng + coord.lng
|
||||||
|
}),
|
||||||
|
{ lat: 0, lng: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// The calculated mean point can be out of the polygon
|
||||||
|
// Idea : take the mean point of the largest convex subpolygon
|
||||||
|
return {lat: sum.lat / polygon.length, lng: sum.lng / polygon.length};
|
||||||
|
}
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
|
||||||
{localZoneSettings.polygons.map((zone, i) => <LabeledPolygon key={i} polygon={zone.polygon} label={zone.id} />)}
|
{localZoneSettings.polygons.map(zone =>
|
||||||
|
<PolygonZone key={zone.id} polygon={zone.polygon} color="black" opacity='0.5' >
|
||||||
|
<Label position={getLabelPosition(zone.polygon)} label={zone.id} color="white" />
|
||||||
|
</PolygonZone>
|
||||||
|
)}
|
||||||
{ currentPolygon.length > 0 && <>
|
{ currentPolygon.length > 0 && <>
|
||||||
<Node pos={currentPolygon[0]} color={"red"} />
|
|
||||||
<Polyline positions={currentPolygon} pathOptions={{ color: "red", weight: 3 }} />
|
<Polyline positions={currentPolygon} pathOptions={{ color: "red", weight: 3 }} />
|
||||||
|
<Node position={currentPolygon[0]} color="red" />
|
||||||
</>}
|
</>}
|
||||||
{highlightNodes.map((node, i) => <Node key={i} pos={node} color={"black"} />)}
|
{highlightNodes.map((node, i) =>
|
||||||
|
<Node key={i} position={node} color="black" />
|
||||||
|
)}
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PolygonZoneSelector() {
|
export default function PolygonZoneSelector({ display }) {
|
||||||
const defaultDuration = 10;
|
const defaultDuration = 10;
|
||||||
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
|
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
|
||||||
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
|
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
|
||||||
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
|
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localZoneSettings.type != ZoneTypes.POLYGON) {
|
if (!localZoneSettings || localZoneSettings.type != ZoneTypes.POLYGON) {
|
||||||
setLocalZoneSettings(defaultZoneSettings.polygon);
|
setLocalZoneSettings(defaultZoneSettings.polygon);
|
||||||
}
|
}
|
||||||
}, [localZoneSettings]);
|
}, [localZoneSettings]);
|
||||||
@@ -76,14 +95,10 @@ export default function PolygonZoneSelector() {
|
|||||||
applyLocalZoneSettings();
|
applyLocalZoneSettings();
|
||||||
applyLocalOutOfZoneDelay();
|
applyLocalOutOfZoneDelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function customStringToInt(e) {
|
|
||||||
return parseInt(e, 10) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full gap-3 flex flex-row'>
|
<div className={display ? 'w-full h-full gap-3 flex flex-row' : "hidden"}>
|
||||||
{localZoneSettings.type == ZoneTypes.POLYGON && <>
|
{localZoneSettings && localZoneSettings.type == ZoneTypes.POLYGON && <>
|
||||||
<div className="h-full flex-1">
|
<div className="h-full flex-1">
|
||||||
<CustomMapContainer>
|
<CustomMapContainer>
|
||||||
<Drawings localZoneSettings={localZoneSettings} addZone={addZone} removeZone={removeZone} />
|
<Drawings localZoneSettings={localZoneSettings} addZone={addZone} removeZone={removeZone} />
|
||||||
@@ -95,23 +110,17 @@ export default function PolygonZoneSelector() {
|
|||||||
</div>
|
</div>
|
||||||
<ReorderList droppableId="zones-order" array={localZoneSettings.polygons} setArray={setLocalZoneSettingsPolygons}>
|
<ReorderList droppableId="zones-order" array={localZoneSettings.polygons} setArray={setLocalZoneSettingsPolygons}>
|
||||||
{ (zone) =>
|
{ (zone) =>
|
||||||
<div className="w-full p-2 bg-white flex flex-row gap-2 items-center justify-between">
|
<div key={zone.id} className="w-full p-2 bg-white flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Zone {zone.id}</p>
|
<p>Zone {zone.id}</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id={zone.id} value={zone.duration || ""} onChange={value => updateDuration(zone.id, value)}/>
|
||||||
<TextInput value={zone.duration || ""} onChange={(e) => updateDuration(zone.id, customStringToInt(e.target.value))}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</ReorderList>
|
</ReorderList>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Timeout</p>
|
<p>Timeout</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id="timeout-polygon-selector" value={localOutOfZoneDelay ?? ""} onChange={setLocalOutOfZoneDelay}/>
|
||||||
<TextInput id="timeout" value={localOutOfZoneDelay ?? ""} onChange={(e) => setLocalOutOfZoneDelay(customStringToInt(e.target.value))}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full h-15">
|
|
||||||
<CustomButton color="green" onClick={handleSubmit}>Apply</CustomButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button className="w-full h-16 text-lg text-white rounded bg-green-600 hover:bg-green-500" onClick={handleSubmit}>Apply</button>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|||||||
import { ReorderList } from '@/components/list';
|
import { ReorderList } from '@/components/list';
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import useLocalVariable from "@/hook/useLocalVariable";
|
import useLocalVariable from "@/hook/useLocalVariable";
|
||||||
import { TextInput } from "@/components/input";
|
import { NumberInput } from "@/components/input";
|
||||||
import { Section } from "@/components/section";
|
import { Section } from "@/components/section";
|
||||||
|
|
||||||
function TeamManagerItem({ team }) {
|
function TeamManagerItem({ team }) {
|
||||||
@@ -52,9 +52,7 @@ export default function TeamManager() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Interval between position updates</p>
|
<p>Interval between position updates</p>
|
||||||
<div className="w-16 h-10">
|
<NumberInput id="position-update" value={localSendPositionDelay ?? ""} onChange={setLocalSendPositionDelay} onBlur={applyLocalSendPositionDelay} />
|
||||||
<TextInput id="position-update" value={localSendPositionDelay ?? ""} onChange={(e) => setLocalSendPositionDelay(parseInt(e.target.value, 10))} onBlur={applyLocalSendPositionDelay} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ function TabButton({title, onClick, isSelected}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfigurationPage() {
|
export default function ParametersPage() {
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const [currentTab, setCurrentTab] = useState(Tabs.PLACEMENT_ZONES);
|
const [currentTab, setCurrentTab] = useState(Tabs.PLACEMENT_ZONES);
|
||||||
|
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full bg-gray-200 p-3 flex flex-row gap-3'>
|
<div className='w-full h-full p-3 flex flex-row gap-3'>
|
||||||
<div className="h-full w-2/6 gap-3 flex flex-col">
|
<div className="h-full w-2/6 gap-3 flex flex-col">
|
||||||
<ParametersTitle/>
|
<ParametersTitle/>
|
||||||
<Messages/>
|
<Messages/>
|
||||||
@@ -56,12 +56,8 @@ export default function ConfigurationPage() {
|
|||||||
<TabButton title="Zones de jeu" onClick={() => setCurrentTab(Tabs.PLAYING_ZONES)} isSelected={currentTab == Tabs.PLAYING_ZONES}/>
|
<TabButton title="Zones de jeu" onClick={() => setCurrentTab(Tabs.PLAYING_ZONES)} isSelected={currentTab == Tabs.PLAYING_ZONES}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex-1 p-3 bg-white">
|
<div className="w-full flex-1 p-3 bg-white">
|
||||||
{ currentTab == Tabs.PLAYING_ZONES &&
|
<PlacementZoneSelector display={currentTab == Tabs.PLACEMENT_ZONES} />
|
||||||
<PlayingZoneSelector />
|
<PlayingZoneSelector display={currentTab == Tabs.PLAYING_ZONES} />
|
||||||
}
|
|
||||||
{ currentTab == Tabs.PLACEMENT_ZONES &&
|
|
||||||
<PlacementZoneSelector />
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import SocketProvider from "@/context/socketContext";
|
|
||||||
|
|
||||||
import { PublicEnvScript } from 'next-runtime-env';
|
import { PublicEnvScript } from 'next-runtime-env';
|
||||||
|
import SocketProvider from "@/context/socketContext";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "La Traque",
|
title: "La Traque",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<PublicEnvScript />
|
<PublicEnvScript />
|
||||||
</head>
|
</head>
|
||||||
<SocketProvider>
|
<body className={inter.className + " w-screen h-screen bg-gray-200"}>
|
||||||
<body className={inter.className + " h-screen"}>{children}</body>
|
<SocketProvider>
|
||||||
</SocketProvider>
|
{children}
|
||||||
</html>
|
</SocketProvider>
|
||||||
);
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
export function CustomButton({ color, children, ...props }) {
|
|
||||||
const colorClasses = {
|
|
||||||
blue: 'bg-blue-600 hover:bg-blue-500',
|
|
||||||
red: 'bg-red-600 hover:bg-red-500',
|
|
||||||
green: 'bg-green-600 hover:bg-green-500',
|
|
||||||
yellow: 'bg-yellow-600 hover:bg-yellow-500',
|
|
||||||
purple: 'bg-purple-600 hover:bg-purple-500',
|
|
||||||
gray: 'bg-gray-600 hover:bg-gray-500',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button {...props} className={`${colorClasses[color]} text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded`}>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
const className = "block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600";
|
export function NumberInput({onChange, ...props}) {
|
||||||
|
function customStringToInt(e) {
|
||||||
|
return parseInt(e, 10) || null;
|
||||||
|
}
|
||||||
|
|
||||||
export function TextInput({...props}) {
|
return (
|
||||||
return (
|
<input className="w-12 h-10 text-center rounded ring-1 ring-inset ring-black placeholder:text-gray-400" onChange={(e) => onChange(customStringToInt(e.target.value))} {...props} />
|
||||||
<input {...props} type="text" className={className} />
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
export function TextArea({...props}) {
|
|
||||||
return (
|
|
||||||
<textarea {...props} className={className} />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,77 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Marker, CircleMarker, Polygon, useMap } from "react-leaflet";
|
import { Marker, Tooltip, CircleMarker, Circle, Polygon, useMap } from "react-leaflet";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import 'leaflet-polylinedecorator';
|
import 'leaflet-polylinedecorator';
|
||||||
|
|
||||||
export function Node({pos, nodeSize = 5, color = 'black'}) {
|
export function Node({position, nodeSize = 5, color = 'black', display = true}) {
|
||||||
return (
|
return (
|
||||||
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
|
display && position && <CircleMarker center={position} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LabeledPolygon({polygon, label, color = 'black', opacity = '0.5', border = 3, iconSize = 24, iconColor = 'white'}) {
|
export function Label({position, label, color, size = 24, display = true}) {
|
||||||
const length = polygon.length;
|
|
||||||
|
|
||||||
if (length < 3) return null;
|
|
||||||
|
|
||||||
const sum = polygon.reduce(
|
|
||||||
(acc, coord) => ({
|
|
||||||
lat: acc.lat + coord.lat,
|
|
||||||
lng: acc.lng + coord.lng
|
|
||||||
}),
|
|
||||||
{ lat: 0, lng: 0 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// meanPoint can be out of the polygon
|
|
||||||
// Idea : take the mean point of the largest connected subpolygon
|
|
||||||
const meanPoint = {lat: sum.lat / length, lng: sum.lng / length}
|
|
||||||
|
|
||||||
const labelIcon = L.divIcon({
|
const labelIcon = L.divIcon({
|
||||||
html: `<div style="
|
html: `<div style="
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: ${iconColor};
|
color: ${color};
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: ${iconSize}px;
|
font-size: ${size}px;
|
||||||
">${label}</div>`,
|
">${label || ""}</div>`,
|
||||||
className: 'custom-label-icon',
|
className: 'custom-label-icon',
|
||||||
iconSize: [iconSize, iconSize],
|
iconSize: [size, size],
|
||||||
iconAnchor: [iconSize / 2, iconSize / 2]
|
iconAnchor: [size / 2, size / 2]
|
||||||
});
|
});
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
<Polygon positions={polygon} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }} />
|
display && position && <Marker position={position} icon={labelIcon} />
|
||||||
<Marker position={meanPoint} icon={labelIcon} />
|
);
|
||||||
</>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Arrow({ pos1, pos2, color = 'black', weight = 5, arrowSize = 20, insetPixels = 25 }) {
|
export function Tag({text, display = true}) {
|
||||||
|
return (
|
||||||
|
display && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{text || ""}</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CircleZone({circle, color, opacity = '0.1', border = 3, display = true, children}) {
|
||||||
|
return (
|
||||||
|
display && circle &&
|
||||||
|
<Circle center={circle.center} radius={circle.radius} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}>
|
||||||
|
{children}
|
||||||
|
</Circle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PolygonZone({polygon, color, opacity = '0.1', border = 3, display = true, children}) {
|
||||||
|
return (
|
||||||
|
display && polygon && polygon.length >= 3 &&
|
||||||
|
<Polygon positions={polygon} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}>
|
||||||
|
{children}
|
||||||
|
</Polygon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Position({position, color, onClick, display = true, children}) {
|
||||||
|
|
||||||
|
const positionIcon = new L.Icon({
|
||||||
|
iconUrl: `/icons/marker/${color}.png`,
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15],
|
||||||
|
popupAnchor: [0, -15],
|
||||||
|
shadowSize: [30, 30],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
display && position &&
|
||||||
|
<Marker position={position} icon={positionIcon} eventHandlers={{click: onClick}}>
|
||||||
|
{children}
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Arrow({ pos1, pos2, color = 'black', weight = 5, arrowSize = 20, insetPixels = 25, display = true }) {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const [insetPositions, setInsetPositions] = useState(null);
|
const [insetPositions, setInsetPositions] = useState(null);
|
||||||
|
|
||||||
@@ -103,7 +128,7 @@ export function Arrow({ pos1, pos2, color = 'black', weight = 5, arrowSize = 20,
|
|||||||
}, [pos1, pos2]);
|
}, [pos1, pos2]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!insetPositions) return;
|
if (!display || !insetPositions) return;
|
||||||
|
|
||||||
// Create the base polyline
|
// Create the base polyline
|
||||||
const polyline = L.polyline(insetPositions, {
|
const polyline = L.polyline(insetPositions, {
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ export function List({array, selectedId, onSelected, children}) {
|
|||||||
return (
|
return (
|
||||||
<div className='w-full h-full bg-gray-300 overflow-y-scroll'>
|
<div className='w-full h-full bg-gray-300 overflow-y-scroll'>
|
||||||
<ul className="w-full p-1 pb-0">
|
<ul className="w-full p-1 pb-0">
|
||||||
{array.map((elem, i) => (
|
{array?.map((elem, i) => (
|
||||||
<li className="w-full" key={elem.id}>
|
<li className="w-full" key={elem.id}>
|
||||||
<div className={"w-full" + cursor() + outline(elem.id)} onClick={() => canSelect && onSelected(elem.id)}>
|
<div className={"w-full" + cursor() + outline(elem.id)} onClick={() => canSelect && onSelected(elem.id)}>
|
||||||
{children(elem, i)}
|
{children(elem, i)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-1"/>
|
<div className="w-full h-1"/>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)) ?? null}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -50,7 +50,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
|
|||||||
{provided => (
|
{provided => (
|
||||||
<div className='w-full h-full bg-gray-300 overflow-y-scroll' ref={provided.innerRef} {...provided.droppableProps}>
|
<div className='w-full h-full bg-gray-300 overflow-y-scroll' ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
<ul className="w-full p-1 pb-0">
|
<ul className="w-full p-1 pb-0">
|
||||||
{localArray.map((elem, i) => (
|
{localArray?.map((elem, i) => (
|
||||||
<li className='w-full' key={elem.id}>
|
<li className='w-full' key={elem.id}>
|
||||||
<Draggable draggableId={elem.id.toString()} index={i}>
|
<Draggable draggableId={elem.id.toString()} index={i}>
|
||||||
{provided => (
|
{provided => (
|
||||||
@@ -61,7 +61,7 @@ export function ReorderList({droppableId, array, setArray, children}) {
|
|||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)) ?? null}
|
||||||
</ul>
|
</ul>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function MapPan({center, zoom, animate=false}) {
|
|||||||
|
|
||||||
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) {
|
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
// TODO use useMapEvents instead of this + detect when zoom
|
||||||
|
|
||||||
// Handle the mouse click left
|
// Handle the mouse click left
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ export function Section({title, outerClassName, innerClassName, children}) {
|
|||||||
return (
|
return (
|
||||||
<div className={outerClassName}>
|
<div className={outerClassName}>
|
||||||
<div className='w-full h-full flex flex-col shadow-2xl'>
|
<div className='w-full h-full flex flex-col shadow-2xl'>
|
||||||
<div className='w-full p-1 bg-custom-light-blue text-center'>
|
{title &&
|
||||||
<h2 className="text-l">{title}</h2>
|
<div className='w-full p-1 bg-custom-light-blue text-center'>
|
||||||
</div>
|
<h2 className="text-l">{title}</h2>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div className='w-full flex-1 min-h-0 p-3 bg-white'>
|
<div className='w-full flex-1 min-h-0 p-3 bg-white'>
|
||||||
<div className={`w-full h-full ${innerClassName}`}>
|
<div className={`w-full h-full ${innerClassName}`}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -27,11 +27,10 @@ export function AdminProvider({ children }) {
|
|||||||
|
|
||||||
useSocketListener(adminSocket, "game_state", (data) => {
|
useSocketListener(adminSocket, "game_state", (data) => {
|
||||||
setGameState(data.state);
|
setGameState(data.state);
|
||||||
setStartDate(data.date)
|
setStartDate(data.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
useSocketListener(adminSocket, "current_zone", (data) => {
|
useSocketListener(adminSocket, "current_zone", (data) => {
|
||||||
setZoneType(data.type);
|
|
||||||
setZoneExtremities({begin: data.begin, end: data.end});
|
setZoneExtremities({begin: data.begin, end: data.end});
|
||||||
setNextZoneDate(data.endDate);
|
setNextZoneDate(data.endDate);
|
||||||
});
|
});
|
||||||
@@ -39,6 +38,7 @@ export function AdminProvider({ children }) {
|
|||||||
useSocketListener(adminSocket, "settings", (data) => {
|
useSocketListener(adminSocket, "settings", (data) => {
|
||||||
setMessages(data.messages);
|
setMessages(data.messages);
|
||||||
setZoneSettings(data.zone);
|
setZoneSettings(data.zone);
|
||||||
|
setZoneType(data.zone.type);
|
||||||
setSendPositionDelay(data.sendPositionDelay);
|
setSendPositionDelay(data.sendPositionDelay);
|
||||||
setOutOfZoneDelay(data.outOfZoneDelay);
|
setOutOfZoneDelay(data.outOfZoneDelay);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ export default function useAdmin() {
|
|||||||
return teams.find(team => team.id === teamId);
|
return teams.find(team => team.id === teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderTeams(newOrder) {
|
|
||||||
adminSocket.emit("reorder_teams", newOrder);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTeam(teamName) {
|
function addTeam(teamName) {
|
||||||
adminSocket.emit("add_team", teamName);
|
adminSocket.emit("add_team", teamName);
|
||||||
}
|
}
|
||||||
@@ -23,6 +19,10 @@ export default function useAdmin() {
|
|||||||
adminSocket.emit("remove_team", teamId);
|
adminSocket.emit("remove_team", teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reorderTeams(newOrder) {
|
||||||
|
adminSocket.emit("reorder_teams", newOrder);
|
||||||
|
}
|
||||||
|
|
||||||
function captureTeam(teamId) {
|
function captureTeam(teamId) {
|
||||||
adminSocket.emit("capture_team", teamId);
|
adminSocket.emit("capture_team", teamId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,42 +2,34 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function useMapCircleDraw(circle, setCircle) {
|
export default function useMapCircleDraw(circle, setCircle) {
|
||||||
const [drawing, setDrawing] = useState(false);
|
const [drawingCircle, setDrawingCircle] = useState(null);
|
||||||
const [center, setCenter] = useState(circle?.center || null);
|
|
||||||
const [radius, setRadius] = useState(circle?.radius || null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDrawing(false);
|
setDrawingCircle(null);
|
||||||
setCenter(circle?.center || null);
|
}, [circle]);
|
||||||
setRadius(circle?.radius || null);
|
|
||||||
}, [circle])
|
|
||||||
|
|
||||||
function handleLeftClick(e) {
|
function handleLeftClick(e) {
|
||||||
if (!drawing) {
|
if (drawingCircle) {
|
||||||
setCenter(e.latlng);
|
setCircle(drawingCircle);
|
||||||
setRadius(null);
|
setDrawingCircle(null);
|
||||||
setDrawing(true);
|
|
||||||
} else {
|
} else {
|
||||||
setDrawing(false);
|
setDrawingCircle({center: e.latlng, radius: 0});
|
||||||
setCircle({center, radius});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRightClick(e) {
|
function handleRightClick(e) {
|
||||||
if (drawing) {
|
if (drawingCircle) {
|
||||||
setDrawing(false);
|
setDrawingCircle(null);
|
||||||
setCenter(circle?.center || null);
|
} else if (e.latlng.distanceTo(circle.center) < circle.radius) {
|
||||||
setRadius(circle?.radius || null);
|
|
||||||
} else {
|
|
||||||
setCircle(null);
|
setCircle(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMouseMove(e) {
|
function handleMouseMove(e) {
|
||||||
if (drawing) {
|
if (drawingCircle) {
|
||||||
setRadius(e.latlng.distanceTo(center));
|
setDrawingCircle({center: drawingCircle.center, radius: e.latlng.distanceTo(drawingCircle.center)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { center, radius, handleLeftClick, handleRightClick, handleMouseMove };
|
return { drawingCircle, handleLeftClick, handleRightClick, handleMouseMove };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function useLocation(interval) {
|
|
||||||
const [location, setLocation] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!navigator.geolocation) {
|
|
||||||
console.log('Geolocation not supported');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interval < 1000 || interval == Infinity) {
|
|
||||||
console.log('Localisation interval no supported');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const watchId = navigator.geolocation.watchPosition(
|
|
||||||
(pos) => {
|
|
||||||
setLocation({
|
|
||||||
lat: pos.coords.latitude,
|
|
||||||
lng: pos.coords.longitude,
|
|
||||||
accuracy: pos.coords.accuracy,
|
|
||||||
timestamp: pos.timestamp
|
|
||||||
});
|
|
||||||
},
|
|
||||||
(err) => console.log("Error :", err),
|
|
||||||
{
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
timeout: 10000,
|
|
||||||
maximumAge: interval,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => navigator.geolocation.clearWatch(watchId);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
@@ -2,32 +2,16 @@
|
|||||||
|
|
||||||
export default function useMultipleCircleDraw(circles, addCircle, removeCircle, radius) {
|
export default function useMultipleCircleDraw(circles, addCircle, removeCircle, radius) {
|
||||||
|
|
||||||
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
|
||||||
const degToRad = (deg) => deg * (Math.PI / 180);
|
|
||||||
var R = 6371; // Radius of the earth in km
|
|
||||||
var dLat = degToRad(lat2 - lat1);
|
|
||||||
var dLon = degToRad(lon2 - lon1);
|
|
||||||
var a =
|
|
||||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
||||||
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
|
|
||||||
Math.sin(dLon / 2) * Math.sin(dLon / 2)
|
|
||||||
;
|
|
||||||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
||||||
var d = R * c; // Distance in km
|
|
||||||
return d * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBaddlyPlaced(latlng) {
|
function isBaddlyPlaced(latlng) {
|
||||||
return circles.some(circle => getDistanceFromLatLon(latlng, circle.center) < 2 * circle.radius);
|
return circles.some(circle => latlng.distanceTo(circle.center) < 2 * circle.radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCircleFromLatlng(latlng) {
|
function getCircleFromLatlng(latlng) {
|
||||||
return circles.find(circle => getDistanceFromLatLon(latlng, circle.center) < circle.radius);
|
return circles.find(circle => latlng.distanceTo(circle.center) < circle.radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeftClick(e) {
|
function handleLeftClick(e) {
|
||||||
if (isBaddlyPlaced(e.latlng)) return;
|
if (!isBaddlyPlaced(e.latlng)) addCircle(e.latlng, radius);
|
||||||
addCircle(e.latlng, radius);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRightClick(e) {
|
function handleRightClick(e) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ZoneTypes } from "./types";
|
import { ZoneTypes, Colors } from "./types";
|
||||||
|
|
||||||
export const mapLocations = {
|
export const mapLocations = {
|
||||||
paris: [48.86, 2.33]
|
paris: [48.86, 2.33]
|
||||||
@@ -26,10 +26,11 @@ export const defaultZoneSettings = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const teamStatus = {
|
export const teamStatus = {
|
||||||
playing: { label: "En jeu", color: "text-custom-green" },
|
default: { label: "Indisponible", color: Colors.black },
|
||||||
captured: { label: "Capturée", color: "text-custom-red" },
|
playing: { label: "En jeu", color: Colors.green },
|
||||||
outofzone: { label: "Hors zone", color: "text-custom-orange" },
|
captured: { label: "Capturée", color: Colors.red },
|
||||||
ready: { label: "Placée", color: "text-custom-green" },
|
outofzone: { label: "Hors zone", color: Colors.orange },
|
||||||
notready: { label: "Non placée", color: "text-custom-red" },
|
ready: { label: "Placée", color: Colors.green },
|
||||||
waiting: { label: "En attente", color: "text-custom-grey" },
|
notready: { label: "Non placée", color: Colors.red },
|
||||||
|
waiting: { label: "En attente", color: Colors.grey },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { GameState } from './types';
|
|||||||
import { teamStatus } from './configurations';
|
import { teamStatus } from './configurations';
|
||||||
|
|
||||||
export function getStatus(team, gamestate) {
|
export function getStatus(team, gamestate) {
|
||||||
|
if (!team) return null;
|
||||||
switch (gamestate) {
|
switch (gamestate) {
|
||||||
case GameState.SETUP:
|
case GameState.SETUP:
|
||||||
return teamStatus.waiting;
|
return teamStatus.waiting;
|
||||||
@@ -11,5 +12,7 @@ export function getStatus(team, gamestate) {
|
|||||||
return team.captured ? teamStatus.captured : team.outofzone ? teamStatus.outofzone : teamStatus.playing;
|
return team.captured ? teamStatus.captured : team.outofzone ? teamStatus.outofzone : teamStatus.playing;
|
||||||
case GameState.FINISHED:
|
case GameState.FINISHED:
|
||||||
return team.captured ? teamStatus.captured : teamStatus.playing;
|
return team.captured ? teamStatus.captured : teamStatus.playing;
|
||||||
|
default:
|
||||||
|
return teamStatus.default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
export const Colors = {
|
||||||
|
black: "#000000",
|
||||||
|
grey: "#808080",
|
||||||
|
green: "#19e119",
|
||||||
|
red: "#e11919",
|
||||||
|
orange: "#fa6400"
|
||||||
|
}
|
||||||
|
|
||||||
export const GameState = {
|
export const GameState = {
|
||||||
SETUP: "setup",
|
SETUP: "setup",
|
||||||
PLACEMENT: "placement",
|
PLACEMENT: "placement",
|
||||||
|
|||||||
Reference in New Issue
Block a user