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; } } }