diff --git a/client/public/databases/units/groundunitdatabase.json b/client/public/databases/units/groundunitdatabase.json index 04fe6d6b..34936b80 100644 --- a/client/public/databases/units/groundunitdatabase.json +++ b/client/public/databases/units/groundunitdatabase.json @@ -1076,7 +1076,9 @@ "aimTime": 5, "shotsToFire": 100, "cost": 15000000, - "canAAA": true + "canAAA": true, + "targetingRange": 500, + "aimMethodRange": 9000 }, "Grad-URAL": { "name": "Grad-URAL", diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 69ecb6b5..9b576252 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -9,11 +9,19 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { row-gap: 10px; } -#unit-control-panel>div:nth-child(2) { +#unit-control-panel>div:nth-child(2), +#unit-controls { display: flex; flex-direction: column; row-gap: 10px; - width: 300px; +} + +#unit-controls { + padding-right: 10px; +} + +#unit-control-panel>div:nth-child(2) { + width: 330px; } #unit-control-panel>*:nth-child(1) { @@ -262,18 +270,47 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { } #advanced-settings-div { + position: relative; column-gap: 5px; display: flex; + height: fit-content; } #advanced-settings-div>*:nth-child(2) { margin-left: auto; + margin-right: 58px; } #advanced-settings-div button { height: 40px; } +#explosion-types-selector { + padding-right: 5px; + border-radius: var(--border-radius-sm); + display: flex; + flex-direction: column; + row-gap: 5px; + background-color: var(--background-steel); + position: absolute; + right: 0px; + bottom: 0px; + height: fit-content; + overflow: hidden; +} + +#explosion-types-selector>*:not(:last-child) { + display: none; +} + +#explosion-types-selector:hover { + padding: 5px 5px 0px 5px; +} + +#explosion-types-selector:hover>*:not(:last-child) { + display: block; +} + /* Element visibility control */ #unit-control-panel:not([data-show-categories-tooltip]) #categories-tooltip, #unit-control-panel:not([data-show-speed-slider]) #speed-slider, diff --git a/client/public/stylesheets/style/style.css b/client/public/stylesheets/style/style.css index 1ff9293e..b8d68b6f 100644 --- a/client/public/stylesheets/style/style.css +++ b/client/public/stylesheets/style/style.css @@ -88,7 +88,7 @@ form { } .ol-scrollable { - overflow-y: scroll; + overflow-y: auto; scrollbar-color: white transparent; scrollbar-width: thin; } diff --git a/client/public/themes/olympus/images/icons/fire-solid.svg b/client/public/themes/olympus/images/icons/fire-solid.svg new file mode 100644 index 00000000..c227821a --- /dev/null +++ b/client/public/themes/olympus/images/icons/fire-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/napalm.svg b/client/public/themes/olympus/images/icons/napalm.svg new file mode 100644 index 00000000..b82d15a8 --- /dev/null +++ b/client/public/themes/olympus/images/icons/napalm.svg @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/secondaries.svg b/client/public/themes/olympus/images/icons/secondaries.svg new file mode 100644 index 00000000..2863e9a4 --- /dev/null +++ b/client/public/themes/olympus/images/icons/secondaries.svg @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/white-phosphorous.svg b/client/public/themes/olympus/images/icons/white-phosphorous.svg new file mode 100644 index 00000000..95627c72 --- /dev/null +++ b/client/public/themes/olympus/images/icons/white-phosphorous.svg @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/client/src/contextmenus/mapcontextmenu.ts b/client/src/contextmenus/mapcontextmenu.ts index 209a4cfb..866e2543 100644 --- a/client/src/contextmenus/mapcontextmenu.ts +++ b/client/src/contextmenus/mapcontextmenu.ts @@ -58,7 +58,7 @@ export class MapContextMenu extends ContextMenu { document.addEventListener("contextMenuExplosion", (e: any) => { this.hide(); - getApp().getServerManager().spawnExplosion(e.detail.strength, this.getLatLng()); + getApp().getServerManager().spawnExplosion(e.detail.strength ?? 0, e.detail.explosionType, this.getLatLng()); }); document.addEventListener("editCoalitionArea", (e: any) => { diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 1ec769f6..b317992d 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -161,6 +161,7 @@ export class UnitControlPanel extends Panel { this.#followRoadsSwitch.resetExpectedValue(); this.#altitudeSlider.resetExpectedValue(); this.#speedSlider.resetExpectedValue(); + this.#calculateMaxHeight(); } addButtons() { @@ -486,6 +487,7 @@ export class UnitControlPanel extends Panel { #calculateMaxHeight() { const element = document.getElementById("unit-control-panel-content"); + this.#calculateTop(); if (element) element.style.maxHeight = `${window.innerHeight - this.getElement().offsetTop - 10}px`; } diff --git a/client/src/server/servermanager.ts b/client/src/server/servermanager.ts index 87a72599..f1ec0a54 100644 --- a/client/src/server/servermanager.ts +++ b/client/src/server/servermanager.ts @@ -160,8 +160,8 @@ export class ServerManager { this.PUT(data, callback); } - spawnExplosion(intensity: number, latlng: LatLng, callback: CallableFunction = () => {}) { - var command = { "intensity": intensity, "location": latlng }; + spawnExplosion(intensity: number, explosionType: string, latlng: LatLng, callback: CallableFunction = () => {}) { + var command = { "explosionType": explosionType, "intensity": intensity, "location": latlng }; var data = { "explosion": command } this.PUT(data, callback); } @@ -212,8 +212,8 @@ export class ServerManager { this.PUT(data, callback); } - deleteUnit(ID: number, explosion: boolean, immediate: boolean, callback: CallableFunction = () => {}) { - var command = { "ID": ID, "explosion": explosion, "immediate": immediate }; + deleteUnit(ID: number, explosion: boolean, explosionType: string, immediate: boolean, callback: CallableFunction = () => {}) { + var command = { "ID": ID, "explosion": explosion, "explosionType": explosionType, "immediate": immediate }; var data = { "deleteUnit": command } this.PUT(data, callback); } diff --git a/client/src/unit/unit.ts b/client/src/unit/unit.ts index 5f529327..47968e04 100644 --- a/client/src/unit/unit.ts +++ b/client/src/unit/unit.ts @@ -851,8 +851,8 @@ export class Unit extends CustomMarker { getApp().getServerManager().setOperateAs(this.ID, coalitionToEnum(operateAs)); } - delete(explosion: boolean, immediate: boolean) { - getApp().getServerManager().deleteUnit(this.ID, explosion, immediate); + delete(explosion: boolean, explosionType: string, immediate: boolean) { + getApp().getServerManager().deleteUnit(this.ID, explosion, explosionType, immediate); } refuel() { diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index f50e0d23..c1e159ab 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -37,7 +37,7 @@ export class UnitsManager { document.addEventListener('contactsUpdated', (e: CustomEvent) => { this.#requestDetectionUpdate = true }); document.addEventListener('copy', () => this.selectedUnitsCopy()); document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); - document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); + document.addEventListener('explodeSelectedUnits', (e: any) => this.selectedUnitsDelete(true, e.detail.type)); document.addEventListener('exportToFile', () => this.exportToFile()); document.addEventListener('importFromFile', () => this.importFromFile()); document.addEventListener('keyup', (event) => this.#onKeyUp(event)); @@ -794,7 +794,7 @@ export class UnitsManager { * @param explosion If true, the unit will be deleted using an explosion * @returns */ - selectedUnitsDelete(explosion: boolean = false) { + selectedUnitsDelete(explosion: boolean = false, explosionType: string = "") { var selectedUnits = this.getSelectedUnits({excludeProtected:true}); /* Can be applied to humans too */ const selectionContainsAHuman = selectedUnits.some((unit: Unit) => { return unit.getHuman() === true; @@ -804,9 +804,9 @@ export class UnitsManager { return; } - const doDelete = (explosion = false, immediate = false) => { + const doDelete = (explosion = false, explosionType = "", immediate = false) => { for (let idx in selectedUnits) { - selectedUnits[idx].delete(explosion, immediate); + selectedUnits[idx].delete(explosion, explosionType, immediate); } this.#showActionMessage(selectedUnits, `deleted`); } @@ -814,12 +814,12 @@ export class UnitsManager { if (selectedUnits.length >= DELETE_SLOW_THRESHOLD) this.#showSlowDeleteDialog(selectedUnits).then((action:any) => { if (action === "delete-slow") - doDelete(explosion, false); + doDelete(explosion, explosionType, false); else if (action === "delete-immediate") - doDelete(explosion, true); + doDelete(explosion, explosionType, true); }) else - doDelete(explosion); + doDelete(explosion, explosionType); } diff --git a/client/views/contextmenus/map.ejs b/client/views/contextmenus/map.ejs index ae4de75d..3e37ba57 100644 --- a/client/views/contextmenus/map.ejs +++ b/client/views/contextmenus/map.ejs @@ -49,9 +49,12 @@
- - - - + + + + + + +
\ No newline at end of file diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index ec9d2b3b..b308cd5f 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -1,7 +1,7 @@
-
+

Selected Units

@@ -14,100 +14,108 @@
-
-

Controls

-
-
-
Speed
-
-
-
-
-
- -
+
+
+

Controls

+
+
+
Speed
+
+
+
+
+
+ +
+
+
+
+
Altitude +
+
+
+
+
+
+ +
+
+
Multiple categories selected
-
-
-
Altitude -
-
-
-
-
-
- -
+ +
+

Rules of engagement

+
+ +
-
Multiple categories selected
-
-
-

Rules of engagement

-
- +
+

Reaction to threat

+
+ +
-
-
-

Reaction to threat

-
- +
+

Radar & ECM

+
+ +
-
-
-

Radar & ECM

-
- +
+

Shots scatter

+
+ +
-
-
-

Shots scatter

-
- +
+

Shots intensity

+
+ +
-
-
-

Shots intensity

-
- +
+

Enable tanker

+
-
-
-

Enable tanker

-
-
+
+

Airborne Early Warning

+
+
-
-

Airborne Early Warning

-
-
+
+

Operate as

+
+
-
-

Operate as

-
-
+
+

Unit active

+
+
-
-

Unit active

-
-
- -
-

Follow roads

-
+
+

Follow roads

+
+

- - + +
+ + + + +
+
diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 480e907d..d1d00b3f 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -28,7 +28,8 @@ Olympus.weapons = {} -- Table holding references to all the currently existing -- Miscellaneous initializations Olympus.missionStartTime = DCS.getRealTime() - +Olympus.napalmCounter = 1 +Olympus.fireCounter = 1 ------------------------------------------------------------------------------------------------------ -- Olympus functions ------------------------------------------------------------------------------------------------------ @@ -434,11 +435,90 @@ function Olympus.smoke(color, lat, lng) end -- Creates an explosion on the ground -function Olympus.explosion(intensity, lat, lng) - Olympus.debug("Olympus.explosion " .. intensity .. " (" .. lat .. ", " .. lng ..")", 2) - trigger.action.explosion(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), intensity) +function Olympus.explosion(intensity, explosionType, lat, lng) + Olympus.debug("Olympus.explosion " .. explosionType .. " " .. intensity .. " (" .. lat .. ", " .. lng ..")", 2) + local pos = coord.LLtoLO(lat, lng, 0) + local vec3 = mist.utils.makeVec3GL(pos) + + if explosionType == "normal" then + trigger.action.explosion(vec3, intensity) + elseif explosionType == "phosphorous" then + Olympus.phosphorous(vec3) + elseif explosionType == "napalm" then + Olympus.napalm(vec3) + elseif explosionType == "secondary" then + Olympus.secondaries(vec3) + elseif explosionType == "fire" then + Olympus.createFire(vec3) + elseif explosionType == "depthCharge" then + + end end +function Olympus.phosphorous(vec3) + trigger.action.explosion(vec3, 1) + for i = 1,math.random(3, 10) do + angle = mist.utils.toRadian((math.random(1, 360))) + local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360))) + trigger.action.signalFlare(randVec, 2, angle) + end +end + +function Olympus.napalm(vec3) + local napeName = "napalmStrike" .. Olympus.napalmCounter + Olympus.napalmCounter = Olympus.napalmCounter + 1 + mist.dynAddStatic( + { + country = 20, + category = 'Fortifications', + hidden = true, + name = napeName, + type ="Fuel tank", + x = vec3.x, + y = vec3.z, + heading = 0, + } -- end of function + ) + timer.scheduleFunction(Olympus.explodeNapalm, vec3, timer.getTime() + 0.1) + timer.scheduleFunction(Olympus.removeNapalm, napeName, timer.getTime() + 0.12) +end + +function Olympus.explodeNapalm(vec3) + trigger.action.explosion(vec3, 10) +end + +function Olympus.removeNapalm(staticName) + StaticObject.getByName(staticName):destroy() +end + +function Olympus.createFire(vec3) + local smokeName = "smokeName" .. Olympus.fireCounter + Olympus.fireCounter = Olympus.fireCounter + 1 + trigger.action.effectSmokeBig(vec3, 2 , 1, smokeName) + trigger.action.explosion(vec3, 1) -- looks wierd to spawn in on flat land without this + timer.scheduleFunction(Olympus.removeFire, smokeName, timer.getTime() + 20) +end + +function Olympus.removeFire (smokeName) + trigger.action.effectSmokeStop(smokeName) +end + +function Olympus.secondaries(vec3) + trigger.action.explosion(vec3, 1) + for i = 1, 10 do + timer.scheduleFunction(Olympus.randomDebries, vec3, timer.getTime() + math.random(0, 180)) + end +end + +function Olympus.randomDebries(vec3) + trigger.action.explosion(vec3, 1) + for i = 1,math.random(3, 10) do + angle = mist.utils.toRadian((math.random(1, 360))) + local randVec = mist.utils.makeVec3GL((mist.getRandPointInCircle(vec3, 5, 1, 0, 360))) + trigger.action.signalFlare(randVec, 3, angle) + end +end + -- Spawns a new unit or group -- Spawn table contains the following parameters -- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit @@ -790,12 +870,16 @@ function Olympus.clone(cloneTable, deleteOriginal) end -- Delete a unit by ID, optionally use an explosion -function Olympus.delete(ID, explosion) +function Olympus.delete(ID, explosion, explosionType) Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) if unit then if unit:getPlayerName() or explosion then - trigger.action.explosion(unit:getPoint() , 250 ) --consider replacing with forcibly deslotting the player, however this will work for now + if explosionType == nil then + explosionType = "normal" + end + local lat, lng, alt = coord.LOtoLL(unit:getPoint()) + Olympus.explosion(250, explosionType, lat, lng) Olympus.debug("Olympus.delete completed successfully", 2) else unit:destroy(); --works for AI units not players diff --git a/src/core/include/commands.h b/src/core/include/commands.h index 6bf3da9d..02707ad0 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -278,10 +278,11 @@ private: class Delete : public Command { public: - Delete(unsigned int ID, bool explosion, bool immediate, function callback = [](){}) : + Delete(unsigned int ID, bool explosion, string explosionType, bool immediate, function callback = [](){}) : Command(callback), ID(ID), explosion(explosion), + explosionType(explosionType), immediate(immediate) { priority = CommandPriority::HIGH; @@ -293,6 +294,7 @@ public: private: const unsigned int ID; const bool explosion; + const string explosionType; const bool immediate; }; @@ -410,10 +412,11 @@ private: class Explosion : public Command { public: - Explosion(unsigned int intensity, Coords location, function callback = [](){}) : + Explosion(unsigned int intensity, string explosionType, Coords location, function callback = [](){}) : Command(callback), location(location), - intensity(intensity) + intensity(intensity), + explosionType(explosionType) { priority = CommandPriority::MEDIUM; }; @@ -423,4 +426,5 @@ public: private: const Coords location; const unsigned int intensity; + const string explosionType; }; diff --git a/src/core/include/unitsmanager.h b/src/core/include/unitsmanager.h index 56b21d18..b3e0f6e7 100644 --- a/src/core/include/unitsmanager.h +++ b/src/core/include/unitsmanager.h @@ -20,7 +20,7 @@ public: void update(json::value& missionData, double dt); void runAILoop(); void getUnitData(stringstream &ss, unsigned long long time); - void deleteUnit(unsigned int ID, bool explosion, bool immediate); + void deleteUnit(unsigned int ID, bool explosion, string explosionType, bool immediate); void acquireControl(unsigned int ID); void loadDatabases(); Unit* getClosestUnit(Unit* unit, unsigned char coalition, vector categories, double &distance); diff --git a/src/core/src/commands.cpp b/src/core/src/commands.cpp index 4ab06ba9..9910ff9d 100644 --- a/src/core/src/commands.cpp +++ b/src/core/src/commands.cpp @@ -165,7 +165,8 @@ string Delete::getString() commandSS.precision(10); commandSS << "Olympus.delete, " << ID << ", " - << (explosion ? "true" : "false"); + << (explosion ? "true" : "false") << ", " + << "\"" << explosionType << "\""; return commandSS.str(); } @@ -244,6 +245,7 @@ string Explosion::getString() commandSS.precision(10); commandSS << "Olympus.explosion, " << intensity << ", " + << "\"" << explosionType << "\"" << ", " << location.lat << ", " << location.lng; return commandSS.str(); diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index f40d156b..a5289b5b 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -408,10 +408,11 @@ void Scheduler::handleRequest(string key, json::value value, string username, js { unsigned int ID = value[L"ID"].as_integer(); bool explosion = value[L"explosion"].as_bool(); + string explosionType = to_string(value[L"explosionType"]); bool immediate = value[L"immediate"].as_bool(); Unit* unit = unitsManager->getUnit(ID); if (unit != nullptr) { - unitsManager->deleteUnit(ID, explosion, immediate); + unitsManager->deleteUnit(ID, explosion, explosionType, immediate); log(username + " deleted unit " + unit->getUnitName() + "(" + unit->getName() + ")", true); } } @@ -498,11 +499,12 @@ void Scheduler::handleRequest(string key, json::value value, string username, js else if (key.compare("explosion") == 0) { unsigned int intensity = value[L"intensity"].as_integer(); + string explosionType = to_string(value[L"explosionType"]); double lat = value[L"location"][L"lat"].as_double(); double lng = value[L"location"][L"lng"].as_double(); - log("Adding " + to_string(intensity) + " explosion at (" + to_string(lat) + ", " + to_string(lng) + ")"); + log("Adding explosion of type " + explosionType + " at (" + to_string(lat) + ", " + to_string(lng) + ")"); Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new Explosion(intensity, loc)); + command = dynamic_cast(new Explosion(intensity, explosionType, loc)); } /************************/ else if (key.compare("bombPoint") == 0) diff --git a/src/core/src/unitsmanager.cpp b/src/core/src/unitsmanager.cpp index 30a5ac20..0e175f4c 100644 --- a/src/core/src/unitsmanager.cpp +++ b/src/core/src/unitsmanager.cpp @@ -142,11 +142,11 @@ void UnitsManager::getUnitData(stringstream &ss, unsigned long long time) p.second->getData(ss, time); } -void UnitsManager::deleteUnit(unsigned int ID, bool explosion, bool immediate) +void UnitsManager::deleteUnit(unsigned int ID, bool explosion, string explosionType, bool immediate) { if (getUnit(ID) != nullptr) { - Command* command = dynamic_cast(new Delete(ID, explosion, immediate)); + Command* command = dynamic_cast(new Delete(ID, explosion, explosionType, immediate)); scheduler->appendCommand(command); } }