diff --git a/traque-back/admin_socket.js b/traque-back/admin_socket.js index feb151c..1d4d58d 100644 --- a/traque-back/admin_socket.js +++ b/traque-back/admin_socket.js @@ -2,8 +2,6 @@ import { io, game, penaltyController } from "./index.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; import { config } from "dotenv"; -import { currentZone, initZone, removeZone } from "./zone_manager.js"; -import { GameState } from "./game.js"; config() const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD; @@ -49,7 +47,12 @@ export function initAdminSocketHandler() { //Other settings that need initialization socket.emit("penalty_settings", penaltyController.settings) socket.emit("game_settings", game.settings) - socket.emit("zone", currentZone) + socket.emit("zone_settings", game.zone.zoneSettings) + socket.emit("zone", game.zone.currentZone) + socket.emit("new_zone", { + begin: game.zone.currentStartZone, + end: game.zone.nextZone + }) } else { //Attempt unsuccessful @@ -69,25 +72,19 @@ export function initAdminSocketHandler() { playersBroadcast("game_settings", game.settings); }) - socket.on("set_zone", (zone) => { + socket.on("set_zone_settings", (settings) => { if (!loggedIn) { socket.emit("error", "Not logged in"); return; } - if(game.state != GameState.PLAYING) { - initZone(zone) - }else { - socket.emit("error", "Game is not in setup state") + if (!game.setZoneSettings(settings)) { + socket.emit("error", "Error changing zone"); + socket.emit("zone_settings", game.zone.zoneSettings) //Still broadcast the old config to the client who submited an incorrect config to keep the client up to date + } else { + secureAdminBroadcast("zone_settings", game.zone.zoneSettings) } - }) - socket.on("remove_zone", (zone, time) => { - if (!loggedIn) { - socket.emit("error", "Not logged in"); - return; - } - removeZone(zone, time) - }); + }) socket.on("set_penalty_settings", (settings) => { if (!loggedIn) { diff --git a/traque-back/game.js b/traque-back/game.js index f424137..9e9d6f3 100644 --- a/traque-back/game.js +++ b/traque-back/game.js @@ -2,7 +2,7 @@ import { secureAdminBroadcast } from "./admin_socket.js"; import { penaltyController } from "./index.js"; import { isInCircle } from "./map_utils.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; -import { resetZone } from "./zone_manager.js"; +import { ZoneManager } from "./zone_manager.js"; export const GameState = { SETUP: "setup", @@ -12,9 +12,10 @@ export const GameState = { } export default class Game { - constructor() { + constructor(onUpdateZone, onUpdateNewZone) { this.teams = []; this.state = GameState.SETUP; + this.zone = new ZoneManager(onUpdateZone, onUpdateNewZone) this.settings = { loserEndGameMessage: "", winnerEndGameMessage: "", @@ -35,9 +36,19 @@ export default class Game { //The game has started if (newState == GameState.PLAYING) { penaltyController.start(); + if (!this.zone.ready()) { + return false; + } this.initLastSentLocations(); + this.zone.reset() + //If the zone cannot be setup, reset everything + if (!this.zone.start()) { + this.setState(GameState.SETUP); + return; + } } if (newState != GameState.PLAYING) { + this.zone.reset(); penaltyController.stop(); } //Game reset @@ -224,8 +235,23 @@ export default class Game { this.updateTeamChasing(); } + /** + * Change the settings of the Zone manager + * The game should not be in PLAYING or FINISHED state + * @param {Object} newSettings The object containing the settings to be changed + * @returns false if failed + */ + setZoneSettings(newSettings) { + //cannot change zones while playing + if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) { + return false; + } + return this.zone.udpateSettings(newSettings) + } + finishGame() { this.setState(GameState.FINISHED); + this.zone.reset(); playersBroadcast("game_state", this.state); } } \ No newline at end of file diff --git a/traque-back/index.js b/traque-back/index.js index f031bae..88b2a4d 100644 --- a/traque-back/index.js +++ b/traque-back/index.js @@ -35,7 +35,19 @@ export const io = new Server(httpsServer, { } }); -export const game = new Game(); +//Zone update broadcast function, called by the game object +function onUpdateNewZone(newZone) { + playersBroadcast("new_zone", newZone) + secureAdminBroadcast("new_zone", newZone) +} + +function onUpdateZone(zone) { + playersBroadcast("zone", zone) + secureAdminBroadcast("zone", zone) +} + + +export const game = new Game(onUpdateZone, onUpdateNewZone); export const penaltyController = new PenaltyController(); diff --git a/traque-back/penalty_controller.js b/traque-back/penalty_controller.js index 54eb31f..88c7f56 100644 --- a/traque-back/penalty_controller.js +++ b/traque-back/penalty_controller.js @@ -3,7 +3,6 @@ import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js"; import { GameState } from "./game.js"; import { secureAdminBroadcast } from "./admin_socket.js"; import { game } from "./index.js"; -import { isInZone } from "./zone_manager.js"; export class PenaltyController { constructor() { @@ -92,10 +91,10 @@ export class PenaltyController { this.game.teams.forEach((team) => { if (team.captured) { return } //All the informations are not ready yet - if (team.currentLocation == null) { + if (team.currentLocation == null || this.game.zone.currentZone == null) { return; } - if (!isInZone(team.currentLocation)) { + if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, this.game.zone.currentZone.center, this.game.zone.currentZone.radius)) { //The team was not previously out of the zone if (!this.outOfBoundsSince[team.id]) { this.outOfBoundsSince[team.id] = new Date(); diff --git a/traque-back/team_socket.js b/traque-back/team_socket.js index 5eeb3ac..23dbddf 100644 --- a/traque-back/team_socket.js +++ b/traque-back/team_socket.js @@ -1,6 +1,5 @@ import { secureAdminBroadcast } from "./admin_socket.js"; import { io, game } from "./index.js"; -import { currentZone } from "./zone_manager.js"; /** * Send a socket message to all the players of a team @@ -9,13 +8,9 @@ import { currentZone } from "./zone_manager.js"; * @param {*} data The payload */ export function teamBroadcast(teamId, event, data) { - if(!game.getTeam(teamId)) { - return false; - } for (let socketId of game.getTeam(teamId).sockets) { io.of("player").to(socketId).emit(event, data) } - return true; } /** @@ -84,7 +79,11 @@ export function initTeamSocket() { socket.emit("login_response", true); socket.emit("game_state", game.state) socket.emit("game_settings", game.settings) - socket.emit("zone", currentZone) + socket.emit("zone", game.zone.currentZone) + socket.emit("new_zone", { + begin: game.zone.currentStartZone, + end: game.zone.nextZone + }) }); socket.on("logout", () => { diff --git a/traque-back/zone_manager.js b/traque-back/zone_manager.js index 2694608..31cdb91 100644 --- a/traque-back/zone_manager.js +++ b/traque-back/zone_manager.js @@ -1,108 +1,199 @@ -import { playersBroadcast, teamBroadcast } from './team_socket.js'; -import { secureAdminBroadcast } from './admin_socket.js'; +import { randomCirclePoint } from 'random-location' +import { isInCircle } from './map_utils.js'; +import { map } from './util.js'; -export let currentZone = [] -let tileSize = 17; - -export class TileNumber { - constructor(x, y) { - this.x = x; - this.y = y; - this.removeDate = null; - } - equals(other) { - return this.x === other.x && this.y === other.y; - } - - removeIn(minutes) { - this.removeDate = Date.now() + 1000 * 60 * minutes - } -} - -export function latLngToTileNumber(pos, tileSize) { - const numTilesX = 2 ** tileSize; - const numTilesY = 2 ** tileSize; - const lngDegrees = pos[1]; - const latRadians = pos[0] * (Math.PI / 180); - return { - x:Math.round(numTilesX * ((lngDegrees + 180) / 360)), - y:Math.round(numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2) - }; -} - -export function broadcastZoneState() { - playersBroadcast("zone", currentZone); - secureAdminBroadcast("zone", currentZone); -} - - -/** - * Remove all tiles from the zone - */ -export function resetZone() { - currentZone = []; - broadcastZoneState(); -} - -export function setTileSize(size) { - resetZone(); - tileSize = size; -} - -/** - * Check whether a position is in the zone - * @param {Object} position The position to check - */ -export function isInZone(position) { - let tile = latLngToTileNumber(position, tileSize); - return currentZone.some(square => square.equals(tile)) -} - -/** - * Initialize a zone with a list of tiles - * @param {Array} zone Array of tiles to add - */ -export function initZone(zone) { - currentZone = []; - try { - for (let { x, y } of zone) { - currentZone.push(new TileNumber(x, y)) +export class ZoneManager { + constructor(onZoneUpdate, onNextZoneUpdate) { + //Setings storing where the zone will start, end and how it should evolve + //The zone will start by staying at its mzx value for reductionInterval minutes + //and then reduce during reductionDuration minutes, then wait again... + //The reduction factor is such that after reductionCount the zone will be the min zone + //a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing + //a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced + this.zoneSettings = { + min: { center: null, radius: null }, + max: { center: null, radius: null }, + reductionCount: 2, + reductionDuration: 1, + reductionInterval: 1, + updateIntervalSeconds: 1, } - broadcastZoneState(); - } catch (e) { - console.error(e); - secureAdminBroadcast("error", "Invalid zone format") - } -} + this.nextZoneDecrement = null; + //Live location of the zone + this.currentZone = { center: null, radius: null }; -/** - * Put a list of tiles in a warning state for a certain amount of time, before removing them - * @param {Array} zone Array of tiles to remove - * @param {Number} time Time before those tiles get removed in minutes - */ -export function removeZone(zone, time) { - for (let tile of zone) { - for (let currentTile of currentZone) { - if (currentTile.equals(tile)) { - currentTile.removeIn(time); + //If the zone is shrinking, this is the target of the current shrinking + //If the zone is not shrinking, this will be the target of the next shrinking + this.nextZone = { center: null, radius: null }; + + //Zone at the begining of the shrinking + this.currentStartZone = { center: null, radius: null }; + + this.startDate = null; + this.started = false; + this.updateIntervalId = null; + this.nextZoneTimeoutId = null; + + this.onZoneUpdate = onZoneUpdate; + this.onNextZoneUpdate = onNextZoneUpdate + } + + /** + * Test if a given configuration object is valid, i.e if all needed values are well defined + * @param {Object} settings Settings object describing a config of a zone manager + * @returns if the config is correct + */ + validateSettings(settings) { + if (settings.reductionCount && (typeof settings.reductionCount != "number" || settings.reductionCount <= 0)) { return false } + if (settings.reductionDuration && (typeof settings.reductionDuration != "number" || settings.reductionDuration < 0)) { return false } + if (settings.reductionInterval && (typeof settings.reductionInterval != "number" || settings.reductionInterval < 0)) { return false } + if (settings.updateIntervalSeconds && (typeof settings.updateIntervalSeconds != "number" || settings.updateIntervalSeconds <= 0)) { return false } + if (settings.max && (typeof settings.max.radius != "number" || typeof settings.max.center.lat != "number" || typeof settings.max.center.lng != "number")) { return false } + if (settings.min && (typeof settings.min.radius != "number" || typeof settings.min.center.lat != "number" || typeof settings.min.center.lng != "number")) { return false } + return true; + } + /** + * Test if the zone manager is ready to start + * @returns true if the zone manager is ready to be started, false otherwise + */ + ready() { + return this.validateSettings(this.zoneSettings); + } + + /** + * Update the settings of the zone, this can be done by passing an object containing the settings to change. + * Unless specified, the durations are in minutes + * Default config : + * `this.zoneSettings = { + * min: {center: null, radius: null}, + * max: {center: null, radius: null}, + * reductionCount: 2, + * reductionDuration: 10, + * reductionInterval: 10, + * updateIntervalSeconds: 10, + * }` + * @param {Object} newSettings The fields of the settings to udpate + * @returns + */ + udpateSettings(newSettings) { + //validate settings + this.zoneSettings = { ...this.zoneSettings, ...newSettings }; + this.nextZoneDecrement = (this.zoneSettings.max.radius - this.zoneSettings.min.radius) / this.zoneSettings.reductionCount; + return true; + } + + /** + * Reinitialize the object and stop all the tasks + */ + reset() { + this.currentZoneCount = 0; + this.started = false; + if (this.updateIntervalId != null) { + clearInterval(this.updateIntervalId); + this.updateIntervalId = null; + } + if (this.nextZoneTimeoutId != null) { + clearTimeout(this.nextZoneTimeoutId); + this.nextZoneTimeoutId = null; + } + } + + /** + * Start the zone reduction sequence + */ + start() { + this.started = true; + this.startDate = new Date(); + //initialize the zone to its max value + this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); + this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); + this.currentZone = JSON.parse(JSON.stringify(this.zoneSettings.max)); + return this.setNextZone(); + + } + + /** + * Get the center of the next zone, this center need to satisfy two properties + * - it needs to be included in the current zone, this means that this new point should lie in the circle of center currentZone.center and radius currentZone.radius - newRadius + * - it needs to include the last zone, which means that the center must lie in the center of center min.center and of radius newRadius - min.radius + * @param newRadius the radius that the new zone will have + * @returns the coordinates of the new center as an object with lat and long fields + */ + getRandomNextCenter(newRadius) { + let ok = false; + let res = null + let tries = 0; + const MAX_TRIES = 100000 + //take a random point satisfying both conditions + while (tries++ < MAX_TRIES && !ok) { + res = randomCirclePoint({ latitude: this.currentZone.center.lat, longitude: this.currentZone.center.lng }, this.currentZone.radius - newRadius); + ok = (isInCircle({ lat: res.latitude, lng: res.longitude }, this.zoneSettings.min.center, newRadius - this.zoneSettings.min.radius)) + } + if(tries>=MAX_TRIES) { + return false; + } + return { + lat: res.latitude, + lng: res.longitude + } + } + + /** + * Compute the next zone satifying the given settings, update the nextZone and currentStartZone + * Wait for the appropriate duration before starting a new zone reduction if needed + */ + setNextZone() { + //At this point, nextZone == currentZone, we need to update the next zone, the raidus decrement, and start a timer before the next shrink + //last zone + if (this.currentZoneCount == this.zoneSettings.reductionCount) { + this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) + this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) + } else if (this.currentZoneCount == this.zoneSettings.reductionCount - 1) { + this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone)) + this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min)) + this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval) + this.currentZoneCount++; + } else if (this.currentZoneCount < this.zoneSettings.reductionCount) { + this.nextZone.center = this.getRandomNextCenter(this.nextZone.radius - this.nextZoneDecrement) + //Next center cannot be found + if(this.nextZone.center === false) { + console.log("no center") + return false; } + this.nextZone.radius -= this.nextZoneDecrement; + this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone)) + this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval) + this.currentZoneCount++; } - } - broadcastZoneState(); -} - -setInterval(() => { - let changed = false; - currentZone = currentZone.map(square => { - if (square.removeDate !== null && square.removeDate < Date.now()) { - changed = true; - return null; - } - return square; - }).filter(square => square !== null) - if (changed) { - broadcastZoneState(); + this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentStartZone))) + this.onNextZoneUpdate({ + begin: JSON.parse(JSON.stringify(this.currentStartZone)), + end: JSON.parse(JSON.stringify(this.nextZone)) + }) + return true; } -}, 1000); + /* + * Start a task that will run periodically updatinng the zone size, and calling the onZoneUpdate callback + * This will also periodically check if the reduction is over or not + * If the reduction is over this function will call setNextZone + */ + startShrinking() { + const startTime = new Date(); + this.updateIntervalId = setInterval(() => { + const completed = ((new Date() - startTime) / (1000 * 60)) / this.zoneSettings.reductionDuration; + this.currentZone.radius = map(completed, 0, 1, this.currentStartZone.radius, this.nextZone.radius) + this.currentZone.center.lat = map(completed,0,1, this.currentStartZone.center.lat, this.nextZone.center.lat) + this.currentZone.center.lng = map(completed,0,1, this.currentStartZone.center.lng, this.nextZone.center.lng) + this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentZone))) + //Zone shrinking is over + if (completed >= 1) { + clearInterval(this.updateIntervalId); + this.updateIntervalId = null; + this.setNextZone(); + return; + } + }, this.zoneSettings.updateIntervalSeconds * 1000); + } +} \ No newline at end of file diff --git a/traque-front/app/admin/layout.js b/traque-front/app/admin/layout.js index c75d592..9ff7fa6 100644 --- a/traque-front/app/admin/layout.js +++ b/traque-front/app/admin/layout.js @@ -11,7 +11,6 @@ export default function AdminLayout({ children}) {
diff --git a/traque-front/app/admin/map/page.js b/traque-front/app/admin/map/page.js deleted file mode 100644 index 8dfecf8..0000000 --- a/traque-front/app/admin/map/page.js +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; -import dynamic from 'next/dynamic'; - -const LiveMap = dynamic(() => import('@/components/admin/maps').then((mod) => mod.LiveMap), { - ssr: false -}); -export default function LiveMapPage() { - return -} \ No newline at end of file diff --git a/traque-front/app/admin/page.js b/traque-front/app/admin/page.js index 6e0ff72..af4a7d7 100644 --- a/traque-front/app/admin/page.js +++ b/traque-front/app/admin/page.js @@ -11,7 +11,7 @@ import dynamic from "next/dynamic"; const ZoneSelector = dynamic(() => import('@/components/admin/zoneSelector').then((mod) => mod.ZoneSelector), { ssr: false }); -const ZoneEditor = dynamic(() => import('@/components/admin/maps').then((mod) => mod.ZoneEditor), { +const LiveMap = dynamic(() => import('@/components/admin/mapPicker').then((mod) => mod.LiveMap), { ssr: false }); export default function AdminPage() { @@ -36,7 +36,7 @@ export default function AdminPage() { {(gameState == GameState.SETUP || gameState == GameState.PLACEMENT) && } {(gameState == GameState.SETUP || gameState == GameState.PLACEMENT) && } {gameState == GameState.PLAYING &&
- +
}
) diff --git a/traque-front/components/admin/mapPicker.jsx b/traque-front/components/admin/mapPicker.jsx new file mode 100644 index 0000000..e511c3b --- /dev/null +++ b/traque-front/components/admin/mapPicker.jsx @@ -0,0 +1,125 @@ +"use client"; +import { useLocation } from "@/hook/useLocation"; +import { useEffect, useState } from "react"; +import "leaflet/dist/leaflet.css"; +import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet"; +import { useMapCircleDraw } from "@/hook/mapDrawing"; +import useAdmin from "@/hook/useAdmin"; + +function MapPan(props) { + const map = useMap(); + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + if (!initialized && props.center) { + map.flyTo(props.center, props.zoom, { animate: false }); + setInitialized(true) + } + }, [props.center]); + return null; +} + +function MapEventListener({ onClick, onMouseMove }) { + const map = useMap(); + useEffect(() => { + map.on('click', onClick); + return () => { + map.off('click', onClick); + } + }, [onClick]); + useEffect(() => { + map.on('mousemove', onMouseMove); + return () => { + map.off('mousemove', onMouseMove); + } + }); + return null; +} + +const DEFAULT_ZOOM = 17; +export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) { + const location = useLocation(Infinity); + const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea); + return ( + + + {center && radius && } + {markerPosition && } + + + ) +} +export const EditMode = { + MIN: 0, + MAX: 1 +} +export function ZonePicker({ minZone, setMinZone, maxZone, setMaxZone, editMode, ...props }) { + const location = useLocation(Infinity); + const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone); + const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone); + function handleClick(e) { + if (editMode == EditMode.MAX) { + maxClick(e); + } else { + minClick(e); + } + } + function handleMouseMove(e) { + if (editMode == EditMode.MAX) { + maxHover(e); + } else { + minHover(e); + } + } + return ( + + + {minCenter && minRadius && } + {maxCenter && maxRadius && } + + + + ) +} + +export function LiveMap() { + const location = useLocation(Infinity); + const { zone, zoneExtremities, teams, getTeamName } = useAdmin(); + return ( + + + + {zone && } + {zoneExtremities && } + {zoneExtremities && } + {teams.map((team) => team.currentLocation && !team.captured && + + {team.name} +

Chasing : {getTeamName(team.chasing)}

+

Chased by : {getTeamName(team.chased)}

+
+
)} +
+ ) +} \ No newline at end of file diff --git a/traque-front/components/admin/mapZoneSelector.jsx b/traque-front/components/admin/mapZoneSelector.jsx deleted file mode 100644 index cf16fde..0000000 --- a/traque-front/components/admin/mapZoneSelector.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useMapGrid } from '@/hook/mapDrawing'; -import { TileNumber, latLngToTileNumber } from '../util/map'; -import { useMapEvent } from 'react-leaflet'; - -export function MapGridZoneSelector({ tilesColor, onClickTile, tileSize }) { - useMapEvent('click', (e) => { - const fractionalTileNumber = latLngToTileNumber(e.latlng, tileSize); - const tileNumber = new TileNumber(Math.floor(fractionalTileNumber.x), Math.floor(fractionalTileNumber.y)); - onClickTile(tileNumber); - }); - - useMapGrid(tilesColor, tileSize); - - return null; -} diff --git a/traque-front/components/admin/maps.jsx b/traque-front/components/admin/maps.jsx deleted file mode 100644 index baa92b3..0000000 --- a/traque-front/components/admin/maps.jsx +++ /dev/null @@ -1,245 +0,0 @@ -"use client"; -import { useLocation } from "@/hook/useLocation"; -import { use, useEffect, useState } from "react"; -import "leaflet/dist/leaflet.css"; -import L from "leaflet"; -import { Circle, LayerGroup, LayersControl, MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet"; -import { useMapCircleDraw, useTilesColor } from "@/hook/mapDrawing"; -import useAdmin from "@/hook/useAdmin"; -import { MapGridZoneSelector } from "./mapZoneSelector.jsx"; -import { useAdminContext } from "@/context/adminContext.jsx"; -import { GreenButton } from "../util/button.jsx"; -import TextInput from "../util/textInput.jsx"; - -export const TILE_SIZE = 17 - -function MapPan(props) { - const map = useMap(); - const [initialized, setInitialized] = useState(false); - - useEffect(() => { - if (!initialized && props.center) { - map.flyTo(props.center, props.zoom, { animate: false }); - setInitialized(true) - } - }, [props.center]); - return null; -} - -function MapEventListener({ onClick, onMouseMove }) { - const map = useMap(); - useEffect(() => { - map.on('click', onClick); - return () => { - map.off('click', onClick); - } - }, [onClick]); - useEffect(() => { - map.on('mousemove', onMouseMove); - return () => { - map.off('mousemove', onMouseMove); - } - }); - return null; -} - -const DEFAULT_ZOOM = 13; -export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) { - const location = useLocation(Infinity); - const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea); - return ( - - - {center && radius && } - {markerPosition && } - - - ) -} - -//https://stackoverflow.com/questions/71231865/show-fixed-100-m-x-100-m-grid-on-lowest-zoom-level - -export function ZoneInitializer({ ...props }) { - const location = useLocation(Infinity); - const { zone } = useAdminContext(); - const { initZone } = useAdmin(); - - const tilesColor = useTilesColor(zone); - - const handleClickTile = (tile) => { - if (zone) { - if (zone.some(t => t.x === tile.x && t.y === tile.y)) { - initZone(zone.filter(t => t.x !== tile.x || t.y !== tile.y)); - } else { - initZone([...zone, tile]); - } - } - } - - return ( - - - - - - - - - - - - ) -} - -export function ZoneEditor() { - const location = useLocation(Infinity); - const { zone, teams, getTeamName, removeZone } = useAdmin(); - const [zonesToDelete, setZonesToDelete] = useState([]); - const [tilesColor, setTilesColor] = useState([]); - const [timeBeforeDeletion, setTimeBeforeDeletion] = useState(null); - - function handleClickTile(tile) { - if (!zone.some(t => t.x === tile.x && t.y === tile.y)) return; - console.log(tile, "click", zonesToDelete); - if (!zonesToDelete.some(t => t.x === tile.x && t.y === tile.y)) { - setZonesToDelete([...zonesToDelete, tile]); - console.log("delete", tile); - } else { - setZonesToDelete(zonesToDelete.filter(t => t.x !== tile.x || t.y !== tile.y)); - } - } - - useEffect(() => { - console.log(zone); - setTilesColor([ - ...zonesToDelete.map(t => ({ x: t.x, y: t.y, color: 'rgba(255, 0, 0, 0.5)' })), - ...zone - .filter(t => !zonesToDelete.some(t2 => t.x == t2.x && t.y == t2.y)) - .map(t => { - console.log(t) - if (t.removeDate == null) { - return { x: t.x, y: t.y, color: 'rgba(0, 0, 255, 0.5)' } - } else { - return { x: t.x, y: t.y, color: 'rgba(255, 255, 0, 0.5)' } - } - }), - ]); - }, [zone, zonesToDelete]); - - const handleSubmit = (e) => { - if (timeBeforeDeletion == null) { - return; - } - e.preventDefault(); - removeZone(zonesToDelete, timeBeforeDeletion); - setZonesToDelete([]); - setTimeBeforeDeletion(null); - } - - - return ( -
-
- - - - {teams.map((team) => team.currentLocation && !team.captured && - - {team.name} -

Chasing : {getTeamName(team.chasing)}

-

Chased by : {getTeamName(team.chased)}

-
-
)} - - - - - - - -
-
-
- - setTimeBeforeDeletion(Number(e.target.value))}> - Delete selected zones -
-
- ) -} - -export function LiveMap({ ...props }) { - const location = useLocation(Infinity); - const { zone, teams, getTeamName } = useAdmin(); - const tilesColor = useTilesColor(zone); - - return ( - - - - - - - { }} tileSize={TILE_SIZE} /> - - - - - {teams.map((team) => team.currentLocation && !team.captured && - - {team.name} -

Chasing : {getTeamName(team.chasing)}

-

Chased by : {getTeamName(team.chased)}

-
-
)} -
-
- - - {teams.map((team) => team.currentLocation && !team.captured && - - Last position of {team.name} - - )} - - -
-
- ) -} \ No newline at end of file diff --git a/traque-front/components/admin/teamEdit.jsx b/traque-front/components/admin/teamEdit.jsx index bb8ea53..e8f845a 100644 --- a/traque-front/components/admin/teamEdit.jsx +++ b/traque-front/components/admin/teamEdit.jsx @@ -4,7 +4,7 @@ import BlueButton, { RedButton } from '../util/button'; import useAdmin from '@/hook/useAdmin'; import dynamic from 'next/dynamic'; -const CircularAreaPicker = dynamic(() => import('./maps').then((mod) => mod.CircularAreaPicker), { +const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), { ssr: false }); diff --git a/traque-front/components/admin/zoneSelector.jsx b/traque-front/components/admin/zoneSelector.jsx index 32c7d89..2ef1781 100644 --- a/traque-front/components/admin/zoneSelector.jsx +++ b/traque-front/components/admin/zoneSelector.jsx @@ -1,12 +1,61 @@ -import { ZoneInitializer } from "./maps"; +import { useEffect, useState } from "react"; +import BlueButton, { GreenButton, RedButton } from "../util/button"; +import { EditMode, ZonePicker } from "./mapPicker"; +import TextInput from "../util/textInput"; +import useAdmin from "@/hook/useAdmin"; export function ZoneSelector() { + const [editMode, setEditMode] = useState(EditMode.MIN); + const [minZone, setMinZone] = useState(null); + const [maxZone, setMaxZone] = useState(null); + const [reductionCount, setReductionCount] = useState(""); + const [reductionDuration, setReductionDuration] = useState(""); + const [reductionInterval, setReductionInterval] = useState(""); + const {zoneSettings, changeZoneSettings} = useAdmin(); + + useEffect(() => { + if (zoneSettings) { + setMinZone(zoneSettings.min); + setMaxZone(zoneSettings.max); + setReductionCount(zoneSettings.reductionCount.toString()); + setReductionDuration(zoneSettings.reductionDuration.toString()); + setReductionInterval(zoneSettings.reductionInterval.toString()); + } + }, [zoneSettings]); + + function handleSettingsSubmit() { + changeZoneSettings({min:minZone, max:maxZone, reductionCount: Number(reductionCount), reductionDuration: Number(reductionDuration), reductionInterval: Number(reductionInterval)}); + } //When the user set one zone, switch to the other + useEffect(() => { + if(editMode == EditMode.MIN) { + setEditMode(EditMode.MAX); + }else { + setEditMode(EditMode.MIN); + } + + }, [minZone, maxZone]); + return

Edit zones

+ {editMode == EditMode.MIN && setEditMode(EditMode.MAX)}>Edit end zone} + {editMode == EditMode.MAX && setEditMode(EditMode.MIN)}>Edit start zone}
- +
+
+

Number of reductions

+ setReductionCount(e.target.value)}> +
+
+

Duration of each reduction

+ setReductionDuration(e.target.value)}> +
+
+

Interval between reductions

+ setReductionInterval(e.target.value)}> +
+ Apply
} \ No newline at end of file diff --git a/traque-front/components/team/actionDrawer.jsx b/traque-front/components/team/actionDrawer.jsx index 6354139..0828c00 100644 --- a/traque-front/components/team/actionDrawer.jsx +++ b/traque-front/components/team/actionDrawer.jsx @@ -16,6 +16,7 @@ export default function ActionDrawer() { useEffect(() => { const interval = setInterval(() => { + console.log(locationSendDeadline) const timeLeft = locationSendDeadline - Date.now(); setTimeLeftBeforePenalty(Math.floor(timeLeft / 1000 / 60)); }, 1000); diff --git a/traque-front/components/team/map.jsx b/traque-front/components/team/map.jsx index 70c783e..e5b505e 100644 --- a/traque-front/components/team/map.jsx +++ b/traque-front/components/team/map.jsx @@ -1,14 +1,11 @@ 'use client'; import React, { useEffect, useState } from 'react' -import { Circle, LayerGroup, LayersControl, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' +import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css' import "leaflet-defaulticon-compatibility"; import "leaflet/dist/leaflet.css"; import useGame from '@/hook/useGame'; import { useTeamContext } from '@/context/teamContext'; -import { useTilesColor } from '@/hook/mapDrawing'; -import { MapGridZoneSelector } from '../admin/mapZoneSelector'; -import { TILE_SIZE } from '../admin/maps'; const DEFAULT_ZOOM = 17; @@ -28,11 +25,27 @@ function MapPan(props) { return null; } -export function LiveMap({ ...props }) { - const {zone} = useTeamContext(); - const tilesColor = useTilesColor(zone); - const { currentPosition, enemyPosition } = useGame(); +function LiveZone() { + const { zone } = useTeamContext(); + console.log('Zone', zone); + return zone && +} +function ZoneExtremities() { + const { zoneExtremities } = useTeamContext(); + console.log('Zone extremities', zoneExtremities); + return zoneExtremities?.begin && zoneExtremities?.end && <> + {/* */} + + + +} + +export function LiveMap({ ...props }) { + const { currentPosition, enemyPosition } = useGame(); + useEffect(() => { + console.log('Current position', currentPosition); + }, [currentPosition]); return ( } - - - - {}} tileSize={TILE_SIZE}/> - - - + + ) } diff --git a/traque-front/components/team/placementOverlay.jsx b/traque-front/components/team/placementOverlay.jsx index c4bb5a6..63e6f68 100644 --- a/traque-front/components/team/placementOverlay.jsx +++ b/traque-front/components/team/placementOverlay.jsx @@ -1,5 +1,6 @@ import { useTeamConnexion } from "@/context/teamConnexionContext"; import useGame from "@/hook/useGame" +import Image from "next/image"; export default function PlacementOverlay() { const { name, ready } = useGame(); diff --git a/traque-front/components/util/map.jsx b/traque-front/components/util/map.jsx deleted file mode 100644 index bf19533..0000000 --- a/traque-front/components/util/map.jsx +++ /dev/null @@ -1,21 +0,0 @@ -export class TileNumber { - constructor(x, y) { - this.x = x; - this.y = y; - this.removeDate = null; - } - equals(other) { - return this.x === other.x && this.y === other.y; - } -} - -export function latLngToTileNumber(latLng, tileSize) { - const numTilesX = 2 ** tileSize; - const numTilesY = 2 ** tileSize; - const lngDegrees = latLng.lng; - const latRadians = latLng.lat * (Math.PI / 180); - return new L.Point( - numTilesX * ((lngDegrees + 180) / 360), - numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2 - ); -} \ No newline at end of file diff --git a/traque-front/context/adminContext.jsx b/traque-front/context/adminContext.jsx index 25f1e51..0c94189 100644 --- a/traque-front/context/adminContext.jsx +++ b/traque-front/context/adminContext.jsx @@ -4,15 +4,15 @@ import { useSocket } from "./socketContext"; import { useSocketListener } from "@/hook/useSocketListener"; import { useAdminConnexion } from "./adminConnexionContext"; import { GameState } from "@/util/gameState"; -import { TileNumber } from "@/components/util/map"; const adminContext = createContext(); function AdminProvider({ children }) { const [teams, setTeams] = useState([]); - const [zone, setZone] = useState([]) + const [zoneSettings, setZoneSettings] = useState(null) const [penaltySettings, setPenaltySettings] = useState(null); const [gameSettings, setGameSettings] = useState(null); + const [zone, setZone] = useState(null); const [zoneExtremities, setZoneExtremities] = useState(null); const { adminSocket } = useSocket(); const { loggedIn } = useAdminConnexion(); @@ -26,16 +26,13 @@ function AdminProvider({ children }) { //Bind listeners to update the team list and the game status on socket message useSocketListener(adminSocket, "teams", setTeams); + useSocketListener(adminSocket, "zone_settings", setZoneSettings); useSocketListener(adminSocket, "game_settings", setGameSettings); useSocketListener(adminSocket, "penalty_settings", setPenaltySettings); - useSocketListener(adminSocket, "zone", (zone) => setZone(zone.map(t => { - let tile = new TileNumber(t.x, t.y); - tile.removeDate = t.removeDate; - return tile; - }))); + useSocketListener(adminSocket, "zone", setZone); useSocketListener(adminSocket, "new_zone", setZoneExtremities); - const value = useMemo(() => ({ zone, zoneExtremities, teams, penaltySettings, gameSettings, gameState }), [teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings]); + const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings]); return ( {children} diff --git a/traque-front/context/teamContext.jsx b/traque-front/context/teamContext.jsx index e22c13f..5d66e9f 100644 --- a/traque-front/context/teamContext.jsx +++ b/traque-front/context/teamContext.jsx @@ -13,6 +13,7 @@ function TeamProvider({children}) { const [gameState, setGameState] = useState(GameState.SETUP); const [gameSettings, setGameSettings] = useState(null); const [zone, setZone] = useState(null); + const [zoneExtremities, setZoneExtremities] = useState(null); const measuredLocation = useLocation(5000); const {teamSocket} = useSocket(); const {loggedIn} = useTeamConnexion(); @@ -26,6 +27,7 @@ function TeamProvider({children}) { useSocketListener(teamSocket, "game_state", setGameState); useSocketListener(teamSocket, "zone", setZone); + useSocketListener(teamSocket, "new_zone", setZoneExtremities); useSocketListener(teamSocket, "game_settings", setGameSettings); @@ -38,7 +40,7 @@ function TeamProvider({children}) { } }, [loggedIn, measuredLocation]); - const value = useMemo(() => ({teamInfos, gameState, zone, gameSettings}), [gameSettings, teamInfos, gameState, zone]); + const value = useMemo(() => ({teamInfos, gameState, zone, zoneExtremities, gameSettings}), [gameSettings, teamInfos, gameState, zone, zoneExtremities]); return ( {children} diff --git a/traque-front/hook/mapDrawing.jsx b/traque-front/hook/mapDrawing.jsx index 17dbd97..804a99a 100644 --- a/traque-front/hook/mapDrawing.jsx +++ b/traque-front/hook/mapDrawing.jsx @@ -1,7 +1,4 @@ -import { TileNumber, latLngToTileNumber } from "@/components/util/map"; import { useEffect, useState } from "react"; -import { useLeafletContext } from '@react-leaflet/core'; -import L from 'leaflet'; export function useMapCircleDraw(area, setArea) { const [drawing, setDrawing] = useState(false); @@ -15,18 +12,18 @@ export function useMapCircleDraw(area, setArea) { }, [area]) function handleClick(e) { - if (!drawing) { + if(!drawing) { setCenter(e.latlng); setRadius(null); setDrawing(true); } else { setDrawing(false); - setArea({ center, radius }); + setArea({center, radius}); } } function handleMouseMove(e) { - if (drawing) { + if(drawing) { setRadius(e.latlng.distanceTo(center)); } } @@ -36,91 +33,4 @@ export function useMapCircleDraw(area, setArea) { center, radius, } -} - -export function useMapGrid(tilesColor, tileSize) { - const { layerContainer, map } = useLeafletContext(); - const [grid, setGrid] = useState(null); - - const Grid = L.GridLayer.extend({ - createTile: function (coords) { - const tile = L.DomUtil.create('canvas', 'leaflet-tile'); - const ctx = tile.getContext('2d'); - const size = this.getTileSize(); - tile.width = size.x - tile.height = size.y - - // calculate projection coordinates of top left tile pixel - const nwPoint = coords.scaleBy(size); - // calculate geographic coordinates of top left tile pixel - const nw = map.unproject(nwPoint, coords.z); - // calculate fraction tile number at top left point - const nwTile = latLngToTileNumber(nw, tileSize) - - // calculate projection coordinates of bottom right tile pixel - const sePoint = new L.Point(nwPoint.x + size.x - 1, nwPoint.y + size.y - 1) - // calculate geographic coordinates of bottom right tile pixel - const se = map.unproject(sePoint, coords.z); - // calculate fractional tile number at bottom right point - const seTile = latLngToTileNumber(se, tileSize) - - const minTileX = nwTile.x - const maxTileX = seTile.x - const minTileY = nwTile.y - const maxTileY = seTile.y - - for (let x = Math.ceil(minTileX) - 1; x <= Math.floor(maxTileX) + 1; x++) { - for (let y = Math.ceil(minTileY) - 1; y <= Math.floor(maxTileY) + 1; y++) { - - let tile = new TileNumber(x, y) - - const xMinPixel = Math.round(size.x * (x - minTileX) / (maxTileX - minTileX)); - const xMaxPixel = Math.round(size.x * (x + 1 - minTileX) / (maxTileX - minTileX)); - const yMinPixel = Math.round(size.y * (y - minTileY) / (maxTileY - minTileY)); - const yMaxPixel = Math.round(size.y * (y + 1 - minTileY) / (maxTileY - minTileY)); - - if (this.tilesColor?.some(t => t.x == tile.x && t.y == tile.y)) { - ctx.fillStyle = this.tilesColor.find(t => t.x == tile.x && t.y == tile.y).color; - } - else { - ctx.fillStyle = 'rgba(255, 255, 255, 0)'; - } - ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel); - } - } - return tile; - }, - }); - - useEffect(() => { - let g = new Grid({ - minZoom: 12, - }); - setGrid(g); - layerContainer.addLayer(g); - }, []) - - useEffect(() => { - if (grid) { - grid.tilesColor = tilesColor; - grid.redraw(); - } - }, [tilesColor,grid]); -} - -export function useTilesColor(zone) { - const [tilesColor, setTilesColor] = useState([]); - useEffect(() => { - if (zone) { - setTilesColor(zone.map(t => { - if(t.removeDate == null) { - return { x: t.x, y: t.y, color: 'rgba(0, 0, 255, 0.3'} - }else { - return { x: t.x, y: t.y, color: 'rgba(255, 255, 0, 0.3'} - } - - })); - } - }, [zone]); - return tilesColor; } \ No newline at end of file diff --git a/traque-front/hook/useAdmin.jsx b/traque-front/hook/useAdmin.jsx index f031e1d..3441641 100644 --- a/traque-front/hook/useAdmin.jsx +++ b/traque-front/hook/useAdmin.jsx @@ -39,12 +39,8 @@ export default function useAdmin(){ adminSocket.emit("change_state", state); } - function initZone(zone) { - adminSocket.emit("set_zone", zone); - } - - function removeZone(zone, time) { - adminSocket.emit("remove_zone", zone, time); + function changeZoneSettings(zone) { + adminSocket.emit("set_zone_settings", zone); } function changePenaltySettings(penalties) { @@ -54,6 +50,6 @@ export default function useAdmin(){ function changeGameSettings(settings) { adminSocket.emit("set_game_settings", settings); } - return {...adminContext,changeGameSettings, removeZone, initZone, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam }; + return {...adminContext,changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam }; } \ No newline at end of file diff --git a/traque-front/public/icons/clock.png b/traque-front/public/icons/clock.png deleted file mode 100644 index 6097257..0000000 Binary files a/traque-front/public/icons/clock.png and /dev/null differ