diff --git a/client/src/server/servermanager.ts b/client/src/server/servermanager.ts index 34c90cf4..c00aa271 100644 --- a/client/src/server/servermanager.ts +++ b/client/src/server/servermanager.ts @@ -329,6 +329,18 @@ export class ServerManager { this.PUT(data, callback); } + scenicAAA(ID: number, callback: CallableFunction = () => {}) { + var command = { "ID": ID } + var data = { "scenicAAA": command } + this.PUT(data, callback); + } + + missOnPurpose(ID: number, callback: CallableFunction = () => {}) { + var command = { "ID": ID } + var data = { "missOnPurpose": command } + this.PUT(data, callback); + } + setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings, callback: CallableFunction = () => {}) { var command = { "ID": ID, diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts index 6c081a5d..3ee4351a 100644 --- a/client/src/unit/unit.ts +++ b/client/src/unit/unit.ts @@ -739,6 +739,15 @@ export class Unit extends CustomMarker { }); } + scenicAAA() { + getApp().getServerManager().scenicAAA(this.ID); + } + + missOnPurpose() { + getApp().getServerManager().missOnPurpose(this.ID); + } + + /***********************************************/ onAdd(map: Map): this { super.onAdd(map); @@ -802,7 +811,7 @@ export class Unit extends CustomMarker { if (selectedUnits.length > 0 && !(selectedUnits.length == 1 && (selectedUnits.includes(this)))) { options["attack"] = { text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons" }; if (getApp().getUnitsManager().getSelectedUnitsCategories().length == 1 && getApp().getUnitsManager().getSelectedUnitsCategories()[0] === "Aircraft") - options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position" };; + options["follow"] = { text: "Follow", tooltip: "Follow the unit at a user defined distance and position" }; } else if ((selectedUnits.length > 0 && (selectedUnits.includes(this))) || selectedUnits.length == 0) { if (this.getCategory() == "Aircraft") { @@ -813,6 +822,11 @@ export class Unit extends CustomMarker { if (selectedUnitTypes.length === 1 && ["NavyUnit", "GroundUnit"].includes(selectedUnitTypes[0]) && getApp().getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getCoalition()}) !== undefined) options["group"] = { text: "Create group", tooltip: "Create a group from the selected units." }; + if (selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0]) && selectedUnits.every((unit: Unit) => { return ["AAA"].includes(unit.getType())})) { + options["scenic-aaa"] = { text: "Scenic AAA", tooltip: "Shoot AAA in the air without aiming at any target" }; + options["miss-aaa"] = { text: "Miss on purpose AAA", tooltip: "Shoot AAA towards the closes enemy, but don't aim precisely" }; + } + if (Object.keys(options).length > 0) { getApp().getMap().showUnitContextMenu(e.originalEvent.x, e.originalEvent.y, e.latlng); getApp().getMap().getUnitContextMenu().setOptions(options, (option: string) => { @@ -831,6 +845,10 @@ export class Unit extends CustomMarker { getApp().getUnitsManager().selectedUnitsRefuel(); else if (action === "group") getApp().getUnitsManager().selectedUnitsCreateGroup(); + else if (action === "scenic-aaa") + getApp().getUnitsManager().selectedUnitsScenicAAA(); + else if (action === "miss-aaa") + getApp().getUnitsManager().selectedUnitsMissOnPurpose(); else if (action === "follow") this.#showFollowOptions(e); } diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index 969f33b1..7314b5f3 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -625,6 +625,26 @@ export class UnitsManager { }); this.#showActionMessage(selectedUnits, `unit simulating fire fight`); } + + /** Instruct units to enter into scenic AAA mode. Units will shoot in the air without aiming + * + */ + selectedUnitsScenicAAA() { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].scenicAAA(); + } + } + + /** Instruct units to enter into miss on purpose mode. Units will aim to the nearest enemy unit but not precisely. + * + */ + selectedUnitsMissOnPurpose() { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].missOnPurpose(); + } + } /*********************** Control operations on selected units ************************/ /** See getUnitsCategories for more info diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 7e3d82e7..98d1b628 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,6 +1,6 @@ local version = "v0.4.4-alpha" -local debug = true -- True enables debug printing using DCS messages +local debug = false -- True enables debug printing using DCS messages -- .dll related variables Olympus.OlympusDLL = nil diff --git a/src/core/include/datatypes.h b/src/core/include/datatypes.h index 207b72ee..4df2f44b 100644 --- a/src/core/include/datatypes.h +++ b/src/core/include/datatypes.h @@ -65,7 +65,9 @@ namespace State CARPET_BOMB, BOMB_BUILDING, FIRE_AT_AREA, - SIMULATE_FIRE_FIGHT + SIMULATE_FIRE_FIGHT, + SCENIC_AAA, + MISS_ON_PURPOSE }; }; diff --git a/src/core/include/unit.h b/src/core/include/unit.h index b36b4d9b..fa93ba3e 100644 --- a/src/core/include/unit.h +++ b/src/core/include/unit.h @@ -186,6 +186,7 @@ protected: /********** Other **********/ unsigned int taskCheckCounter = 0; + unsigned int internalCounter = 0; bool hasTaskAssigned = false; double initialFuel = 0; map updateTimeMap; diff --git a/src/core/include/unitsmanager.h b/src/core/include/unitsmanager.h index 9a6b2f1d..f7af7304 100644 --- a/src/core/include/unitsmanager.h +++ b/src/core/include/unitsmanager.h @@ -23,7 +23,8 @@ public: void deleteUnit(unsigned int ID, bool explosion, bool immediate); void acquireControl(unsigned int ID); void loadDatabases(); - + Unit* getClosestUnit(Unit* unit, unsigned char coalition, vector categories, double &distance); + private: map units; json::value missionDB; diff --git a/src/core/src/groundunit.cpp b/src/core/src/groundunit.cpp index 3f70a590..158d149d 100644 --- a/src/core/src/groundunit.cpp +++ b/src/core/src/groundunit.cpp @@ -73,6 +73,12 @@ void GroundUnit::setState(unsigned char newState) setTargetPosition(Coords(NULL)); break; } + case State::SCENIC_AAA: { + break; + } + case State::MISS_ON_PURPOSE: { + break; + } default: break; } @@ -99,6 +105,16 @@ void GroundUnit::setState(unsigned char newState) resetActiveDestination(); break; } + case State::SCENIC_AAA: { + clearActivePath(); + resetActiveDestination(); + break; + } + case State::MISS_ON_PURPOSE: { + clearActivePath(); + resetActiveDestination(); + break; + } default: break; } @@ -156,11 +172,13 @@ void GroundUnit::AIloop() scheduler->appendCommand(command); setHasTask(true); } + + break; } case State::SIMULATE_FIRE_FIGHT: { setTask("Simulating fire fight"); - if (!getHasTask() || (((double)(rand()) / (double)(RAND_MAX)) < 0.002)) { + if (!getHasTask() || internalCounter == 0) { double dist; double bearing1; double bearing2; @@ -175,7 +193,6 @@ void GroundUnit::AIloop() if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) { barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); - log(to_string(barrelHeight) + " " + to_string(muzzleVelocity)); } } @@ -193,6 +210,93 @@ void GroundUnit::AIloop() scheduler->appendCommand(command); setHasTask(true); } + + if (internalCounter == 0) + internalCounter = 20 * scheduler->getFrameRate(); /* 20 seconds */ + internalCounter--; + + break; + } + 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)))); + + 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) + internalCounter = 20 * scheduler->getFrameRate(); /* 20 seconds */ + internalCounter--; + + break; + } + case State::MISS_ON_PURPOSE: { + setTask("Missing on purpose"); + + /* Only run this when the internal counter reaches 0 to avoid excessive computations when no nearby target */ + if (internalCounter == 0) { + double distance = 0; + Unit* target = unitsManager->getClosestUnit(this, 1, { "Aircraft", "Helicopter" }, distance); /* Red, TODO make assignable */ + + /* Only do if we have a valid target close enough for AAA */ + if (target != nullptr && distance < 10000 /* m */) { + double aimTime = 10; /* s TODO: add to database */ + + /* Compute where the target will be in aimTime seconds. We don't consider vertical velocity atm, since after all we are not really tring to hit */ + double aimDistance = target->getSpeed() * aimTime; + double aimLat = 0; + double aimLng = 0; + Geodesic::WGS84().Direct(target->getPosition().lat, target->getPosition().lng, target->getHeading(), aimDistance, aimLat, aimLng); + + /* Compute distance to the aim point */ + double dist; + double bearing1; + double bearing2; + Geodesic::WGS84().Inverse(position.lat, position.lng, aimLat, aimLng, dist, bearing1, bearing2); + + /* Default gun values */ + double barrelHeight = 1.0; /* m */ + double muzzleVelocity = 860; /* m/s */ + if (database.has_object_field(to_wstring(name))) { + json::value databaseEntry = database[to_wstring(name)]; + if (databaseEntry.has_number_field(L"barrelHeight") && databaseEntry.has_number_field(L"muzzleVelocity")) { + barrelHeight = databaseEntry[L"barrelHeight"].as_number().to_double(); + muzzleVelocity = databaseEntry[L"muzzleVelocity"].as_number().to_double(); + } + } + + /* Send the command */ + std::ostringstream taskSS; + taskSS.precision(10); + taskSS << "{id = 'FireAtPoint', lat = " << aimLat << ", lng = " << aimLng << ", alt = " << target->getPosition().alt + 0.5 * 9.81 * (dist * dist) / (muzzleVelocity * muzzleVelocity) << ", radius = 0.001}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str(), [this]() { this->setHasTaskAssigned(true); })); + scheduler->appendCommand(command); + setHasTask(true); + } + else { + if (getHasTask()) + resetTask(); + } + } + + if (internalCounter == 0) + internalCounter = 5 * scheduler->getFrameRate(); + internalCounter--; + + break; } default: break; diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index b1564bdf..10e72279 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -390,6 +390,7 @@ void Scheduler::handleRequest(string key, json::value value, string username, js Coords location; location.lat = lat; location.lng = lng; cloneOptions.push_back({ ID, location }); + log(username + " cloning unit with ID " + to_string(ID), true); } command = dynamic_cast(new Clone(cloneOptions, deleteOriginal)); @@ -575,6 +576,22 @@ void Scheduler::handleRequest(string key, json::value value, string username, js unit->setTargetPosition(loc); log(username + " tasked unit " + unit->getName() + " to simulate a fire fight", true); } + else if (key.compare("scenicAAA") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::SCENIC_AAA); + log(username + " tasked unit " + unit->getName() + " to enter scenic AAA state", true); + } + else if (key.compare("missOnPurpose") == 0) + { + unsigned int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + 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) { setCommandModeOptions(value); log(username + " updated the Command Mode Options", true); diff --git a/src/core/src/unitsmanager.cpp b/src/core/src/unitsmanager.cpp index 5a412ac0..a157956e 100644 --- a/src/core/src/unitsmanager.cpp +++ b/src/core/src/unitsmanager.cpp @@ -11,6 +11,9 @@ #include "scheduler.h" #include "defines.h" +#include +using namespace GeographicLib; + #include "base64.hpp" using namespace base64; @@ -148,6 +151,48 @@ void UnitsManager::deleteUnit(unsigned int ID, bool explosion, bool immediate) } } +Unit* UnitsManager::getClosestUnit(Unit* unit, unsigned char coalition, vector categories, double &distance) { + Unit* closestUnit = nullptr; + distance = 0; + + for (auto const& p : units) { + /* Check if the units category is of the correct type */ + bool requestedCategory = false; + for (auto const& category : categories) { + if (p.second->getCategory().compare(category) == 0) { + requestedCategory = true; + break; + } + } + + /* Check if the unit belongs to the desired coalition, is alive, and is of the category requested */ + if (requestedCategory && p.second->getCoalition() == coalition && p.second->getAlive()) { + /* Compute the distance from the unit to the tested unit */ + double dist; + double bearing1; + double bearing2; + Geodesic::WGS84().Inverse(unit->getPosition().lat, unit->getPosition().lng, p.second->getPosition().lat, p.second->getPosition().lng, dist, bearing1, bearing2); + + /* If the closest unit has not been assigned yet, assign it to this unit */ + if (closestUnit == nullptr) + { + closestUnit = p.second; + distance = dist; + + } + else { + /* Check if the unit is closer than the one already selected */ + if (dist < distance) { + closestUnit = p.second; + distance = dist; + } + } + } + } + + return closestUnit; +} + void UnitsManager::acquireControl(unsigned int ID) { Unit* leader = getGroupLeader(ID); if (leader != nullptr) {