/* This module manages the play area during the game, shrinking it over time based of some settings. */ import { randomCirclePoint } from 'random-location' import { isInCircle } from './map_utils.js'; import { playersBroadcast } from './team_socket.js'; import { secureAdminBroadcast } from './admin_socket.js'; /** * Scale a value that is known to be in a range to a new range * for instance map(50,0,100,1000,2000) will return 1500 as 50 is halfway between 0 and 100 and 1500 is halfway through 1000 and 2000 * @param {Number} value value to map * @param {Number} oldMin minimum value of the number * @param {Number} oldMax maximum value of the number * @param {Number} newMin minimum value of the output * @param {Number} newMax maximum value of the output * @returns */ function map(value, oldMin, oldMax, newMin, newMax) { return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin; } export default { //Setings storing where the zone will start, end and how it should evolve //The zone will start by staying at its max 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 zoneSettings: { min: { center: null, radius: null }, max: { center: null, radius: null }, reductionCount: 2, reductionDuration: 1, reductionInterval: 1, updateIntervalSeconds: 1, }, nextZoneDecrement: null, //Live location of the zone currentZone: { center: null, radius: null }, //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 nextZone: { center: null, radius: null }, //Zone at the begining of the shrinking currentStartZone: { center: null, radius: null }, startDate: null, started: false, updateIntervalId: null, nextZoneTimeoutId: null, nextZoneDate: null, /** * 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() { if (!this.ready()) return false; this.reset(); 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() { this.nextZoneDate = Date.now() + this.zoneSettings.reductionInterval * 60 * 1000; //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++; } this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentStartZone))) this.onNextZoneUpdate({ begin: JSON.parse(JSON.stringify(this.currentStartZone)), end: JSON.parse(JSON.stringify(this.nextZone)), endDate: JSON.parse(JSON.stringify(this.nextZoneDate)), }) return true; }, /* * 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() { this.nextZoneDate = Date.now() + this.zoneSettings.reductionDuration * 60 * 1000; this.onZoneUpdateStart(JSON.parse(JSON.stringify(this.nextZoneDate))); 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); }, //a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced onNextZoneUpdate(newZone) { playersBroadcast("new_zone", newZone) secureAdminBroadcast("new_zone", newZone) }, //a call to onZoneUpdateStart will be made when the zone reduction starts onZoneUpdateStart(date) { playersBroadcast("zone_start", date) secureAdminBroadcast("zone_start", date) }, //a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing onZoneUpdate(zone) { playersBroadcast("zone", zone) secureAdminBroadcast("zone", zone) }, }