mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: added laser code change, target move, and delete
Note: deleted lasers are not removed from table and keep being drawn. Also added a cooler looking server page
This commit is contained in:
parent
cc902aec04
commit
9525982161
@ -432,10 +432,10 @@ private:
|
||||
};
|
||||
|
||||
/* Shine a laser with a specific code */
|
||||
class Laser : public Command
|
||||
class FireLaser : public Command
|
||||
{
|
||||
public:
|
||||
Laser(unsigned int ID, unsigned int code, Coords destination, function<void(void)> callback = []() {}) :
|
||||
FireLaser(unsigned int ID, unsigned int code, Coords destination, function<void(void)> callback = []() {}) :
|
||||
Command(callback),
|
||||
ID(ID),
|
||||
destination(destination),
|
||||
@ -453,10 +453,10 @@ private:
|
||||
};
|
||||
|
||||
/* Shine a infrared light */
|
||||
class Infrared : public Command
|
||||
class FireInfrared : public Command
|
||||
{
|
||||
public:
|
||||
Infrared(unsigned int ID, Coords destination, function<void(void)> callback = []() {}) :
|
||||
FireInfrared(unsigned int ID, Coords destination, function<void(void)> callback = []() {}) :
|
||||
Command(callback),
|
||||
ID(ID),
|
||||
destination(destination)
|
||||
@ -470,3 +470,58 @@ private:
|
||||
const unsigned int ID;
|
||||
const Coords destination;
|
||||
};
|
||||
|
||||
/* Change a laser code */
|
||||
class SetLaserCode : public Command
|
||||
{
|
||||
public:
|
||||
SetLaserCode(unsigned int spotID, unsigned int code, function<void(void)> callback = []() {}) :
|
||||
Command(callback),
|
||||
spotID(spotID),
|
||||
code(code)
|
||||
{
|
||||
priority = CommandPriority::LOW;
|
||||
};
|
||||
virtual string getString();
|
||||
virtual unsigned int getLoad() { return 5; }
|
||||
|
||||
private:
|
||||
const unsigned int spotID;
|
||||
const unsigned int code;
|
||||
};
|
||||
|
||||
/* Delete a spot code */
|
||||
class DeleteSpot : public Command
|
||||
{
|
||||
public:
|
||||
DeleteSpot(unsigned int spotID, function<void(void)> callback = []() {}) :
|
||||
Command(callback),
|
||||
spotID(spotID)
|
||||
{
|
||||
priority = CommandPriority::LOW;
|
||||
};
|
||||
virtual string getString();
|
||||
virtual unsigned int getLoad() { return 5; }
|
||||
|
||||
private:
|
||||
const unsigned int spotID;
|
||||
};
|
||||
|
||||
/* Move spot to a new target */
|
||||
class MoveSpot : public Command
|
||||
{
|
||||
public:
|
||||
MoveSpot(unsigned int spotID, Coords destination, function<void(void)> callback = []() {}) :
|
||||
Command(callback),
|
||||
spotID(spotID),
|
||||
destination(destination)
|
||||
{
|
||||
priority = CommandPriority::LOW;
|
||||
};
|
||||
virtual string getString();
|
||||
virtual unsigned int getLoad() { return 5; }
|
||||
|
||||
private:
|
||||
const unsigned int spotID;
|
||||
const Coords destination;
|
||||
};
|
||||
@ -259,8 +259,8 @@ string Explosion::getString()
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
/* Laser command */
|
||||
string Laser::getString()
|
||||
/* FireLaser command */
|
||||
string FireLaser::getString()
|
||||
{
|
||||
std::ostringstream commandSS;
|
||||
commandSS.precision(10);
|
||||
@ -272,8 +272,8 @@ string Laser::getString()
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
/* Infrared command */
|
||||
string Infrared::getString()
|
||||
/* FireInfrared command */
|
||||
string FireInfrared::getString()
|
||||
{
|
||||
std::ostringstream commandSS;
|
||||
commandSS.precision(10);
|
||||
@ -282,4 +282,37 @@ string Infrared::getString()
|
||||
<< destination.lat << ", "
|
||||
<< destination.lng;
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
/* SetLaserCode command */
|
||||
string SetLaserCode::getString()
|
||||
{
|
||||
std::ostringstream commandSS;
|
||||
commandSS.precision(10);
|
||||
commandSS << "Olympus.setLaserCode, "
|
||||
<< spotID << ", "
|
||||
<< code;
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
/* MoveSpot command */
|
||||
string MoveSpot::getString()
|
||||
{
|
||||
std::ostringstream commandSS;
|
||||
commandSS.precision(10);
|
||||
commandSS << "Olympus.moveSpot, "
|
||||
<< spotID << ", "
|
||||
<< destination.lat << ", "
|
||||
<< destination.lng;
|
||||
return commandSS.str();
|
||||
}
|
||||
|
||||
/* DeleteSpot command */
|
||||
string DeleteSpot::getString()
|
||||
{
|
||||
std::ostringstream commandSS;
|
||||
commandSS.precision(10);
|
||||
commandSS << "Olympus.deleteSpot, "
|
||||
<< spotID;
|
||||
return commandSS.str();
|
||||
}
|
||||
@ -705,9 +705,9 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
Coords loc; loc.lat = lat; loc.lng = lng;
|
||||
unsigned int code = value[L"code"].as_integer();
|
||||
|
||||
log("Adding laser with code " + to_string(code) + " from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
|
||||
log("Firing laser with code " + to_string(code) + " from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
|
||||
|
||||
command = dynamic_cast<Command*>(new Laser(ID, code, loc));
|
||||
command = dynamic_cast<Command*>(new FireLaser(ID, code, loc));
|
||||
}
|
||||
}
|
||||
/************************/
|
||||
@ -719,13 +719,41 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
double lat = value[L"location"][L"lat"].as_double();
|
||||
double lng = value[L"location"][L"lng"].as_double();
|
||||
Coords loc; loc.lat = lat; loc.lng = lng;
|
||||
|
||||
log("Adding infrared from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
|
||||
|
||||
command = dynamic_cast<Command*>(new Infrared(ID, loc));
|
||||
log("Firing infrared from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
|
||||
|
||||
command = dynamic_cast<Command*>(new FireInfrared(ID, loc));
|
||||
}
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("setLaserCode") == 0)
|
||||
{
|
||||
unsigned int spotID = value[L"spotID"].as_integer();
|
||||
unsigned int code = value[L"code"].as_integer();
|
||||
|
||||
log("Setting laser code " + to_string(code) + " to spot with ID " + to_string(spotID));
|
||||
command = dynamic_cast<Command*>(new SetLaserCode(spotID, code));
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("moveSpot") == 0)
|
||||
{
|
||||
unsigned int spotID = value[L"spotID"].as_integer();
|
||||
|
||||
double lat = value[L"location"][L"lat"].as_double();
|
||||
double lng = value[L"location"][L"lng"].as_double();
|
||||
Coords loc; loc.lat = lat; loc.lng = lng;
|
||||
|
||||
log("Moving spot with ID " + to_string(spotID) + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
|
||||
command = dynamic_cast<Command*>(new MoveSpot(spotID, loc));
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("deleteSpot") == 0)
|
||||
{
|
||||
unsigned int spotID = value[L"spotID"].as_integer();
|
||||
log("Deleting spot with ID " + to_string(spotID));
|
||||
command = dynamic_cast<Command*>(new DeleteSpot(spotID));
|
||||
}
|
||||
/************************/
|
||||
else if (key.compare("setCommandModeOptions") == 0)
|
||||
{
|
||||
setCommandModeOptions(value);
|
||||
|
||||
@ -9,17 +9,32 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
"chart.js": "^4.4.7",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-clock": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@tanem/svg-injector": "^10.1.68",
|
||||
"@turf/clusters": "^7.1.0",
|
||||
"@turf/turf": "^6.5.0",
|
||||
"@types/dom-webcodecs": "^0.1.12",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-leaflet": "^3.0.0",
|
||||
"@types/turf": "^3.5.32",
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"buffer": "^6.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
@ -28,26 +43,6 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"eslint-plugin-readable-tailwind": "^1.5.2",
|
||||
"globals": "^15.7.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript-eslint": "^7.14.1",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-externals": "^0.6.2",
|
||||
"vite-plugin-file": "^1.0.5",
|
||||
"web-audio-peak-meter": "^3.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@tanem/svg-injector": "^10.1.68",
|
||||
"@turf/turf": "^6.5.0",
|
||||
"@types/dom-webcodecs": "^0.1.12",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/react-leaflet": "^3.0.0",
|
||||
"@types/turf": "^3.5.32",
|
||||
"buffer": "^6.0.3",
|
||||
"js-sha256": "^0.11.0",
|
||||
"jsstore": "^4.8.2",
|
||||
"leaflet": "^1.9.4",
|
||||
@ -55,11 +50,19 @@
|
||||
"leaflet-path-drag": "^1.9.5",
|
||||
"magvar": "^1.1.5",
|
||||
"opus-decoder": "^0.7.6",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"turf": "^3.0.14",
|
||||
"usng": "^0.3.0"
|
||||
"typescript-eslint": "^7.14.1",
|
||||
"usng": "^0.3.0",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-externals": "^0.6.2",
|
||||
"vite-plugin-file": "^1.0.5",
|
||||
"web-audio-peak-meter": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -496,6 +496,7 @@ export const DELETE_CYCLE_TIME = 0.05;
|
||||
export const DELETE_SLOW_THRESHOLD = 50;
|
||||
|
||||
export const GROUPING_ZOOM_TRANSITION = 13;
|
||||
export const SPOTS_EDIT_ZOOM_TRANSITION = 14;
|
||||
|
||||
export const MAX_SHOTS_SCATTER = 3;
|
||||
export const MAX_SHOTS_INTENSITY = 3;
|
||||
|
||||
@ -69,7 +69,7 @@ import { SmokeMarker } from "./markers/smokemarker";
|
||||
/* Register the handler for the box selection */
|
||||
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
|
||||
|
||||
initDraggablePath(L); // TODO: breaks app when compiled
|
||||
initDraggablePath(L);
|
||||
|
||||
export class Map extends L.Map {
|
||||
/* Options */
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import { DivIcon, LatLng } from "leaflet";
|
||||
import { CustomMarker } from "./custommarker";
|
||||
|
||||
export class DestinationPreviewHandle extends CustomMarker {
|
||||
constructor(latlng: LatLng) {
|
||||
super(latlng, { interactive: true, draggable: true });
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
this.setIcon(
|
||||
new DivIcon({
|
||||
iconSize: [18, 18],
|
||||
iconAnchor: [9, 9],
|
||||
className: "leaflet-destination-preview-handle-marker",
|
||||
})
|
||||
);
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("ol-destination-preview-handle-icon");
|
||||
this.getElement()?.appendChild(el);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Marker, LatLng, DivIcon, DomEvent, Map } from "leaflet";
|
||||
import { Marker, LatLng, DivIcon, Map } from "leaflet";
|
||||
|
||||
export class SpotEditMarker extends Marker {
|
||||
#textValue: string;
|
||||
@ -6,14 +6,28 @@ export class SpotEditMarker extends Marker {
|
||||
#rotationAngle: number; // Rotation angle in radians
|
||||
#previousValue: string;
|
||||
|
||||
constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0) {
|
||||
onValueUpdated: (value: number) => void = () => {};
|
||||
onDeleteButtonClicked: () => void = () => {};
|
||||
|
||||
/**
|
||||
* Constructor for SpotEditMarker
|
||||
* @param {LatLng} latlng - The geographical position of the marker.
|
||||
* @param {string} textValue - The initial text value to display.
|
||||
* @param {number} rotationAngle - The initial rotation angle in radians.
|
||||
*/
|
||||
constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0, type: string) {
|
||||
super(latlng, {
|
||||
icon: new DivIcon({
|
||||
className: "leaflet-spot-input-marker",
|
||||
html: `<div class="container">
|
||||
html:
|
||||
type === "laser"
|
||||
? `<div class="container">
|
||||
<input class="input"/>
|
||||
<div class="text">${textValue}</div>
|
||||
<div class="delete">X</div>
|
||||
</div>`
|
||||
: `<div class="container">
|
||||
<div class="delete">X</div>
|
||||
</div>`,
|
||||
}),
|
||||
});
|
||||
@ -23,21 +37,34 @@ export class SpotEditMarker extends Marker {
|
||||
this.#previousValue = textValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the marker is added to the map.
|
||||
* @param {Map} map - The map instance.
|
||||
* @returns {this} - The current instance of SpotEditMarker.
|
||||
*/
|
||||
onAdd(map: Map): this {
|
||||
super.onAdd(map);
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const text = element.querySelector(".text");
|
||||
const button = element.querySelector(".delete");
|
||||
const container = element.querySelector(".container") as HTMLDivElement;
|
||||
const button = element.querySelector(".delete") as HTMLDivElement;
|
||||
const input = element.querySelector(".input") as HTMLInputElement;
|
||||
|
||||
// Add click event listener to toggle edit mode
|
||||
text?.addEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
|
||||
text?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
text?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
container?.addEventListener("mousedown", (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#setEditMode(ev, !this.#isEditable);
|
||||
});
|
||||
container?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
container?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
|
||||
// Add click event listener to delete spot
|
||||
button?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
|
||||
button?.addEventListener("mousedown", (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#onButtonClicked(ev);
|
||||
});
|
||||
button?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
button?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
|
||||
@ -45,46 +72,62 @@ export class SpotEditMarker extends Marker {
|
||||
input?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.addEventListener("blur", (ev) => this.#toggleEditMode(ev));
|
||||
input?.addEventListener("keydown", (ev) => this.#acceptInput(ev));
|
||||
input?.addEventListener("blur", (ev) => this.#setEditMode(ev, false));
|
||||
input?.addEventListener("keydown", (ev) => this.#onKeyDown(ev));
|
||||
input?.addEventListener("input", (ev) => this.#validateInput(ev));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the marker is removed from the map.
|
||||
* @param {Map} map - The map instance.
|
||||
* @returns {this} - The current instance of SpotEditMarker.
|
||||
*/
|
||||
onRemove(map: Map): this {
|
||||
super.onRemove(map);
|
||||
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const text = element.querySelector(".text");
|
||||
const button = element.querySelector(".delete");
|
||||
const input = element.querySelector(".input");
|
||||
const container = element.querySelector(".container") as HTMLDivElement;
|
||||
const button = element.querySelector(".delete") as HTMLDivElement;
|
||||
const input = element.querySelector(".input") as HTMLInputElement;
|
||||
|
||||
// Add click event listener to toggle edit mode
|
||||
text?.removeEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
|
||||
text?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
text?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
// Remove click event listener to toggle edit mode
|
||||
container?.removeEventListener("mousedown", (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#setEditMode(ev, !this.#isEditable);
|
||||
});
|
||||
container?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
container?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
|
||||
// Add click event listener to delete spot
|
||||
button?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
|
||||
// Remove click event listener to delete spot
|
||||
button?.removeEventListener("mousedown", (ev) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#onButtonClicked(ev);
|
||||
});
|
||||
button?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
button?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
|
||||
// Add click event listener to input spot
|
||||
// Remove click event listener to input spot
|
||||
input?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
|
||||
input?.removeEventListener("blur", (ev) => this.#toggleEditMode(ev));
|
||||
input?.removeEventListener("keydown", (ev) => this.#acceptInput(ev));
|
||||
input?.removeEventListener("blur", (ev) => this.#setEditMode(ev, false));
|
||||
input?.removeEventListener("keydown", (ev) => this.#onKeyDown(ev));
|
||||
input?.removeEventListener("input", (ev) => this.#validateInput(ev));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// Method to set the text value
|
||||
/**
|
||||
* Sets the text value of the marker.
|
||||
* @param {string} textValue - The new text value.
|
||||
*/
|
||||
setTextValue(textValue: string) {
|
||||
this.#textValue = textValue;
|
||||
const element = this.getElement();
|
||||
@ -94,12 +137,18 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the text value
|
||||
/**
|
||||
* Gets the text value of the marker.
|
||||
* @returns {string} - The current text value.
|
||||
*/
|
||||
getTextValue() {
|
||||
return this.#textValue;
|
||||
}
|
||||
|
||||
// Method to set the rotation angle in radians
|
||||
/**
|
||||
* Sets the rotation angle of the marker in radians.
|
||||
* @param {number} angle - The new rotation angle in radians.
|
||||
*/
|
||||
setRotationAngle(angle: number) {
|
||||
this.#rotationAngle = angle;
|
||||
if (!this.#isEditable) {
|
||||
@ -107,12 +156,17 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
// Method to get the rotation angle in radians
|
||||
/**
|
||||
* Gets the rotation angle of the marker in radians.
|
||||
* @returns {number} - The current rotation angle in radians.
|
||||
*/
|
||||
getRotationAngle() {
|
||||
return this.#rotationAngle;
|
||||
}
|
||||
|
||||
// Method to update the rotation angle to ensure the text is always readable
|
||||
/**
|
||||
* Updates the rotation angle to ensure the text is always readable.
|
||||
*/
|
||||
#updateRotation() {
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
@ -129,11 +183,12 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
#toggleEditMode(ev) {
|
||||
this.#isEditable = !this.#isEditable;
|
||||
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
/**
|
||||
* Toggles the edit mode of the marker.
|
||||
* @param {Event} ev - The event object.
|
||||
*/
|
||||
#setEditMode(ev: Event, editable: boolean) {
|
||||
this.#isEditable = editable;
|
||||
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
@ -167,23 +222,43 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
#stopEventPropagation(ev) {
|
||||
/**
|
||||
* Stops the event propagation.
|
||||
* @param {Event} ev - The event object.
|
||||
*/
|
||||
#stopEventPropagation(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
#validateInput(ev) {
|
||||
/**
|
||||
* Validates the input value.
|
||||
* @param {Event} ev - The event object.
|
||||
*/
|
||||
#validateInput(ev: Event) {
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const input = ev.target as HTMLInputElement;
|
||||
const text = element.querySelector(".text");
|
||||
|
||||
// Validate the input value
|
||||
// Validate the input value (must be a valid laser code)
|
||||
const value = input.value;
|
||||
|
||||
// Check if the value is a partial valid input
|
||||
// Conditions for partial validity:
|
||||
// 1. The first digit is 1.
|
||||
// 2. The first digit is 1 and the second digit is between 1 and 7.
|
||||
// 3. The first digit is 1, the second digit is between 1 and 7, and the third digit is between 1 and 8.
|
||||
// 4. The first digit is 1, the second digit is between 1 and 7, the third digit is between 1 and 8, and the fourth digit is between 1 and 8.
|
||||
const isPartialValid = /^[1]$|^[1][1-7]$|^[1][1-7][1-8]$|^[1][1-7][1-8][1-8]$/.test(value);
|
||||
|
||||
// Check if the value is a complete valid input
|
||||
// Conditions for complete validity:
|
||||
// 1. The input is a four-digit number where:
|
||||
// - The first digit is 1.
|
||||
// - The second digit is between 1 and 7.
|
||||
// - The third digit is between 1 and 8.
|
||||
// - The fourth digit is between 1 and 8.
|
||||
// 2. The number is between 1111 and 1788.
|
||||
const isValid = /^[1][1-7][1-8][1-8]$/.test(value) && Number(value) <= 1788;
|
||||
|
||||
if (isPartialValid || isValid) {
|
||||
@ -197,6 +272,9 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles invalid input by causing a small red flash around the input element.
|
||||
*/
|
||||
#errorFunction() {
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
@ -210,7 +288,59 @@ export class SpotEditMarker extends Marker {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the key down event.
|
||||
* @param {Event} ev - The keyboard event object.
|
||||
*/
|
||||
#onKeyDown(ev) {
|
||||
if (ev.key === "Enter") this.#acceptInput(ev);
|
||||
else if (ev.key === "Escape") this.#setEditMode(ev, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts the input value when the Enter key is pressed.
|
||||
* @param {Event} ev - The keyboard event object.
|
||||
*/
|
||||
#acceptInput(ev) {
|
||||
if (ev.key === "Enter") this.#toggleEditMode(ev);
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const input = element.querySelector(".input") as HTMLInputElement;
|
||||
if (input) {
|
||||
// Validate the input value (must be a valid laser code)
|
||||
const value = Number(input.value);
|
||||
if (value >= 1111 && value <= 1788) {
|
||||
this.#setEditMode(ev, false);
|
||||
this.onValueUpdated(value);
|
||||
this.#successFunction();
|
||||
} else {
|
||||
this.#errorFunction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the button click event.
|
||||
* @param {Event} ev - The event object.
|
||||
*/
|
||||
#onButtonClicked(ev: Event) {
|
||||
if (this.#isEditable) this.#acceptInput(ev);
|
||||
else this.onDeleteButtonClicked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles valid input by causing a green flash around the container.
|
||||
*/
|
||||
#successFunction() {
|
||||
const element = this.getElement();
|
||||
if (element) {
|
||||
const container = element.querySelector(".container") as HTMLDivElement;
|
||||
if (container) {
|
||||
container.classList.add("success-flash");
|
||||
setTimeout(() => {
|
||||
container.classList.remove("success-flash");
|
||||
}, 900); // Duration of the flash effect (3 flashes, 300ms each)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,8 @@ import { CustomMarker } from "./custommarker";
|
||||
export class SpotMarker extends CustomMarker {
|
||||
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
|
||||
super(latlng, options);
|
||||
this.options.interactive = false;
|
||||
this.options.interactive = true;
|
||||
this.options.draggable = true;
|
||||
this.setZIndexOffset(9999);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
/* Container for the spot input marker */
|
||||
.leaflet-spot-input-marker {
|
||||
text-align: center;
|
||||
display: flex !important;
|
||||
@ -5,6 +6,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Delete button styles */
|
||||
.leaflet-spot-input-marker .delete {
|
||||
background-color: darkred;
|
||||
color: #fffd;
|
||||
@ -17,10 +19,12 @@
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
/* Delete button hover effect */
|
||||
.leaflet-spot-input-marker .delete:hover {
|
||||
background-color: lightcoral;
|
||||
}
|
||||
|
||||
/* Container for the spot input marker content */
|
||||
.leaflet-spot-input-marker .container {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
@ -31,14 +35,17 @@
|
||||
font-size: 13px;
|
||||
column-gap: 6px;
|
||||
align-content: center;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
/* Text inside the spot input marker */
|
||||
.leaflet-spot-input-marker .text {
|
||||
margin-left: 12px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
/* Input field inside the spot input marker */
|
||||
.leaflet-spot-input-marker .input {
|
||||
display: none;
|
||||
color: white;
|
||||
@ -50,25 +57,32 @@
|
||||
outline: none; /* Remove default outline */
|
||||
font-size: 13px;
|
||||
width: 80px;
|
||||
border-top-left-radius: 999px;
|
||||
border-bottom-left-radius: 999px;
|
||||
}
|
||||
|
||||
/* Input field focus effect */
|
||||
.leaflet-spot-input-marker .input:focus {
|
||||
border-color: #777; /* Border color on focus */
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Shadow on focus */
|
||||
}
|
||||
|
||||
/* Hide input field inside the container */
|
||||
.leaflet-spot-input-marker .container .input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide text in edit mode */
|
||||
.leaflet-spot-input-marker .container .text.edit-mode {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Error flash animation for input field */
|
||||
.leaflet-spot-input-marker .input.error-flash {
|
||||
animation: error-flash 0.3s;
|
||||
}
|
||||
|
||||
/* Keyframes for error flash animation */
|
||||
@keyframes error-flash {
|
||||
0% {
|
||||
border-color: #555;
|
||||
@ -81,10 +95,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit mode styles for delete button */
|
||||
.leaflet-spot-input-marker .delete.edit-mode {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
/* Edit mode hover effect for delete button */
|
||||
.leaflet-spot-input-marker .delete.edit-mode:hover {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
/* Success flash animation for container */
|
||||
.leaflet-spot-input-marker .container.success-flash {
|
||||
animation: success-flash 0.3s 3;
|
||||
}
|
||||
|
||||
/* Keyframes for success flash animation */
|
||||
@keyframes success-flash {
|
||||
0%, 100% {
|
||||
border-color: transparent;
|
||||
}
|
||||
33%, 66% {
|
||||
border-color: green;
|
||||
}
|
||||
}
|
||||
@ -70,11 +70,26 @@ export class MissionManager {
|
||||
updateSpots(data: SpotsData) {
|
||||
for (let idx in data.spots) {
|
||||
const spotID = Number(idx);
|
||||
const spot = data.spots[idx];
|
||||
if (this.#spots[spotID] === undefined) {
|
||||
const spot = data.spots[idx];
|
||||
this.#spots[spotID] = new Spot(spotID, spot.type, new LatLng(spot.targetPosition.lat, spot.targetPosition.lng), spot.sourceUnitID, spot.code);
|
||||
} else {
|
||||
if (spot.type === "laser")
|
||||
this.#spots[spotID].setCode(spot.code ?? 0)
|
||||
this.#spots[spotID].setTargetPosition( new LatLng(spot.targetPosition.lat, spot.targetPosition.lng));
|
||||
}
|
||||
}
|
||||
|
||||
/* Iterate the existing spots and remove all spots that where deleted */
|
||||
for (let idx in this.#spots) {
|
||||
if (data.spots[idx] === undefined) {
|
||||
delete this.#spots[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSpotByID(spotID: number) {
|
||||
return this.#spots[spotID];
|
||||
}
|
||||
|
||||
/** Update airbase information
|
||||
|
||||
@ -179,7 +179,7 @@ export class OlympusApp {
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const server = urlParams.get("server");
|
||||
if (server == null) {
|
||||
if (!server) {
|
||||
this.setState(OlympusState.IDLE);
|
||||
/* If no profile exists already with that name, create it from scratch from the defaults */
|
||||
if (this.getProfile() === null) this.saveProfile();
|
||||
@ -342,20 +342,20 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
startServerMode() {
|
||||
ConfigLoadedEvent.on((config) => {
|
||||
this.getAudioManager().start();
|
||||
|
||||
Object.values(config.controllers).forEach((controllerOptions) => {
|
||||
if (controllerOptions.type.toLowerCase() === "awacs") {
|
||||
this.getControllerManager().addController(
|
||||
new AWACSController(
|
||||
{ frequency: controllerOptions.frequency, modulation: controllerOptions.modulation },
|
||||
controllerOptions.coalition,
|
||||
controllerOptions.callsign
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
//ConfigLoadedEvent.on((config) => {
|
||||
// this.getAudioManager().start();
|
||||
//
|
||||
// Object.values(config.controllers).forEach((controllerOptions) => {
|
||||
// if (controllerOptions.type.toLowerCase() === "awacs") {
|
||||
// this.getControllerManager().addController(
|
||||
// new AWACSController(
|
||||
// { frequency: controllerOptions.frequency, modulation: controllerOptions.modulation },
|
||||
// controllerOptions.coalition,
|
||||
// controllerOptions.callsign
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
//});
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,18 @@ import {
|
||||
emissionsCountermeasures,
|
||||
reactionsToThreat,
|
||||
} from "../constants/constants";
|
||||
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, SpotsData, TACAN } from "../interfaces";
|
||||
import {
|
||||
AirbasesData,
|
||||
BullseyesData,
|
||||
CommandModeOptions,
|
||||
GeneralSettings,
|
||||
MissionData,
|
||||
Radio,
|
||||
ServerRequestOptions,
|
||||
ServerStatus,
|
||||
SpotsData,
|
||||
TACAN,
|
||||
} from "../interfaces";
|
||||
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent, WrongCredentialsEvent } from "../events";
|
||||
|
||||
export class ServerManager {
|
||||
@ -40,13 +51,15 @@ export class ServerManager {
|
||||
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
|
||||
this.#lastUpdateTimes[MISSION_URI] = Date.now();
|
||||
|
||||
getApp().getShortcutManager().addShortcut("togglePause", {
|
||||
label: "Pause data update",
|
||||
keyUpCallback: () => {
|
||||
this.setPaused(!this.getPaused());
|
||||
},
|
||||
code: "Enter"
|
||||
})
|
||||
getApp()
|
||||
.getShortcutManager()
|
||||
.addShortcut("togglePause", {
|
||||
label: "Pause data update",
|
||||
keyUpCallback: () => {
|
||||
this.setPaused(!this.getPaused());
|
||||
},
|
||||
code: "Enter",
|
||||
});
|
||||
|
||||
MapOptionsChangedEvent.on((mapOptions) => {
|
||||
/* TODO if (this.#updateMode === "normal" && mapOptions.AWACSMode) {
|
||||
@ -56,7 +69,7 @@ export class ServerManager {
|
||||
this.#updateMode = "normal";
|
||||
this.startUpdate();
|
||||
} */
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setUsername(newUsername: string) {
|
||||
@ -117,8 +130,10 @@ export class ServerManager {
|
||||
if (xmlHttp.responseType == "arraybuffer") this.#lastUpdateTimes[uri] = callback(xmlHttp.response);
|
||||
else {
|
||||
/* Check if the response headers contain the enabled command modes and set them */
|
||||
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
|
||||
getApp().getMissionManager().setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ??[])
|
||||
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
|
||||
getApp()
|
||||
.getMissionManager()
|
||||
.setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ?? []);
|
||||
|
||||
const result = JSON.parse(xmlHttp.responseText);
|
||||
this.#lastUpdateTimes[uri] = callback(result);
|
||||
@ -534,14 +549,29 @@ export class ServerManager {
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
setCommandModeOptions(
|
||||
commandModeOptions: CommandModeOptions,
|
||||
callback: CallableFunction = () => {}
|
||||
) {
|
||||
setCommandModeOptions(commandModeOptions: CommandModeOptions, callback: CallableFunction = () => {}) {
|
||||
var data = { setCommandModeOptions: commandModeOptions };
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
setLaserCode(spotID: number, code: number, callback: CallableFunction = () => {}) {
|
||||
var command = { spotID: spotID, code: code };
|
||||
var data = { setLaserCode: command };
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
moveSpot(spotID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
|
||||
var command = { spotID: spotID, location: latlng };
|
||||
var data = { moveSpot: command };
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
deleteSpot(spotID: number, callback: CallableFunction = () => {}) {
|
||||
var command = { spotID: spotID };
|
||||
var data = { deleteSpot: command };
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
reloadDatabases(callback: CallableFunction = () => {}) {
|
||||
var data = { reloadDatabases: {} };
|
||||
this.PUT(data, callback);
|
||||
@ -615,41 +645,44 @@ export class ServerManager {
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
window.setInterval(() => {
|
||||
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
|
||||
this.getUnits((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getUnitsManager()?.update(buffer, false);
|
||||
return time;
|
||||
}, false);
|
||||
}
|
||||
}, this.#updateMode === "normal"? 250: 2000)
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
window.setInterval(() => {
|
||||
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
|
||||
this.getWeapons((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getWeaponsManager()?.update(buffer, false);
|
||||
return time;
|
||||
}, false);
|
||||
}
|
||||
}, this.#updateMode === "normal"? 250: 2000)
|
||||
window.setInterval(
|
||||
() => {
|
||||
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
|
||||
this.getUnits((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getUnitsManager()?.update(buffer, false);
|
||||
return time;
|
||||
}, false);
|
||||
}
|
||||
},
|
||||
this.#updateMode === "normal" ? 250 : 2000
|
||||
)
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
window.setInterval(
|
||||
() => {
|
||||
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
|
||||
this.getUnits((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getUnitsManager()?.update(buffer, true);
|
||||
this.getWeapons((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getWeaponsManager()?.update(buffer, false);
|
||||
return time;
|
||||
}, true);
|
||||
}, false);
|
||||
}
|
||||
},
|
||||
5000
|
||||
this.#updateMode === "normal" ? 250 : 2000
|
||||
)
|
||||
);
|
||||
|
||||
this.#intervals.push(
|
||||
window.setInterval(() => {
|
||||
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
|
||||
this.getUnits((buffer: ArrayBuffer) => {
|
||||
var time = getApp().getUnitsManager()?.update(buffer, true);
|
||||
return time;
|
||||
}, true);
|
||||
}
|
||||
}, 5000)
|
||||
);
|
||||
|
||||
// Mission clock and elapsed time
|
||||
this.#intervals.push(
|
||||
window.setInterval(() => {
|
||||
|
||||
@ -4,12 +4,26 @@ import { ServerStatus } from "../interfaces";
|
||||
import { FaCheck, FaXmark } from "react-icons/fa6";
|
||||
import { zeroAppend } from "../other/utils";
|
||||
import { colors } from "../constants/constants";
|
||||
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
|
||||
import "react-circular-progressbar/dist/styles.css";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { Chart, LineElement, LinearScale, Title, CategoryScale, Legend, PointElement } from "chart.js";
|
||||
|
||||
Chart.register(LineElement, LinearScale, Title, CategoryScale, Legend, PointElement);
|
||||
|
||||
export function ServerOverlay() {
|
||||
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
|
||||
const [loadData, setLoadData] = useState<number[]>([]);
|
||||
const [frameRateData, setFrameRateData] = useState<number[]>([]);
|
||||
const [timeLabels, setTimeLabels] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
ServerStatusUpdatedEvent.on((status) => setServerStatus(status));
|
||||
ServerStatusUpdatedEvent.on((status) => {
|
||||
setServerStatus(status);
|
||||
setLoadData((prevData) => [...prevData, status.load].slice(-300));
|
||||
setFrameRateData((prevData) => [...prevData, status.frameRate].slice(-300));
|
||||
setTimeLabels((prevLabels) => [...prevLabels, new Date().toLocaleTimeString()].slice(-300));
|
||||
});
|
||||
}, []);
|
||||
|
||||
let loadColor = colors.OLYMPUS_GREEN;
|
||||
@ -20,9 +34,9 @@ export function ServerOverlay() {
|
||||
if (serverStatus.frameRate < 30) frameRateColor = colors.OLYMPUS_RED;
|
||||
else if (serverStatus.frameRate >= 30 && serverStatus.frameRate < 60) frameRateColor = colors.OLYMPUS_ORANGE;
|
||||
|
||||
const MThours = serverStatus.missionTime? serverStatus.missionTime.h: 0;
|
||||
const MTminutes = serverStatus.missionTime? serverStatus.missionTime.m: 0;
|
||||
const MTseconds = serverStatus.missionTime? serverStatus.missionTime.s: 0;
|
||||
const MThours = serverStatus.missionTime ? serverStatus.missionTime.h : 0;
|
||||
const MTminutes = serverStatus.missionTime ? serverStatus.missionTime.m : 0;
|
||||
const MTseconds = serverStatus.missionTime ? serverStatus.missionTime.s : 0;
|
||||
|
||||
const EThours = Math.floor((serverStatus.elapsedTime ?? 0) / 3600);
|
||||
const ETminutes = Math.floor((serverStatus.elapsedTime ?? 0) / 60) % 60;
|
||||
@ -31,43 +45,136 @@ export function ServerOverlay() {
|
||||
let MTtimeString = `${zeroAppend(MThours, 2)}:${zeroAppend(MTminutes, 2)}:${zeroAppend(MTseconds, 2)}`;
|
||||
let ETtimeString = `${zeroAppend(EThours, 2)}:${zeroAppend(ETminutes, 2)}:${zeroAppend(ETseconds, 2)}`;
|
||||
|
||||
const missionTime = new Date();
|
||||
missionTime.setHours(MThours);
|
||||
missionTime.setMinutes(MTminutes);
|
||||
missionTime.setSeconds(MTseconds);
|
||||
|
||||
const data = {
|
||||
labels: timeLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Server Load",
|
||||
data: loadData,
|
||||
borderColor: colors.LIGHT_BLUE,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
yAxisID: "y-load", // Specify the y-axis for load
|
||||
},
|
||||
{
|
||||
label: "Server Framerate",
|
||||
data: frameRateData,
|
||||
borderColor: colors.WHITE,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
yAxisID: "y-framerate", // Specify the y-axis for framerate
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
animation: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: "category",
|
||||
labels: timeLabels,
|
||||
},
|
||||
"y-framerate": {
|
||||
type: "linear",
|
||||
position: "left",
|
||||
beginAtZero: true,
|
||||
max: 120, // Max value for framerate
|
||||
},
|
||||
"y-load": {
|
||||
type: "linear",
|
||||
position: "right",
|
||||
beginAtZero: true,
|
||||
max: 2000, // Max value for load
|
||||
grid: {
|
||||
drawOnChartArea: false, // Only want the grid lines for one axis to show up
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: "top",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-0 z-50 h-full w-full flex-col bg-olympus-900 p-5
|
||||
absolute left-0 top-0 z-50 flex h-full w-full flex-col bg-black
|
||||
bg-opacity-80 p-5 backdrop-blur-sm animate-fadeIn
|
||||
`}
|
||||
>
|
||||
<div className="flex-col content-center">
|
||||
<h2 className="mb-10 text-3xl font-bold text-white">DCS Olympus server</h2>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex gap-5 text-white">
|
||||
<div className="w-64">Connected to DCS:</div>
|
||||
<div>{serverStatus.connected? <FaCheck className={`
|
||||
text-xl text-green-500
|
||||
`}/> : <FaXmark className={`text-xl text-red-500`}/>}</div>
|
||||
<div
|
||||
className={`
|
||||
m-auto flex w-3/4 max-w-4xl flex-col items-center rounded-lg bg-white
|
||||
bg-opacity-10 p-5 shadow-lg animate-slideIn
|
||||
`}
|
||||
>
|
||||
<h2 className="mb-5 text-4xl font-bold text-white drop-shadow-lg">DCS Olympus Server</h2>
|
||||
<div className="flex w-full gap-12">
|
||||
<div className="flex w-72 flex-col gap-4 text-lg text-white">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-semibold">Connected to DCS:</div>
|
||||
<div>{serverStatus.connected ? <FaCheck className={`
|
||||
text-2xl text-green-500
|
||||
`} /> : <FaXmark className={`text-2xl text-red-500`} />}</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="font-semibold">Elapsed time:</div>
|
||||
<div>{ETtimeString}</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="font-semibold">Mission local time:</div>
|
||||
<div>{MTtimeString}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-5 text-white">
|
||||
<div className="w-64">Server load:</div>
|
||||
<div style={{color: loadColor}}>{serverStatus.load}</div>
|
||||
</div>
|
||||
<div className="flex gap-5 text-white">
|
||||
<div className="w-64">Server framerate:</div>
|
||||
<div style={{color: frameRateColor}}>{serverStatus.frameRate} fps</div>
|
||||
</div>
|
||||
<div className="flex gap-5 text-white">
|
||||
<div className="w-64">Elapsed time:</div>
|
||||
<div>{ETtimeString}</div>
|
||||
</div>
|
||||
<div className="flex gap-5 text-white">
|
||||
<div className="w-64">Mission local time:</div>
|
||||
<div>{MTtimeString}</div>
|
||||
<div className="flex items-center justify-between gap-5 text-white">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 font-semibold">Load</div>
|
||||
<div className="h-24 w-24">
|
||||
<CircularProgressbar
|
||||
value={serverStatus.load}
|
||||
maxValue={2000}
|
||||
text={`${serverStatus.load}`}
|
||||
styles={buildStyles({
|
||||
textColor: loadColor,
|
||||
pathColor: loadColor,
|
||||
trailColor: "rgba(255, 255, 255, 0.2)",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2 font-semibold">Framerate</div>
|
||||
<div className="h-24 w-24">
|
||||
<CircularProgressbar
|
||||
value={serverStatus.frameRate}
|
||||
maxValue={120}
|
||||
text={`${serverStatus.frameRate} fps`}
|
||||
styles={buildStyles({
|
||||
textColor: frameRateColor,
|
||||
pathColor: frameRateColor,
|
||||
trailColor: "rgba(255, 255, 255, 0.2)",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 w-full flex-1">
|
||||
{/* @ts-ignore */}
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
<img src="images/olympus-500x500.png" className={`
|
||||
absolute right-4 top-4 ml-auto flex h-24
|
||||
`}></img>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -121,4 +121,32 @@ input[type="range"]:focus::-moz-range-thumb {
|
||||
100% {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bouncing-ball {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
animation: bounce 2s infinite;
|
||||
}
|
||||
|
||||
.ball-logo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-150px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-75px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,13 +8,7 @@ import { MainMenu } from "./panels/mainmenu";
|
||||
import { SideBar } from "./panels/sidebar";
|
||||
import { OptionsMenu } from "./panels/optionsmenu";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import {
|
||||
NO_SUBSTATE,
|
||||
OlympusState,
|
||||
OlympusSubState,
|
||||
OptionsSubstate,
|
||||
UnitControlSubState
|
||||
} from "../constants/constants";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
|
||||
@ -30,7 +24,7 @@ import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
|
||||
import { KeybindModal } from "./modals/keybindmodal";
|
||||
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
|
||||
import { JTACMenu } from "./panels/jtacmenu";
|
||||
import { AppStateChangedEvent } from "../events";
|
||||
import { AppStateChangedEvent, ServerStatusUpdatedEvent } from "../events";
|
||||
import { GameMasterMenu } from "./panels/gamemastermenu";
|
||||
import { InfoBar } from "./panels/infobar";
|
||||
import { HotGroupBar } from "./panels/hotgroupsbar";
|
||||
@ -41,6 +35,7 @@ import { AWACSMenu } from "./panels/awacsmenu";
|
||||
import { ServerOverlay } from "./serveroverlay";
|
||||
import { ImportExportModal } from "./modals/importexportmodal";
|
||||
import { WarningModal } from "./modals/warningmodal";
|
||||
import { ServerStatus } from "../interfaces";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
@ -57,12 +52,19 @@ export type OlympusUIState = {
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
|
||||
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
|
||||
const [connectedOnce, setConnectedOnce] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((state, subState) => {
|
||||
setAppState(state);
|
||||
setAppSubState(subState);
|
||||
});
|
||||
ServerStatusUpdatedEvent.on((status) => {
|
||||
// If we connected at least once, record it
|
||||
if (status.connected) setConnectedOnce(true);
|
||||
setServerStatus(status);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,11 +78,7 @@ export function UI() {
|
||||
font-sans
|
||||
`}
|
||||
>
|
||||
{appState !== OlympusState.SERVER && (
|
||||
<>
|
||||
<Header />
|
||||
</>
|
||||
)}
|
||||
{appState !== OlympusState.SERVER && <Header />}
|
||||
<div className="flex h-full w-full flex-row-reverse">
|
||||
{appState === OlympusState.SERVER && <ServerOverlay />}
|
||||
|
||||
@ -138,6 +136,45 @@ export function UI() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!serverStatus.connected && appState !== OlympusState.LOGIN && (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-0 z-50 flex h-screen w-screen items-center
|
||||
justify-center bg-gray-900 bg-opacity-80 text-white backdrop-blur-sm
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-4">
|
||||
<div className="bouncing-ball">
|
||||
<img
|
||||
src="images/olympus-500x500.png"
|
||||
alt="Olympus Logo"
|
||||
className={`ball-logo`}
|
||||
/>
|
||||
</div>
|
||||
{!connectedOnce && <div>Establishing connection</div>}
|
||||
{connectedOnce && <div>Connection lost</div>}
|
||||
{!connectedOnce && <div className="text-gray-400">Trying to connect with the server, please wait...</div>}
|
||||
{connectedOnce && (
|
||||
<div className="text-gray-400">
|
||||
Try reloading this page. However, this usually means that you internet connection is down, or that the server is offline, paused, or loading a
|
||||
new mission.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{appState === OlympusState.NOT_INITIALIZED && (
|
||||
<div
|
||||
className={`
|
||||
absolute left-0 top-0 z-50 flex h-screen w-screen items-center
|
||||
justify-center bg-gray-900 text-white
|
||||
`}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ import {
|
||||
TRAIL_LENGTH,
|
||||
colors,
|
||||
UnitState,
|
||||
SPOTS_EDIT_ZOOM_TRANSITION,
|
||||
} from "../constants/constants";
|
||||
import { DataExtractor } from "../server/dataextractor";
|
||||
import { Weapon } from "../weapon/weapon";
|
||||
@ -71,6 +72,7 @@ import { ArrowMarker } from "../map/markers/arrowmarker";
|
||||
import { Spot } from "../mission/spot";
|
||||
import { SpotEditMarker } from "../map/markers/spoteditmarker";
|
||||
import { SpotMarker } from "../map/markers/spotmarker";
|
||||
import { get } from "http";
|
||||
|
||||
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
|
||||
|
||||
@ -180,7 +182,7 @@ export abstract class Unit extends CustomMarker {
|
||||
#racetrackAnchorMarkers: CoalitionAreaHandle[] = [new CoalitionAreaHandle(new LatLng(0, 0)), new CoalitionAreaHandle(new LatLng(0, 0))];
|
||||
#racetrackArrow: ArrowMarker = new ArrowMarker(new LatLng(0, 0));
|
||||
#inhibitRacetrackDraw: boolean = false;
|
||||
#spots: { [key: number]: Polyline } = {};
|
||||
#spotLines: { [key: number]: Polyline } = {};
|
||||
#spotEditMarkers: { [key: number]: SpotEditMarker } = {};
|
||||
#spotMarkers: { [key: number]: SpotMarker } = {};
|
||||
|
||||
@ -1867,39 +1869,150 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
#drawSpots() {
|
||||
// Iterate over all spots and draw lines, edit markers, and markers
|
||||
Object.values(getApp().getMissionManager().getSpots()).forEach((spot: Spot) => {
|
||||
if (spot.getSourceUnitID() === this.ID) {
|
||||
const spotBearing = deg2rad(bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false));
|
||||
const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition());
|
||||
const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2);
|
||||
if (this.#spots[spot.getID()] === undefined) {
|
||||
this.#spots[spot.getID()] = new Polyline([this.getPosition(), spot.getTargetPosition()], {
|
||||
color: spot.getType() === "laser" ? colors.BLUE_VIOLET : colors.DARK_RED,
|
||||
dashArray: "1, 8",
|
||||
});
|
||||
this.#spots[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotEditMarkers[spot.getID()] = new SpotEditMarker(midPosition, `${spot.getCode() ?? ""}`);
|
||||
this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
|
||||
this.#spotMarkers[spot.getID()] = new SpotMarker(spot.getTargetPosition());
|
||||
} else {
|
||||
if (!getApp().getMap().hasLayer(this.#spots[spot.getID()])) this.#spots[spot.getID()].addTo(getApp().getMap());
|
||||
if (!getApp().getMap().hasLayer(this.#spotEditMarkers[spot.getID()])) this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
if (!getApp().getMap().hasLayer(this.#spotMarkers[spot.getID()])) this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
|
||||
this.#spots[spot.getID()].setLatLngs([this.getPosition(), spot.getTargetPosition()]);
|
||||
this.#spotEditMarkers[spot.getID()].setLatLng(midPosition);
|
||||
this.#spotMarkers[spot.getID()].setLatLng(spot.getTargetPosition());
|
||||
// Draw the spot line
|
||||
this.#drawSpotLine(spot, spotBearing);
|
||||
|
||||
// Draw the spot edit marker if the map is zoomed in enough
|
||||
if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) {
|
||||
// Draw the spot edit marker
|
||||
this.#drawSpotEditMarker(spot, midPosition, spotBearing);
|
||||
}
|
||||
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
|
||||
this.#spotEditMarkers[spot.getID()].setTextValue(`${spot.getCode() ?? ""}`);
|
||||
|
||||
// Draw the spot marker
|
||||
this.#drawSpotMarker(spot);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws or updates the spot line.
|
||||
* @param {Spot} spot - The spot object.
|
||||
* @param {number} spotBearing - The bearing of the spot.
|
||||
*/
|
||||
#drawSpotLine(spot: Spot, spotBearing: number) {
|
||||
if (this.#spotLines[spot.getID()] === undefined) {
|
||||
// Create a new polyline for the spot
|
||||
this.#spotLines[spot.getID()] = new Polyline([this.getPosition(), spot.getTargetPosition()], {
|
||||
color: spot.getType() === "laser" ? colors.BLUE_VIOLET : colors.DARK_RED,
|
||||
dashArray: "1, 8",
|
||||
});
|
||||
this.#spotLines[spot.getID()].addTo(getApp().getMap());
|
||||
} else {
|
||||
// Update the existing polyline
|
||||
if (!getApp().getMap().hasLayer(this.#spotLines[spot.getID()])) this.#spotLines[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotLines[spot.getID()].setLatLngs([this.getPosition(), spot.getTargetPosition()]);
|
||||
}
|
||||
|
||||
/* Iterate all existing lines and remove those associated to a spot that no longer exists */
|
||||
Object.keys(this.#spotLines).forEach((spotID) => {
|
||||
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
|
||||
getApp().getMap().removeLayer(this.#spotLines[spotID]);
|
||||
delete this.#spotLines[spotID];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws or updates the spot edit marker.
|
||||
* @param {Spot} spot - The spot object.
|
||||
* @param {LatLng} midPosition - The midpoint position between the unit and the spot.
|
||||
* @param {number} spotBearing - The bearing of the spot.
|
||||
*/
|
||||
#drawSpotEditMarker(spot: Spot, midPosition: LatLng, spotBearing: number) {
|
||||
if (this.#spotEditMarkers[spot.getID()] === undefined) {
|
||||
// Create a new spot edit marker
|
||||
this.#spotEditMarkers[spot.getID()] = new SpotEditMarker(midPosition, `${spot.getCode() ?? ""}`, 0, spot.getType());
|
||||
this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
|
||||
this.#spotEditMarkers[spot.getID()].onDeleteButtonClicked = () => {
|
||||
getApp().getServerManager().deleteSpot(spot.getID());
|
||||
};
|
||||
this.#spotEditMarkers[spot.getID()].onValueUpdated = (value) => {
|
||||
getApp().getServerManager().setLaserCode(spot.getID(), value);
|
||||
};
|
||||
} else {
|
||||
// Update the existing spot edit marker
|
||||
if (!getApp().getMap().hasLayer(this.#spotEditMarkers[spot.getID()])) this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotEditMarkers[spot.getID()].setLatLng(midPosition);
|
||||
}
|
||||
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
|
||||
this.#spotEditMarkers[spot.getID()].setTextValue(`${spot.getCode() ?? ""}`);
|
||||
|
||||
/* Iterate all existing edit markers and remove those associated to a spot that no longer exists */
|
||||
Object.keys(this.#spotEditMarkers).forEach((spotID) => {
|
||||
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
|
||||
getApp().getMap().removeLayer(this.#spotEditMarkers[spotID]);
|
||||
delete this.#spotEditMarkers[spotID];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws or updates the spot marker.
|
||||
* @param {Spot} spot - The spot object.
|
||||
*/
|
||||
#drawSpotMarker(spot: Spot) {
|
||||
if (this.#spotMarkers[spot.getID()] === undefined) {
|
||||
// Create a new spot marker
|
||||
this.#spotMarkers[spot.getID()] = new SpotMarker(spot.getTargetPosition());
|
||||
this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
this.#spotMarkers[spot.getID()].on("dragstart", (event) => {
|
||||
event.target.options["freeze"] = true;
|
||||
});
|
||||
this.#spotMarkers[spot.getID()].on("dragend", (event) => {
|
||||
getApp().getServerManager().moveSpot(spot.getID(), event.target.getLatLng());
|
||||
event.target.options["freeze"] = false;
|
||||
});
|
||||
} else {
|
||||
// Update the existing spot marker
|
||||
if (!getApp().getMap().hasLayer(this.#spotMarkers[spot.getID()])) this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
|
||||
var frozen = this.#spotMarkers[spot.getID()].options["freeze"];
|
||||
if (!frozen) {
|
||||
this.#spotMarkers[spot.getID()].setLatLng(spot.getTargetPosition());
|
||||
}
|
||||
}
|
||||
|
||||
/* Iterate all existing markers and remove those associated to a spot that no longer exists */
|
||||
Object.keys(this.#spotMarkers).forEach((spotID) => {
|
||||
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
|
||||
getApp().getMap().removeLayer(this.#spotMarkers[spotID]);
|
||||
delete this.#spotMarkers[spotID];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#clearSpots() {
|
||||
Object.values(this.#spots).forEach((spot) => getApp().getMap().removeLayer(spot));
|
||||
// Clear all spot lines, edit markers, and markers
|
||||
this.#clearSpotLines();
|
||||
this.#clearSpotEditMarkers();
|
||||
this.#clearSpotMarkers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all spot lines from the map.
|
||||
*/
|
||||
#clearSpotLines() {
|
||||
Object.values(this.#spotLines).forEach((spot) => getApp().getMap().removeLayer(spot));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all spot edit markers from the map.
|
||||
*/
|
||||
#clearSpotEditMarkers() {
|
||||
Object.values(this.#spotEditMarkers).forEach((spotEditMarker) => getApp().getMap().removeLayer(spotEditMarker));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all spot markers from the map.
|
||||
*/
|
||||
#clearSpotMarkers() {
|
||||
Object.values(this.#spotMarkers).forEach((spotMarker) => getApp().getMap().removeLayer(spotMarker));
|
||||
}
|
||||
|
||||
@ -2093,6 +2206,11 @@ export abstract class Unit extends CustomMarker {
|
||||
#onZoom(e: any) {
|
||||
if (this.checkZoomRedraw()) this.#redrawMarker();
|
||||
this.#updateMarker();
|
||||
|
||||
// Clear the spot edit markers if the map is zoomed out too much
|
||||
if (getApp().getMap().getZoom() < SPOTS_EDIT_ZOOM_TRANSITION) {
|
||||
this.#clearSpotEditMarkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -301,7 +301,7 @@ export class UnitsManager {
|
||||
this.#requestDetectionUpdate = false;
|
||||
}
|
||||
|
||||
/* Update the detection lines of all the units. This code is handled by the UnitsManager since it must be run both when the detected OR the detecting unit is updated */
|
||||
/* Update all the lines of all the selected units. This code is handled by the UnitsManager since, for example, it must be run both when the detected OR the detecting unit is updated */
|
||||
for (let ID in this.#units) {
|
||||
if (this.#units[ID].getSelected()) this.#units[ID].drawLines();
|
||||
}
|
||||
|
||||
@ -615,6 +615,33 @@ function Olympus.fireInfrared(ID, lat, lng)
|
||||
end
|
||||
end
|
||||
|
||||
-- Set new laser code
|
||||
function Olympus.setLaserCode(spotID, code)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil and spot.type == "laser" then
|
||||
spot.object:setCode(code)
|
||||
spot.code = code
|
||||
end
|
||||
end
|
||||
|
||||
-- Move spot to a new location
|
||||
function Olympus.moveSpot(spotID, lat, lng)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil then
|
||||
spot.object:setPoint(coord.LLtoLO(lat, lng, 0))
|
||||
spot.targetPosition = {lat = lat, lng = lng}
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove the spot
|
||||
function Olympus.deleteSpot(spotID)
|
||||
local spot = Olympus.spots[spotID]
|
||||
if spot ~= nil then
|
||||
spot.object:destroy()
|
||||
Olympus.spots[spotID] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Spawns a new unit or group
|
||||
-- Spawn table contains the following parameters
|
||||
-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user