mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 10:20:16 +01:00
249 lines
8.3 KiB
JavaScript
249 lines
8.3 KiB
JavaScript
import { playersBroadcast } from './team_socket.js';
|
|
import { secureAdminBroadcast } from './admin_socket.js';
|
|
|
|
|
|
/* -------------------------------- Useful functions and constants -------------------------------- */
|
|
|
|
const EARTH_RADIUS = 6_371_000; // Radius of the earth in m
|
|
|
|
function haversine_distance({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
|
|
const degToRad = (deg) => deg * (Math.PI / 180);
|
|
const dLat = degToRad(lat2 - lat1);
|
|
const dLon = degToRad(lon2 - lon1);
|
|
const 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);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return c * EARTH_RADIUS;
|
|
}
|
|
|
|
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
|
|
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
|
|
}
|
|
|
|
|
|
/* -------------------------------- Polygon zones -------------------------------- */
|
|
|
|
const defaultPolygonSettings = { polygons: [], durations: [] };
|
|
|
|
function polygonZone(points, duration) {
|
|
return {
|
|
points: points,
|
|
duration: duration,
|
|
|
|
isInZone(location) {
|
|
const {lat: x, lng: y} = location;
|
|
let inside = false;
|
|
|
|
for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) {
|
|
const {lat: xi, lng: yi} = this.points[i];
|
|
const {lat: xj, lng: yj} = this.points[j];
|
|
|
|
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
|
|
|
|
if (intersects) inside = !inside;
|
|
}
|
|
|
|
return inside;
|
|
}
|
|
}
|
|
}
|
|
|
|
function mergePolygons(poly1, poly2) {
|
|
// poly1 and poly2 are clockwise, not overlaping and touching polygons. If those two polygons were on a map, they would be
|
|
// one against each other, and the merge would make a new clockwise polygon out of the outer border of the two polygons.
|
|
// If it happens that poly1 and poly2 are not touching, poly1 would be returned untouched.
|
|
// Basically because polygons are clockwise, the alogorithm starts from a point A in poly1 not shared by poly2, and
|
|
// when a point is shared by poly1 and poly2, the algorithm continues in poly2, and so on until point A.
|
|
|
|
const getPointIndex = (point, array) => {
|
|
for (let i = 0; i < array.length; i++) {
|
|
if (latlngEqual(array[i], point)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Find the index of the first point of poly1 that doesn't belong to merge (it exists)
|
|
let i = 0;
|
|
while (getPointIndex(poly1[i], poly2) != -1) i++;
|
|
// Starting the merge from that point
|
|
const merge = [poly1[i]];
|
|
i = (i + 1) % poly1.length;
|
|
let currentArray = poly1;
|
|
let otherArray = poly2;
|
|
while (!latlngEqual(currentArray[i], merge[0])) {
|
|
const j = getPointIndex(currentArray[i], otherArray);
|
|
if (j != -1) {
|
|
[currentArray, otherArray] = [otherArray, currentArray];
|
|
i = j;
|
|
}
|
|
merge.push(currentArray[i]);
|
|
i = (i + 1) % currentArray.length;
|
|
}
|
|
return merge;
|
|
}
|
|
|
|
function polygonSettingsToZones(settings) {
|
|
const { polygons, durations } = settings;
|
|
const reversedPolygons = polygons.slice().reverse();
|
|
const reversedDurations = durations.slice().reverse();
|
|
|
|
const zones = [];
|
|
|
|
for (let i = 0; i < reversedPolygons.length; i++) {
|
|
const polygon =reversedPolygons[i];
|
|
const duration = reversedDurations[i];
|
|
const length = zones.length;
|
|
|
|
if (length == 0) {
|
|
zones.push(polygonZone(
|
|
polygon,
|
|
duration
|
|
));
|
|
} else {
|
|
zones.push(polygonZone(
|
|
mergePolygons(zones[length-1].points, polygon),
|
|
duration
|
|
));
|
|
}
|
|
}
|
|
|
|
return zones.slice().reverse();
|
|
}
|
|
|
|
|
|
/* -------------------------------- Circle zones -------------------------------- */
|
|
|
|
const defaultCircleSettings = { min: null, max: null, reductionCount: 4, duration: 1 };
|
|
|
|
function circleZone(center, radius, duration) {
|
|
return {
|
|
center: center,
|
|
radius: radius,
|
|
duration: duration,
|
|
|
|
isInZone(location) {
|
|
return haversine_distance(center, location) < this.radius;
|
|
}
|
|
}
|
|
}
|
|
|
|
function circleSettingsToZones(settings) {
|
|
const {min, max, reductionCount, duration} = settings;
|
|
if (haversine_distance(max.center, min.center) > max.radius - min.radius) {
|
|
return null;
|
|
}
|
|
const zones = [circleZone(max.center, max.radius, duration)];
|
|
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
|
|
let center = max.center;
|
|
let radius = max.radius;
|
|
for (let i = 1; i < reductionCount; i++) {
|
|
radius -= radiusReductionLength;
|
|
let new_center = null;
|
|
while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) {
|
|
const angle = Math.random() * 2 * Math.PI;
|
|
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
|
|
const lat0Rad = center.lat * Math.PI / 180;
|
|
const lon0Rad = center.lng * Math.PI / 180;
|
|
const latRad = Math.asin(
|
|
Math.sin(lat0Rad) * Math.cos(angularDistance) +
|
|
Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle)
|
|
);
|
|
|
|
const lonRad = lon0Rad + Math.atan2(
|
|
Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad),
|
|
Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad)
|
|
);
|
|
new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI};
|
|
}
|
|
center = new_center;
|
|
zones.push(circleZone(center, radius, duration))
|
|
}
|
|
zones.push(circleZone(min.center, min.radius, 0));
|
|
return zones;
|
|
}
|
|
|
|
|
|
/* -------------------------------- Zone manager -------------------------------- */
|
|
|
|
export default {
|
|
isRunning: false,
|
|
zones: [], // A zone has to be connected space that doesn't contain an earth pole
|
|
currentZone: { id: 0, timeoutId: null, endDate: null },
|
|
zoneType: "polygon",
|
|
settings: defaultPolygonSettings,
|
|
settingsToZones: polygonSettingsToZones,
|
|
|
|
start() {
|
|
this.isRunning = true;
|
|
this.currentZone.id = -1;
|
|
this.goNextZone();
|
|
},
|
|
|
|
stop() {
|
|
this.isRunning = false;
|
|
clearTimeout(this.currentZone.timeoutId);
|
|
},
|
|
|
|
goNextZone() {
|
|
this.currentZone.id++;
|
|
if (this.currentZone.id >= this.zones.length - 1) {
|
|
this.currentZone.endDate = Date.now();
|
|
} else {
|
|
this.currentZone.timeoutId = setTimeout(() => this.goNextZone(), this.getCurrentZone().duration * 60 * 1000);
|
|
this.currentZone.endDate = Date.now() + this.getCurrentZone().duration * 60 * 1000;
|
|
}
|
|
this.zoneBroadcast();
|
|
},
|
|
|
|
getCurrentZone() {
|
|
return this.zones[this.currentZone.id];
|
|
},
|
|
|
|
getNextZone() {
|
|
if (this.currentZone.id + 1 < this.zones.length) {
|
|
return this.zones[this.currentZone.id + 1];
|
|
} else {
|
|
return this.zones[this.currentZone.id];
|
|
}
|
|
},
|
|
|
|
isInZone(location) {
|
|
if (this.zones.length == 0) {
|
|
return true;
|
|
} else {
|
|
return this.getCurrentZone().isInZone(location);
|
|
}
|
|
},
|
|
|
|
changeSettings(settings) {
|
|
const zones = this.settingsToZones(settings);
|
|
if (!zones) return false;
|
|
this.zones = zones;
|
|
this.settings = settings;
|
|
this.zoneBroadcast();
|
|
return true;
|
|
},
|
|
|
|
changeZoneType(type) {
|
|
if (this.zoneType == type) return;
|
|
if (type == "circle") {
|
|
this.zoneType = "circle";
|
|
this.settings = defaultCircleSettings;
|
|
this.settingsToZones = circleSettingsToZones;
|
|
} else if (type == "polygon") {
|
|
this.zoneType = "polygon";
|
|
this.settings = defaultPolygonSettings;
|
|
this.settingsToZones = polygonSettingsToZones;
|
|
}
|
|
},
|
|
|
|
zoneBroadcast() {
|
|
const zone = {
|
|
begin: this.getCurrentZone(),
|
|
end: this.getNextZone(),
|
|
endDate:this.currentZone.endDate,
|
|
};
|
|
playersBroadcast("current_zone", zone);
|
|
secureAdminBroadcast("current_zone", zone);
|
|
},
|
|
}
|