diff --git a/client/demo.js b/client/demo.js
index 57a0eb5a..1e2288d0 100644
--- a/client/demo.js
+++ b/client/demo.js
@@ -63,7 +63,8 @@ const DEMO_UNIT_DATA = {
ammo: [{ quantity: 2, name: "A cool missile\0Ciao", guidance: 0, category: 0, missileCategory: 0 } ],
contacts: [{ID: 1001, detectionMethod: 16}],
activePath: [ ],
- isLeader: true
+ isLeader: true,
+ operateAs: 2
}, ["5"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 0, country: 0, name: "Gepard", unitName: "Cool guy 2-2", groupName: "Cool group 4", state: 1, task: "Being cool",
hasTask: false, position: { lat: 37.21, lng: -116.1, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50,
desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0,
@@ -79,7 +80,8 @@ const DEMO_UNIT_DATA = {
ammo: [{ quantity: 2, name: "A cool missile", guidance: 0, category: 0, missileCategory: 0 } ],
contacts: [],
activePath: [ ],
- isLeader: false
+ isLeader: false,
+ operateAs: 2
},
["6"]:{ category: "Aircraft", alive: true, human: false, controlled: false, coalition: 1, country: 0, name: "FA-18C_hornet", unitName: "Bad boi 1-2", groupName: "Bad group 1", state: 1, task: "Being bad",
hasTask: false, position: { lat: 36.8, lng: -116, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50,
@@ -187,6 +189,7 @@ class DemoDataGenerator {
array = this.appendContacts(array, unit.contacts, 36);
array = this.appendActivePath(array, unit.activePath, 37);
array = this.appendUint8(array, unit.isLeader, 38);
+ array = this.appendUint8(array, unit.operateAs, 39);
array = this.concat(array, this.uint8ToByteArray(255));
}
res.end(Buffer.from(array, 'binary'));
diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css
index 9a06c25d..e1feb19a 100644
--- a/client/public/stylesheets/other/contextmenus.css
+++ b/client/public/stylesheets/other/contextmenus.css
@@ -377,20 +377,12 @@
content: url("/resources/theme/images/icons/follow.svg");
}
-#scenic-aaa-red::before {
- content: url("/resources/theme/images/icons/scenic-red.svg");
+#scenic-aaa::before {
+ content: url("/resources/theme/images/icons/scenic.svg");
}
-#scenic-aaa-blue::before {
- content: url("/resources/theme/images/icons/scenic-blue.svg");
-}
-
-#miss-aaa-red::before {
- content: url("/resources/theme/images/icons/miss-red.svg");
-}
-
-#miss-aaa-blue::before {
- content: url("/resources/theme/images/icons/miss-blue.svg");
+#miss-aaa::before {
+ content: url("/resources/theme/images/icons/miss.svg");
}
#group-ground::before {
diff --git a/client/public/themes/olympus/images/icons/miss.svg b/client/public/themes/olympus/images/icons/miss.svg
new file mode 100644
index 00000000..0a88637e
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/miss.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/client/public/themes/olympus/images/icons/scenic.svg b/client/public/themes/olympus/images/icons/scenic.svg
new file mode 100644
index 00000000..6321f72a
--- /dev/null
+++ b/client/public/themes/olympus/images/icons/scenic.svg
@@ -0,0 +1,49 @@
+
+
diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts
index cd0b3225..e952cd0b 100644
--- a/client/src/constants/constants.ts
+++ b/client/src/constants/constants.ts
@@ -197,5 +197,6 @@ export enum DataIndexes {
contacts,
activePath,
isLeader,
+ operateAs,
endOfData = 255
};
\ No newline at end of file
diff --git a/client/src/interfaces.ts b/client/src/interfaces.ts
index 567979e1..0661f486 100644
--- a/client/src/interfaces.ts
+++ b/client/src/interfaces.ts
@@ -176,6 +176,7 @@ export interface UnitData {
contacts: Contact[];
activePath: LatLng[];
isLeader: boolean;
+ operateAs: string;
}
export interface LoadoutItemBlueprint {
diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts
index 58f799ba..619590f4 100644
--- a/client/src/other/utils.ts
+++ b/client/src/other/utils.ts
@@ -351,6 +351,16 @@ export function enumToCoalition(coalitionID: number) {
return "";
}
+export function coalitionToEnum(coalition: string) {
+ switch (coalition){
+ case "neutral": return 0;
+ case "red": return 1;
+ case "blue": return 2;
+ }
+ return 0;
+}
+
+
export function convertDateAndTimeToDate(dateAndTime: DateAndTime) {
const date = dateAndTime.date;
const time = dateAndTime.time;
diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts
index d28cfad0..f6953a12 100644
--- a/client/src/panels/unitcontrolpanel.ts
+++ b/client/src/panels/unitcontrolpanel.ts
@@ -17,7 +17,7 @@ export class UnitControlPanel extends Panel {
#speedTypeSwitch: Switch;
#onOffSwitch: Switch;
#followRoadsSwitch: Switch;
- #operateAs: Switch;
+ #operateAsSwitch: Switch;
#TACANXYDropdown: Dropdown;
#radioDecimalsDropdown: Dropdown;
#radioCallsignDropdown: Dropdown;
@@ -69,8 +69,8 @@ export class UnitControlPanel extends Panel {
});
/* Operate as */
- this.#operateAs = new Switch("operate-as-switch", (value: boolean) => {
- //getApp().getUnitsManager().selectedUnitsSetFollowRoads(value);
+ this.#operateAsSwitch = new Switch("operate-as-switch", (value: boolean) => {
+ getApp().getUnitsManager().selectedUnitsSetOperateAs(value);
});
/* Advanced settings dialog */
@@ -181,6 +181,7 @@ export class UnitControlPanel extends Panel {
var desiredSpeedType = getApp().getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getDesiredSpeedType()});
var onOff = getApp().getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getOnOff()});
var followRoads = getApp().getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getFollowRoads()});
+ var operateAs = getApp().getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getOperateAs()});
this.#altitudeTypeSwitch.setValue(desiredAltitudeType != undefined? desiredAltitudeType == "ASL": undefined, false);
this.#speedTypeSwitch.setValue(desiredSpeedType != undefined? desiredSpeedType == "CAS": undefined, false);
@@ -218,6 +219,7 @@ export class UnitControlPanel extends Panel {
this.#onOffSwitch.setValue(onOff, false);
this.#followRoadsSwitch.setValue(followRoads, false);
+ this.#operateAsSwitch.setValue(operateAs? operateAs === "blue": undefined, false);
}
}
}
diff --git a/client/src/server/servermanager.ts b/client/src/server/servermanager.ts
index b73037b1..361fa6c2 100644
--- a/client/src/server/servermanager.ts
+++ b/client/src/server/servermanager.ts
@@ -293,6 +293,13 @@ export class ServerManager {
this.PUT(data, callback);
}
+ setOperateAs(ID: number, operateAs: number, callback: CallableFunction = () => {}) {
+ var command = { "ID": ID, "operateAs": operateAs }
+ var data = { "setOperateAs": command }
+ this.PUT(data, callback);
+ }
+
+
refuel(ID: number, callback: CallableFunction = () => {}) {
var command = { "ID": ID };
var data = { "refuel": command }
diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts
index 83c9a0e6..6cfb1897 100644
--- a/client/src/unit/unit.ts
+++ b/client/src/unit/unit.ts
@@ -1,6 +1,6 @@
import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map, Point } from 'leaflet';
import { getApp } from '..';
-import { enumToCoalition, enumToEmissioNCountermeasure, getMarkerCategoryByName, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, deg2rad, ftToM, getGroundElevation } from '../other/utils';
+import { enumToCoalition, enumToEmissioNCountermeasure, getMarkerCategoryByName, enumToROE, enumToReactionToThreat, enumToState, getUnitDatabaseByCategory, mToFt, msToKnots, rad2deg, bearing, deg2rad, ftToM, getGroundElevation, coalitionToEnum } from '../other/utils';
import { CustomMarker } from '../map/markers/custommarker';
import { SVGInjector } from '@tanem/svg-injector';
import { UnitDatabase } from './databases/unitdatabase';
@@ -77,6 +77,7 @@ export class Unit extends CustomMarker {
#contacts: Contact[] = [];
#activePath: LatLng[] = [];
#isLeader: boolean = false;
+ #operateAs: string = "blue";
#selectable: boolean;
#selected: boolean = false;
@@ -130,6 +131,7 @@ export class Unit extends CustomMarker {
getContacts() { return this.#contacts };
getActivePath() { return this.#activePath };
getIsLeader() { return this.#isLeader };
+ getOperateAs() { return this.#operateAs };
static getConstructor(type: string) {
if (type === "GroundUnit") return GroundUnit;
@@ -232,6 +234,7 @@ export class Unit extends CustomMarker {
case DataIndexes.contacts: this.#contacts = dataExtractor.extractContacts(); document.dispatchEvent(new CustomEvent("contactsUpdated", { detail: this })); break;
case DataIndexes.activePath: this.#activePath = dataExtractor.extractActivePath(); break;
case DataIndexes.isLeader: this.#isLeader = dataExtractor.extractBool(); break;
+ case DataIndexes.operateAs: this.#operateAs = enumToCoalition(dataExtractor.extractUInt8()); break;
}
}
@@ -291,7 +294,8 @@ export class Unit extends CustomMarker {
ammo: this.#ammo,
contacts: this.#contacts,
activePath: this.#activePath,
- isLeader: this.#isLeader
+ isLeader: this.#isLeader,
+ operateAs: this.#operateAs
}
}
@@ -685,6 +689,11 @@ export class Unit extends CustomMarker {
getApp().getServerManager().setFollowRoads(this.ID, followRoads);
}
+ setOperateAs(operateAs: string) {
+ if (!this.#human)
+ getApp().getServerManager().setOperateAs(this.ID, coalitionToEnum(operateAs));
+ }
+
delete(explosion: boolean, immediate: boolean) {
getApp().getServerManager().deleteUnit(this.ID, explosion, immediate);
}
@@ -731,11 +740,23 @@ export class Unit extends CustomMarker {
});
}
- scenicAAA(coalition: string) {
+ scenicAAA() {
+ var coalition = "neutral";
+ if (this.getCoalition() === "red")
+ coalition = "blue";
+ else if (this.getCoalition() == "blue")
+ coalition = "red";
+ //TODO
getApp().getServerManager().scenicAAA(this.ID, coalition);
}
- missOnPurpose(coalition: string) {
+ missOnPurpose() {
+ var coalition = "neutral";
+ if (this.getCoalition() === "red")
+ coalition = "blue";
+ else if (this.getCoalition() == "blue")
+ coalition = "red";
+ //TODO
getApp().getServerManager().missOnPurpose(this.ID, coalition);
}
@@ -754,14 +775,10 @@ export class Unit extends CustomMarker {
getApp().getUnitsManager().selectedUnitsRefuel();
else if (action === "group-ground" || action === "group-navy")
getApp().getUnitsManager().selectedUnitsCreateGroup();
- else if (action === "scenic-aaa-red")
- getApp().getUnitsManager().selectedUnitsScenicAAA("red");
- else if (action === "scenic-aaa-blue")
- getApp().getUnitsManager().selectedUnitsScenicAAA("blue");
- else if (action === "miss-aaa-red")
- getApp().getUnitsManager().selectedUnitsMissOnPurpose("red");
- else if (action === "miss-aaa-blue")
- getApp().getUnitsManager().selectedUnitsMissOnPurpose("blue");
+ else if (action === "scenic-aaa")
+ getApp().getUnitsManager().selectedUnitsScenicAAA();
+ else if (action === "miss-aaa")
+ getApp().getUnitsManager().selectedUnitsMissOnPurpose();
else if (action === "follow")
this.#showFollowOptions(e);
}
@@ -1270,10 +1287,8 @@ export class GroundUnit extends Unit {
}
if (["AAA", "flak"].includes(this.getType())) {
- options["scenic-aaa-red"] = { text: "Scenic AAA (red)", tooltip: "Shoot AAA in the air without aiming at any target, when a red air unit gets close enough", type: "and" };
- options["scenic-aaa-blue"] = { text: "Scenic AAA (blue)", tooltip: "Shoot AAA in the air without aiming at any target, when a blue air unit gets close enough", type: "and" };
- options["miss-aaa-red"] = { text: "Miss on purpose AAA (red)", tooltip: "Shoot AAA towards the closest red air unit, but don't aim precisely", type: "and" };
- options["miss-aaa-blue"] = { text: "Miss on purpose AAA (blue)", tooltip: "Shoot AAA towards the closest blue air unit, but don't aim precisely", type: "and" };
+ options["scenic-aaa"] = { text: "Scenic AAA", tooltip: "Shoot AAA in the air without aiming at any target, when a enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" };
+ options["miss-aaa"] = { text: "Miss on purpose AAA", tooltip: "Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim", type: "and" };
}
}
/* All other options */
diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts
index 0125adef..bdeb4ee7 100644
--- a/client/src/unit/unitsmanager.ts
+++ b/client/src/unit/unitsmanager.ts
@@ -500,6 +500,19 @@ export class UnitsManager {
this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`);
}
+ /** Instruct selected units to operate as a certain coalition
+ *
+ * @param operateAsBool If true, units will operate as blue
+ */
+ selectedUnitsSetOperateAs(operateAsBool: boolean) {
+ var operateAs = operateAsBool? "blue": "red";
+ var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
+ for (let idx in selectedUnits) {
+ selectedUnits[idx].setOperateAs(operateAs);
+ }
+ this.#showActionMessage(selectedUnits, `operate as set to ${operateAs}`);
+ }
+
/** Instruct units to attack a specific unit
*
* @param ID ID of the unit to attack
@@ -629,23 +642,23 @@ export class UnitsManager {
/** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming
*
*/
- selectedUnitsScenicAAA(coalition: string) {
+ selectedUnitsScenicAAA() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
for (let idx in selectedUnits) {
- selectedUnits[idx].scenicAAA(coalition);
+ selectedUnits[idx].scenicAAA();
}
- this.#showActionMessage(selectedUnits, `unit set to perform scenic AAA against ${coalition} units`);
+ this.#showActionMessage(selectedUnits, `unit set to perform scenic AAA`);
}
/** Instruct units to enter into miss on purpose mode. Units will aim to the nearest enemy unit but not precisely.
*
*/
- selectedUnitsMissOnPurpose(coalition: string) {
+ selectedUnitsMissOnPurpose() {
var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true });
for (let idx in selectedUnits) {
- selectedUnits[idx].missOnPurpose(coalition);
+ selectedUnits[idx].missOnPurpose();
}
- this.#showActionMessage(selectedUnits, `unit set to perform miss on purpose AAA against ${coalition} units`);
+ this.#showActionMessage(selectedUnits, `unit set to perform miss on purpose AAA`);
}
/*********************** Control operations on selected units ************************/
diff --git a/src/core/include/datatypes.h b/src/core/include/datatypes.h
index 4df2f44b..dd098232 100644
--- a/src/core/include/datatypes.h
+++ b/src/core/include/datatypes.h
@@ -43,6 +43,7 @@ namespace DataIndex {
contacts,
activePath,
isLeader,
+ operateAs,
lastIndex,
endOfData = 255
};
diff --git a/src/core/include/unit.h b/src/core/include/unit.h
index fa93ba3e..ebbcff94 100644
--- a/src/core/include/unit.h
+++ b/src/core/include/unit.h
@@ -100,6 +100,7 @@ public:
virtual void setContacts(vector newValue);
virtual void setActivePath(list newValue);
virtual void setIsLeader(bool newValue) { updateValue(isLeader, newValue, DataIndex::isLeader); }
+ virtual void setOperateAs(unsigned char newValue) { updateValue(operateAs, newValue, DataIndex::operateAs); }
/********** Getters **********/
virtual string getCategory() { return category; };
@@ -140,6 +141,7 @@ public:
virtual vector getTargets() { return contacts; }
virtual list getActivePath() { return activePath; }
virtual bool getIsLeader() { return isLeader; }
+ virtual unsigned char getOperateAs() { return operateAs; }
protected:
unsigned int ID;
@@ -182,6 +184,7 @@ protected:
vector contacts;
list activePath;
bool isLeader = false;
+ unsigned char operateAs = 2;
Coords activeDestination = Coords(NULL);
/********** Other **********/
diff --git a/src/core/src/groundunit.cpp b/src/core/src/groundunit.cpp
index d3242469..a84a69c3 100644
--- a/src/core/src/groundunit.cpp
+++ b/src/core/src/groundunit.cpp
@@ -220,21 +220,28 @@ void GroundUnit::AIloop()
case State::SCENIC_AAA: {
setTask("Scenic AAA");
- if (!getHasTask() || internalCounter == 0) {
- double r = 15; /* m */
- double barrelElevation = r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
+ if ((!getHasTask() || internalCounter == 0) && getOperateAs() > 0) {
+ double distance = 0;
+ unsigned char targetCoalition = getOperateAs() == 2 ? 1 : 2;
+ Unit* target = unitsManager->getClosestUnit(this, targetCoalition, { "Aircraft", "Helicopter" }, distance);
- double lat = 0;
- double lng = 0;
- double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360;
- Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng);
+ /* Only run if an enemy air unit is closer than 20km to avoid useless load */
+ if (distance < 20000 /* m */) {
+ double r = 15; /* m */
+ double barrelElevation = r * tan(acos(((double)(rand()) / (double)(RAND_MAX))));
- std::ostringstream taskSS;
- taskSS.precision(10);
- taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation << ", radius = 0.001}";
- Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
- scheduler->appendCommand(command);
- setHasTask(true);
+ double lat = 0;
+ double lng = 0;
+ double randomBearing = ((double)(rand()) / (double)(RAND_MAX)) * 360;
+ Geodesic::WGS84().Direct(position.lat, position.lng, randomBearing, r, lat, lng);
+
+ std::ostringstream taskSS;
+ taskSS.precision(10);
+ taskSS << "{id = 'FireAtPoint', lat = " << lat << ", lng = " << lng << ", alt = " << position.alt + barrelElevation << ", radius = 0.001}";
+ Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); }));
+ scheduler->appendCommand(command);
+ setHasTask(true);
+ }
}
if (internalCounter == 0)
@@ -247,9 +254,10 @@ void GroundUnit::AIloop()
setTask("Missing on purpose");
/* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */
- if (internalCounter == 0) {
+ if (internalCounter == 0 && getOperateAs() > 0) {
double distance = 0;
- Unit* target = unitsManager->getClosestUnit(this, 1, { "Aircraft", "Helicopter" }, distance); /* Red, TODO make assignable */
+ unsigned char targetCoalition = getOperateAs() == 2 ? 1 : 2;
+ Unit* target = unitsManager->getClosestUnit(this, targetCoalition, {"Aircraft", "Helicopter"}, distance);
/* Only do if we have a valid target close enough for AAA */
if (target != nullptr && distance < 10000 /* m */) {
diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp
index 10e72279..df480c32 100644
--- a/src/core/src/scheduler.cpp
+++ b/src/core/src/scheduler.cpp
@@ -592,7 +592,16 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
unit->setState(State::MISS_ON_PURPOSE);
log(username + " tasked unit " + unit->getName() + " to enter Miss On Purpose state", true);
}
- else if (key.compare("setCommandModeOptions") == 0) {
+ else if (key.compare("setOperateAs") == 0)
+ {
+ unsigned int ID = value[L"ID"].as_integer();
+ unitsManager->acquireControl(ID);
+ unsigned char operateAs = value[L"operateAs"].as_number().to_uint32();
+ Unit* unit = unitsManager->getGroupLeader(ID);
+ unit->setOperateAs(operateAs);
+ }
+ else if (key.compare("setCommandModeOptions") == 0)
+ {
setCommandModeOptions(value);
log(username + " updated the Command Mode Options", true);
}
diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp
index 4aaf5d8a..a5414aab 100644
--- a/src/core/src/unit.cpp
+++ b/src/core/src/unit.cpp
@@ -191,6 +191,7 @@ void Unit::refreshLeaderData(unsigned long long time) {
case DataIndex::radio: updateValue(radio, leader->radio, datumIndex); break;
case DataIndex::generalSettings: updateValue(generalSettings, leader->generalSettings, datumIndex); break;
case DataIndex::activePath: updateValue(activePath, leader->activePath, datumIndex); break;
+ case DataIndex::operateAs: updateValue(operateAs, leader->operateAs, datumIndex); break;
}
}
}
@@ -270,6 +271,7 @@ void Unit::getData(stringstream& ss, unsigned long long time)
case DataIndex::contacts: appendVector(ss, datumIndex, contacts); break;
case DataIndex::activePath: appendList(ss, datumIndex, activePath); break;
case DataIndex::isLeader: appendNumeric(ss, datumIndex, isLeader); break;
+ case DataIndex::operateAs: appendNumeric(ss, datumIndex, operateAs); break;
}
}
}