diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 0049e34e..dea45dc1 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,39 +1,81 @@ -Olympus = {} Olympus.unitCounter = 1 - +Olympus.payloadRegistry = {} + function Olympus.notify(message, displayFor) trigger.action.outText(message, displayFor) end -function Olympus.move(unitName, lat, lng, altitude, speed, category, targetName) - Olympus.notify("Olympus.move " .. unitName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. speed .. "m/s " .. category .. " target " .. targetName, 10) - local unit = Unit.getByName(unitName) - if unit ~= nil then +-- Gets a unit class reference from a given ObjectID (the ID used by Olympus for unit referencing) +function Olympus.getUnitByID(ID) + for name, table in pairs(mist.DBs.unitsByName) do + local unit = Unit.getByName(name) + if unit and unit:getObjectID() == ID then + return unit + end + end + return nil +end + +function Olympus.getCountryIDByCoalition(coalition) + local countryID = 0 + if coalition == 'red' then + countryID = country.id.RUSSIA + else + countryID = country.id.USA + end + return countryID +end + +function Olympus.getCoalitionByCoalitionID(coalitionID) + local coalition = "neutral" + if coalitionID == 1 then + coalition = "red" + elseif coalitionID == 2 then + coalition = "blue" + end + return coalition +end + +-- Builds a valid task depending on the provided options +function Olympus.buildTask(options) + local task = nil + -- Engage specific target by ID. Checks if target exists. + if options['id'] == 'EngageUnit' and options['targetID'] ~= nil then + local target = Olympus.getUnitByID(options['targetID']) + if target and target:isExist() then + task = { + id = 'EngageUnit', + params = { + unitId = options['targetID'], + } + } + end + end + return task +end + +-- Move a unit. Since most tasks in DCS are Enroute tasks, this function is the main way to control the unit AI +function Olympus.move(ID, lat, lng, altitude, speed, category, taskOptions) + Olympus.notify("Olympus.move " .. ID .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. speed .. "m/s " .. category, 2) + local unit = Olympus.getUnitByID(ID) + if unit then if category == "Aircraft" then local startPoint = mist.getLeadPos(unit:getGroup()) local endPoint = coord.LLtoLO(lat, lng, 0) - local task = nil - if targetName ~= "" then - targetID = Unit.getByName(targetName):getID() - task = { - id = 'EngageUnit', - params = { - unitId = targetID, - } - } - end + local path = { + [1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO'), + [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') + } - local path = {} - path[#path + 1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO') - if task ~= nil then - path[#path].task = task - end - path[#path + 1] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') - if task ~= nil then - path[#path].task = task + -- If a task exists assign it to the controller + local task = Olympus.buildTask(taskOptions) + if task then + path[1].task = task + path[2].task = task end + -- Assign the mission task to the controller local missionTask = { id = 'Mission', params = { @@ -47,7 +89,7 @@ function Olympus.move(unitName, lat, lng, altitude, speed, category, targetName) if groupCon then groupCon:setTask(missionTask) end - Olympus.notify("Olympus.move executed succesfully on a air unit", 10) + Olympus.notify("Olympus.move executed successfully on a Aircraft", 2) elseif category == "GroundUnit" then vars = { @@ -59,17 +101,18 @@ function Olympus.move(unitName, lat, lng, altitude, speed, category, targetName) disableRoads = true } mist.groupToRandomPoint(vars) - Olympus.notify("Olympus.move executed succesfully on a ground unit", 10) + Olympus.notify("Olympus.move executed succesfully on a ground unit", 2) else - Olympus.notify("Olympus.move not implemented yet for " .. category, 10) + Olympus.notify("Olympus.move not implemented yet for " .. category, 2) end else - Olympus.notify("Error in Olympus.move " .. unitName, 10) + Olympus.notify("Error in Olympus.move " .. unitName, 2) end end +-- Creates a simple smoke on the ground function Olympus.smoke(color, lat, lng) - Olympus.notify("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 10) + Olympus.notify("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 2) local colorEnum = nil if color == "green" then colorEnum = trigger.smokeColor.Green @@ -85,14 +128,15 @@ function Olympus.smoke(color, lat, lng) trigger.action.smoke(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), colorEnum) end -function Olympus.spawnGround(coalition, type, lat, lng, ID) - Olympus.notify("Olympus.spawnGround " .. coalition .. " " .. type .. " (" .. lat .. ", " .. lng ..")", 10) +-- Spawns a single ground unit +function Olympus.spawnGroundUnit(coalition, unitType, lat, lng) + Olympus.notify("Olympus.spawnGroundUnit " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) local unitTable = { [1] = { - ["type"] = type, + ["type"] = unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, ["playerCanDrive"] = true, @@ -100,13 +144,8 @@ function Olympus.spawnGround(coalition, type, lat, lng, ID) }, } - local countryID = nil - if coalition == 'red' then - countryID = country.id.RUSSIA - else - countryID = country.id.USA - end - + local countryID = Olympus.getCountryIDByCoalition(coalition) + local vars = { units = unitTable, @@ -116,17 +155,31 @@ function Olympus.spawnGround(coalition, type, lat, lng, ID) } mist.dynAdd(vars) Olympus.unitCounter = Olympus.unitCounter + 1 - Olympus.notify("Olympus.spawnGround completed succesfully", 10) + Olympus.notify("Olympus.spawnGround completed succesfully", 2) end -function Olympus.spawnAir(coalition, unitType, lat, lng, payloadName) - local alt = 5000 - Olympus.notify("Olympus.spawnAir " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..") " .. payloadName, 10) +-- Spawns a single aircraft. Spawn options are: +-- payloadName: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType +-- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase +-- payload: a table, if present the unit will receive this specific payload. Overrides payloadName +function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) + local payloadName = spawnOptions["payloadName"] + local airbaseName = spawnOptions["airbaseName"] + local payload = spawnOptions["payload"] + + Olympus.notify("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) - local payload = {} - if Olympus.unitPayloads[unitType][payloadName] ~= nil then - payload = Olympus.unitPayloads[unitType][payloadName] + + if payload == nil then + if payloadName and payloadName ~= "" and Olympus.unitPayloads[unitType][payloadName] ~= nil then + payload = Olympus.unitPayloads[unitType][payloadName] + else + payload = {} + end end + + local countryID = Olympus.getCountryIDByCoalition(coalition) + local unitTable = { [1] = @@ -134,12 +187,10 @@ function Olympus.spawnAir(coalition, unitType, lat, lng, payloadName) ["type"] = unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, - ["alt"] = alt, ["skill"] = "Excellent", ["payload"] = { ["pylons"] = payload, - ["fuel"] = 4900, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, @@ -151,31 +202,97 @@ function Olympus.spawnAir(coalition, unitType, lat, lng, payloadName) [1] = 1, [2] = 1, [3] = 1, - ["name"] = "Enfield11", + ["name"] = "Olympus" .. Olympus.unitCounter, }, + ["name"] = "Olympus-" .. Olympus.unitCounter }, } - local countryID = nil - if coalition == 'red' then - countryID = country.id.RUSSIA - else - countryID = country.id.USA + -- If a airbase is provided the first waypoint is set as a From runway takeoff. + local route = {} + if airbaseName and airbaseName ~= "" then + local airbase = Airbase.getByName(airbaseName) + if airbase then + local airbaseID = airbase:getID() + route = + { + ["points"] = + { + [1] = + { + ["action"] = "From Runway", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = {["tasks"] = {},}, + }, + ["type"] = "TakeOff", + ["ETA"] = 0, + ["ETA_locked"] = true, + ["x"] = spawnLocation.x, + ["y"] = spawnLocation.z, + ["formation_template"] = "", + ["airdromeId"] = airbaseID, + ["speed_locked"] = true, + }, + }, + } + end end - + local vars = { units = unitTable, country = countryID, category = 'airplane', - task = "CAP", - tasks = {}, name = "Olympus-" .. Olympus.unitCounter, + route = route, + task = 'CAP', } mist.dynAdd(vars) + + -- Save the payload to be reused in case the unit is cloned. TODO: save by ID not by name (it works but I like consistency) + Olympus.payloadRegistry[vars.name] = payload Olympus.unitCounter = Olympus.unitCounter + 1 - Olympus.notify("Olympus.spawnAir completed succesfully", 10) + Olympus.notify("Olympus.spawnAir completed successfully", 2) end -Olympus.notify("OlympusCommand script loaded correctly", 10) \ No newline at end of file +-- Clones a unit by ID. Will clone the unit with the same original payload as the source unit. TODO: only works on Olympus unit not ME units. +function Olympus.clone(ID) + Olympus.notify("Olympus.clone " .. ID, 2) + local unit = Olympus.getUnitByID(ID) + if unit then + local coalition = Olympus.getCoalitionByCoalitionID(unit:getCoalition()) + local lat, lng, alt = coord.LOtoLL(unit:getPoint()) + + -- TODO: only works on Aircraft + local spawnOptions = { + payload = Olympus.payloadRegistry[unitName] + } + Olympus.spawnAircraft(coalition, unit:getTypeName(), lat + 0.001, lng + 0.001, spawnOptions) + end + Olympus.notify("Olympus.clone completed successfully", 2) +end + +function Olympus.follow(leaderID, ID) + Olympus.notify("Olympus.follow " .. ID .. " " .. leaderID, 2) + local leader = Olympus.getUnitByID(leaderID) + local unit = Olympus.getUnitByID(ID) + local followTask = { + id = 'Follow', + params = { + groupId = leader:getGroup():getID(), + pos = {x = 0 , y = 0, z = 20} , + lastWptIndexFlag = false, + lastWptIndex = 1 + } + } + Olympus.notify("Olympus.follow group ID" .. unit:getGroup():getID(), 2) + unit:getGroup():getController():pushTask(followTask) + Olympus.notify("Olympus.follow completed successfully", 2) +end + + + +Olympus.notify("OlympusCommand script loaded successfully", 2) \ No newline at end of file diff --git a/scripts/OlympusMission.lua b/scripts/OlympusMission.lua index 72d588c1..30533cb4 100644 --- a/scripts/OlympusMission.lua +++ b/scripts/OlympusMission.lua @@ -11,8 +11,6 @@ function Olympus.setMissionData(arg, time) local bullseyeVec3 = coalition.getMainRefPoint(0) local bullseyeLatitude, bullseyeLongitude, bullseyeAltitude = coord.LOtoLL(bullseyeVec3) local bullseye = {} - bullseye["x"] = bullseyeVec3.x - bullseye["y"] = bullseyeVec3.z bullseye["lat"] = bullseyeLatitude bullseye["lng"] = bullseyeLongitude @@ -41,9 +39,26 @@ function Olympus.setMissionData(arg, time) end end + -- Airbases data + local base = world.getAirbases() + local basesData = {} + for i = 1, #base do + local info = {} + local latitude, longitude, altitude = coord.LOtoLL(Airbase.getPoint(base[i])) + info["callsign"] = Airbase.getCallsign(base[i]) + info["coalition"] = Airbase.getCoalition(base[i]) + info["lat"] = latitude + info["lng"] = longitude + if Airbase.getUnit(base[i]) then + info["unitId"] = Airbase.getUnit(base[i]):getID() + end + basesData[i] = info + end + -- Assemble missionData table missionData["bullseye"] = bullseye missionData["unitsData"] = unitsData + missionData["airbases"] = basesData local command = "Olympus.missionData = " .. Olympus.serializeTable(missionData) .. "\n" .. "Olympus.OlympusDLL.setMissionData()" net.dostring_in("export", command) @@ -55,8 +70,6 @@ function Olympus.serializeTable(val, name, skipnewlines, depth) depth = depth or 0 local tmp = string.rep(" ", depth) - - if name then if type(name) == "number" then tmp = tmp .. "[" .. name .. "]" .. " = " @@ -67,11 +80,9 @@ function Olympus.serializeTable(val, name, skipnewlines, depth) if type(val) == "table" then tmp = tmp .. "{" .. (not skipnewlines and "\n" or "") - for k, v in pairs(val) do tmp = tmp .. Olympus.serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "") end - tmp = tmp .. string.rep(" ", depth) .. "}" elseif type(val) == "number" then tmp = tmp .. tostring(val) diff --git a/scripts/server.py b/scripts/server.py new file mode 100644 index 00000000..b4da36ea --- /dev/null +++ b/scripts/server.py @@ -0,0 +1,8 @@ +try: + # Python 2 + from SimpleHTTPServer import test, SimpleHTTPRequestHandler +except ImportError: + # Python 3 + from http.server import test, SimpleHTTPRequestHandler + +test(SimpleHTTPRequestHandler) \ No newline at end of file diff --git a/scripts/server.spec b/scripts/server.spec new file mode 100644 index 00000000..886a8932 --- /dev/null +++ b/scripts/server.spec @@ -0,0 +1,44 @@ +# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + + +a = Analysis( + ['server.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='server', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/src/core/include/Commands.h b/src/core/include/Commands.h index 3c10488e..ed01f63b 100644 --- a/src/core/include/Commands.h +++ b/src/core/include/Commands.h @@ -8,7 +8,7 @@ namespace CommandPriority { }; namespace CommandType { - enum CommandTypes { NO_TYPE, MOVE, SMOKE, LASE, EXPLODE, SPAWN_AIR, SPAWN_GROUND }; + enum CommandTypes { NO_TYPE, MOVE, SMOKE, LASE, EXPLODE, SPAWN_AIR, SPAWN_GROUND, CLONE, LAND, REFUEL, FOLLOW }; }; /* Base command class */ @@ -17,7 +17,7 @@ class Command public: int getPriority() { return priority; } int getType() { return type; } - virtual void execute(lua_State* L) = 0; + virtual wstring getString(lua_State* L) = 0; protected: int priority = CommandPriority::LOW; @@ -28,28 +28,26 @@ protected: class MoveCommand : public Command { public: - MoveCommand(int ID, wstring unitName, Coords destination, double speed, double altitude, wstring unitCategory, wstring targetName): - ID(ID), - unitName(unitName), + MoveCommand(int ID, Coords destination, double speed, double altitude, wstring unitCategory, wstring taskOptions): + ID(ID), destination(destination), speed(speed), altitude(altitude), unitCategory(unitCategory), - targetName(targetName) + taskOptions(taskOptions) { - priority = CommandPriority::LOW; + priority = CommandPriority::HIGH; type = CommandType::MOVE; }; - virtual void execute(lua_State* L); + virtual wstring getString(lua_State* L); private: const int ID; - const wstring unitName; const Coords destination; const wstring unitCategory; const double speed; const double altitude; - const wstring targetName; + const wstring taskOptions; }; /* Smoke command */ @@ -63,7 +61,7 @@ public: priority = CommandPriority::LOW; type = CommandType::SMOKE; }; - virtual void execute(lua_State* L); + virtual wstring getString(lua_State* L); private: const wstring color; @@ -71,10 +69,10 @@ private: }; /* Spawn ground unit command */ -class SpawnGroundCommand : public Command +class SpawnGroundUnitCommand : public Command { public: - SpawnGroundCommand(wstring coalition, wstring unitType, Coords location) : + SpawnGroundUnitCommand(wstring coalition, wstring unitType, Coords location) : coalition(coalition), unitType(unitType), location(location) @@ -82,7 +80,7 @@ public: priority = CommandPriority::LOW; type = CommandType::SPAWN_GROUND; }; - virtual void execute(lua_State* L); + virtual wstring getString(lua_State* L); private: const wstring coalition; @@ -91,23 +89,60 @@ private: }; /* Spawn air unit command */ -class SpawnAirCommand : public Command +class SpawnAircraftCommand : public Command { public: - SpawnAirCommand(wstring coalition, wstring unitType, Coords location, wstring payloadName) : + SpawnAircraftCommand(wstring coalition, wstring unitType, Coords location, wstring payloadName, wstring airbaseName) : coalition(coalition), unitType(unitType), location(location), - payloadName(payloadName) + payloadName(payloadName), + airbaseName(airbaseName) { priority = CommandPriority::LOW; type = CommandType::SPAWN_AIR; }; - virtual void execute(lua_State* L); + + virtual wstring getString(lua_State* L); private: const wstring coalition; const wstring unitType; const Coords location; const wstring payloadName; + const wstring airbaseName; +}; + +/* Clone unit command */ +class CloneCommand : public Command +{ +public: + CloneCommand(int ID) : + ID(ID) + { + priority = CommandPriority::LOW; + type = CommandType::CLONE; + }; + virtual wstring getString(lua_State* L); + +private: + const int ID; +}; + +/* Follow command */ +class FollowCommand : public Command +{ +public: + FollowCommand(int leaderID, int ID) : + leaderID(leaderID), + ID(ID) + { + priority = CommandPriority::LOW; + type = CommandType::FOLLOW; + }; + virtual wstring getString(lua_State* L); + +private: + const int leaderID; + const int ID; }; diff --git a/src/core/include/Unit.h b/src/core/include/Unit.h index b826f835..5076df12 100644 --- a/src/core/include/Unit.h +++ b/src/core/include/Unit.h @@ -14,26 +14,27 @@ public: ~Unit(); void update(json::value json); + json::value json(); void setPath(list path); + void setActiveDestination(Coords newActiveDestination) { activeDestination = newActiveDestination; } void setAlive(bool newAlive) { alive = newAlive; } void setTarget(int targetID); - wstring getTarget(); - wstring getCurrentTask(); - - void resetActiveDestination(); + void setLeader(bool newLeader) { leader = newLeader; } + void setWingman(bool newWingman) { wingman = newWingman; } + void setWingmen(vector newWingmen) { wingmen = newWingmen; } + void setFormation(wstring newFormation) { formation = newFormation; } virtual void changeSpeed(wstring change) {}; virtual void changeAltitude(wstring change) {}; - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; + void resetActiveDestination(); int getID() { return ID; } wstring getName() { return name; } wstring getUnitName() { return unitName; } wstring getGroupName() { return groupName; } - json::value getType() { return type; } // This functions returns the complete type of the object (Level1, Level2, Level3, Level4) + json::value getType() { return type; } // This function returns the complete type of the object (Level1, Level2, Level3, Level4) int getCountry() { return country; } int getCoalitionID() { return coalitionID; } double getLatitude() { return latitude; } @@ -42,34 +43,40 @@ public: double getHeading() { return heading; } json::value getFlags() { return flags; } Coords getActiveDestination() { return activeDestination; } - virtual wstring getCategory() { return L"No category"; }; - - json::value json(); + wstring getTarget(); + wstring getCurrentTask() { return currentTask; } + virtual double getTargetSpeed() { return targetSpeed; }; + virtual double getTargetAltitude() { return targetAltitude; }; protected: int ID; - bool AI = false; - bool alive = true; - wstring name = L"undefined"; - wstring unitName = L"undefined"; - wstring groupName = L"undefined"; - json::value type = json::value::null(); - int country = NULL; - int coalitionID = NULL; - double latitude = NULL; - double longitude = NULL; - double altitude = NULL; - double heading = NULL; - double speed = NULL; - json::value flags = json::value::null(); - Coords oldPosition = Coords(0); // Used to approximate speed - int targetID = NULL; - bool holding = false; - bool looping = false; - - double targetSpeed = 0; - double targetAltitude = 0; + bool AI = false; + bool alive = true; + wstring name = L"undefined"; + wstring unitName = L"undefined"; + wstring groupName = L"undefined"; + json::value type = json::value::null(); + int country = NULL; + int coalitionID = NULL; + double latitude = NULL; + double longitude = NULL; + double altitude = NULL; + double heading = NULL; + double speed = NULL; + json::value flags = json::value::null(); + Coords oldPosition = Coords(0); // Used to approximate speed + int targetID = NULL; + bool holding = false; + bool looping = false; + wstring taskOptions = L"{}"; + wstring currentTask = L""; + bool leader = false; + bool wingman = false; + wstring formation = L""; + vector wingmen; + double targetSpeed = 0; + double targetAltitude = 0; list activePath; Coords activeDestination = Coords(0); diff --git a/src/core/include/scriptLoader.h b/src/core/include/scriptLoader.h index d0d66634..e1f01dd1 100644 --- a/src/core/include/scriptLoader.h +++ b/src/core/include/scriptLoader.h @@ -1,4 +1,13 @@ #pragma once #include "framework.h" +#define PROTECTED_CALL "Olympus = {}\n \ + function Olympus.protectedCall(...)\n\n \ + local status, retval = pcall(...)\n \ + if not status then\n \ + trigger.action.outText(\"ERROR: \" ..retval, 20)\n \ + end\n \ + end\n \ + trigger.action.outText(\"Olympus.protectedCall registered successfully\", 10)\n" + void registerLuaFunctions(lua_State* L); diff --git a/src/core/src/Commands.cpp b/src/core/src/Commands.cpp index 1841e9c0..a557730b 100644 --- a/src/core/src/Commands.cpp +++ b/src/core/src/Commands.cpp @@ -1,86 +1,87 @@ #include "commands.h" #include "logger.h" +#include "dcstools.h" /* Move command */ -void MoveCommand::execute(lua_State* L) +wstring MoveCommand::getString(lua_State* L) { - std::ostringstream command; - command.precision(10); - command << "Olympus.move(\"" << to_string(unitName) << "\", " << destination.lat << ", " << destination.lng << ", " << altitude << ", " << speed << ", \"" << to_string(unitCategory) << "\", \"" << to_string(targetName) << "\")"; - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, command.str().c_str()); - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error executing MoveCommand"); - } - else - { - log("MoveCommand executed successfully"); - } + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.move, " + << ID << ", " + << destination.lat << ", " + << destination.lng << ", " + << altitude << ", " + << speed << ", " + << "\"" << unitCategory << "\"" << ", " + << taskOptions; + return commandSS.str(); } /* Smoke command */ -void SmokeCommand::execute(lua_State* L) +wstring SmokeCommand::getString(lua_State* L) { - std::ostringstream command; - command.precision(10); - command << "Olympus.smoke(\"" << to_string(color) << "\", " << location.lat << ", " << location.lng << ")"; - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, command.str().c_str()); - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error executing SmokeCommand"); - } - else - { - log("SmokeCommand executed successfully"); - } + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.smoke, " + << "\"" << color << "\"" << ", " + << location.lat << ", " + << location.lng; + return commandSS.str(); } /* Spawn ground command */ -void SpawnGroundCommand::execute(lua_State* L) +wstring SpawnGroundUnitCommand::getString(lua_State* L) { - std::ostringstream command; - command.precision(10); - command << "Olympus.spawnGround(\"" << to_string(coalition) << "\", \"" << to_string(unitType) << "\", " << location.lat << ", " << location.lng << ")"; - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, command.str().c_str()); - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error executing SpawnGroundCommand"); - } - else - { - log("SpawnGroundCommand executed successfully"); - } + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.spawnGroundUnit, " + << "\"" << coalition << "\"" << ", " + << "\"" << unitType << "\"" << ", " + << location.lat << ", " + << location.lng; + return commandSS.str(); } /* Spawn air command */ -void SpawnAirCommand::execute(lua_State* L) +wstring SpawnAircraftCommand::getString(lua_State* L) { - std::ostringstream command; - command.precision(10); - command << "Olympus.spawnAir(\"" << to_string(coalition) << "\", \"" << to_string(unitType) << "\", " << location.lat << ", " << location.lng << "," << "\"" << to_string(payloadName) << "\")"; + std::wostringstream optionsSS; + optionsSS.precision(10); + optionsSS << "{" + << "payloadName = \"" << payloadName << "\", " + << "airbaseName = \"" << airbaseName << "\"," + << "}"; - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, command.str().c_str()); - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error executing SpawnAirCommand"); - } - else - { - log("SpawnAirCommand executed successfully"); - } + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.spawnAircraft, " + << "\"" << coalition << "\"" << ", " + << "\"" << unitType << "\"" << ", " + << location.lat << ", " + << location.lng << "," + << optionsSS.str(); + return commandSS.str(); } + +/* Clone unit command */ +wstring CloneCommand::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.clone, " + << ID; + return commandSS.str(); +} + +/* Follow unit command */ +wstring FollowCommand::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.follow, " + << leaderID << "," + << ID; + + return commandSS.str(); +} \ No newline at end of file diff --git a/src/core/src/Scheduler.cpp b/src/core/src/Scheduler.cpp index 38304424..12a7903a 100644 --- a/src/core/src/Scheduler.cpp +++ b/src/core/src/Scheduler.cpp @@ -33,42 +33,15 @@ void Scheduler::execute(lua_State* L) { if (command->getPriority() == priority) { - log("Executing command"); - switch (command->getType()) + wstring commandString = L"Olympus.protectedCall(" + command->getString(L) + L")"; + if (dostring_in(L, "server", to_string(commandString))) { - case CommandType::MOVE: - { - MoveCommand* moveCommand = dynamic_cast(command); - moveCommand->execute(L); - commands.remove(command); - break; - } - case CommandType::SMOKE: - { - SmokeCommand* smokeCommand = dynamic_cast(command); - smokeCommand->execute(L); - commands.remove(command); - break; - } - case CommandType::SPAWN_GROUND: - { - SpawnGroundCommand* spawnCommand = dynamic_cast(command); - spawnCommand->execute(L); - commands.remove(command); - break; - } - case CommandType::SPAWN_AIR: - { - SpawnAirCommand* spawnCommand = dynamic_cast(command); - spawnCommand->execute(L); - commands.remove(command); - break; - } - default: - log("Unknown command of type " + to_string(command->getType())); - commands.remove(command); - break; + log(L"Error executing command " + commandString); } + { + log(L"Command " + commandString + L" executed succesfully"); + } + commands.remove(command); return; } } @@ -130,7 +103,7 @@ void Scheduler::handleRequest(wstring key, json::value value) double lng = value[L"location"][L"lng"].as_double(); log(L"Spawning " + coalition + L" ground unit of type " + type + L" at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new SpawnGroundCommand(coalition, type, loc)); + command = dynamic_cast(new SpawnGroundUnitCommand(coalition, type, loc)); } else if (key.compare(L"spawnAir") == 0) { @@ -140,15 +113,16 @@ void Scheduler::handleRequest(wstring key, json::value value) double lng = value[L"location"][L"lng"].as_double(); Coords loc; loc.lat = lat; loc.lng = lng; wstring payloadName = value[L"payloadName"].as_string(); - log(L"Spawning " + coalition + L" air unit of type " + type + L" with payload " + payloadName + L" at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); - command = dynamic_cast(new SpawnAirCommand(coalition, type, loc, payloadName)); + wstring airbaseName = value[L"airbaseName"].as_string(); + log(L"Spawning " + coalition + L" air unit of type " + type + L" with payload " + payloadName + L" at (" + to_wstring(lat) + L", " + to_wstring(lng) + L" " + airbaseName + L")"); + command = dynamic_cast(new SpawnAircraftCommand(coalition, type, loc, payloadName, airbaseName)); } else if (key.compare(L"attackUnit") == 0) { - int unitID = value[L"unitID"].as_integer(); + int ID = value[L"ID"].as_integer(); int targetID = value[L"targetID"].as_integer(); - Unit* unit = unitsFactory->getUnit(unitID); + Unit* unit = unitsFactory->getUnit(ID); Unit* target = unitsFactory->getUnit(targetID); wstring unitName; @@ -193,6 +167,43 @@ void Scheduler::handleRequest(wstring key, json::value value) unit->changeAltitude(value[L"change"].as_string()); } } + else if (key.compare(L"cloneUnit") == 0) + { + int ID = value[L"ID"].as_integer(); + command = dynamic_cast(new CloneCommand(ID)); + log(L"Cloning unit " + to_wstring(ID)); + } + else if (key.compare(L"setLeader") == 0) + { + int ID = value[L"ID"].as_integer(); + Unit* unit = unitsFactory->getUnit(ID); + json::value wingmenIDs = value[L"wingmenIDs"]; + vector wingmen; + if (unit != nullptr) + { + for (auto itr = wingmenIDs.as_array().begin(); itr != wingmenIDs.as_array().end(); itr++) + { + Unit* wingman = unitsFactory->getUnit(itr->as_integer()); + if (wingman != nullptr) + { + wingman->setWingman(true); + wingmen.push_back(wingman); + log(L"Setting " + wingman->getName() + L" as wingman leader"); + } + } + unit->setWingmen(wingmen); + unit->setLeader(true); + unit->resetActiveDestination(); + log(L"Setting " + unit->getName() + L" as formation leader"); + } + } + else if (key.compare(L"setFormation") == 0) + { + int ID = value[L"ID"].as_integer(); + Unit* unit = unitsFactory->getUnit(ID); + wstring formation = value[L"formation"].as_string(); + unit->setFormation(formation); + } else { log(L"Unknown command: " + key); diff --git a/src/core/src/Unit.cpp b/src/core/src/Unit.cpp index a0693075..e25db73c 100644 --- a/src/core/src/Unit.cpp +++ b/src/core/src/Unit.cpp @@ -116,59 +116,46 @@ wstring Unit::getTarget() } } -wstring Unit::getCurrentTask() +void Unit::AIloop() { - if (activePath.size() == 0) + // For wingman units, the leader decides the active destination + if (!wingman) { - return L"Idle"; - } - else - { - if (getTarget().empty()) + /* Set the active destination to be always equal to the first point of the active path. This is in common with all AI units */ + if (activePath.size() > 0) { - if (looping) + if (activeDestination != activePath.front()) { - return L"Looping"; - } - else if (holding) - { - return L"Holding"; - } - else - { - return L"Reaching destination"; + activeDestination = activePath.front(); + Command* command = dynamic_cast(new MoveCommand(ID, activeDestination, getTargetSpeed(), getTargetAltitude(), getCategory(), taskOptions)); + scheduler->appendCommand(command); + + if (leader) + { + for (auto itr = wingmen.begin(); itr != wingmen.end(); itr++) + { + // Manually set the path and the active destination of the wingmen + (*itr)->setPath(activePath); + (*itr)->setActiveDestination(activeDestination); + Command* command = dynamic_cast(new FollowCommand(ID, (*itr)->getID())); + scheduler->appendCommand(command); + } + } } } else { - return L"Attacking " + getTarget(); + if (activeDestination != NULL) + { + log(unitName + L" no more points in active path"); + activeDestination = Coords(0); // Set the active path to NULL + currentTask = L"Idle"; + } } } } -void Unit::AIloop() -{ - /* Set the active destination to be always equal to the first point of the active path. This is in common with all AI units */ - if (activePath.size() > 0) - { - if (activeDestination != activePath.front()) - { - activeDestination = activePath.front(); - Command* command = dynamic_cast(new MoveCommand(ID, unitName, activeDestination, getTargetSpeed(), getTargetAltitude(), getCategory(), getTarget())); - scheduler->appendCommand(command); - } - } - else - { - if (activeDestination != NULL) - { - log(unitName + L" no more points in active path"); - activeDestination = Coords(0); // Set the active path to NULL - } - } -} - -/* This function calls again the MoveCommand to reach the active destination. This is useful to change speed and altitude, for example */ +/* This function reset the activation so that the AI lopp will call again the MoveCommand. This is useful to change speed and altitude, for example */ void Unit::resetActiveDestination() { log(unitName + L" resetting active destination"); @@ -197,6 +184,15 @@ json::value Unit::json() json[L"flags"] = flags; json[L"category"] = json::value::string(getCategory()); json[L"currentTask"] = json::value::string(getCurrentTask()); + json[L"leader"] = leader; + json[L"wingman"] = wingman; + json[L"formation"] = json::value::string(formation); + + int i = 0; + for (auto itr = wingmen.begin(); itr != wingmen.end(); itr++) + { + json[L"wingmenIDs"][i++] = (*itr)->getID(); + } /* Send the active path as a json object */ if (activePath.size() > 0) { @@ -224,6 +220,21 @@ AirUnit::AirUnit(json::value json, int ID) : Unit(json, ID) void AirUnit::AIloop() { + if (targetID != 0) + { + std::wostringstream taskOptionsSS; + taskOptionsSS << "{" + << "id = 'EngageUnit'" << "," + << "targetID = " << targetID << "," + << "}"; + taskOptions = taskOptionsSS.str(); + currentTask = L"Attacking " + getTarget(); + } + else + { + currentTask = L"Reaching destination"; + } + /* Call the common AI loop */ Unit::AIloop(); @@ -257,6 +268,7 @@ void AirUnit::AIloop() activePath.push_back(point3); activePath.push_back(Coords(latitude, longitude)); holding = true; + currentTask = L"Holding"; } } diff --git a/src/core/src/scriptLoader.cpp b/src/core/src/scriptLoader.cpp index 7463b6ec..2cf5a134 100644 --- a/src/core/src/scriptLoader.cpp +++ b/src/core/src/scriptLoader.cpp @@ -1,12 +1,39 @@ #include "scriptLoader.h" #include "logger.h" #include "luatools.h" +#include "dcstools.h" + +#include + +bool executeLuaScript(lua_State* L, string path) +{ + replace(path.begin(), path.end(), '\\', '/'); + // Encase the loading call in a procted call to catch any syntax errors + string str = "Olympus.protectedCall(dofile, \"" + path + "\")"; + if (dostring_in(L, "server", str.c_str()) != 0) + { + log("Error registering " + path); + } + else + { + log(path + " registered successfully"); + } +} /* Executes the "OlympusCommand.lua" file to load in the "Server" Lua space all the Lua functions necessary to perform DCS commands (like moving units) */ void registerLuaFunctions(lua_State* L) { string modLocation; + if (dostring_in(L, "server", PROTECTED_CALL)) + { + log("Error registering protectedCall"); + } + else + { + log("protectedCall registered successfully"); + } + char* buf = nullptr; size_t sz = 0; if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr) @@ -20,97 +47,7 @@ void registerLuaFunctions(lua_State* L) return; } - { - ifstream f(modLocation + "\\Scripts\\mist_4_4_90.lua"); - string str; - log("Reading MIST from " + modLocation + "\\Scripts\\mist_4_4_90.lua"); - if (f) { - ostringstream ss; - ss << f.rdbuf(); - str = ss.str(); - log("MIST read succesfully"); - } - else - { - log("Error reading MIST"); - return; - } - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, str.c_str()); - - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error registering MIST"); - } - else - { - log("MIST registered successfully"); - } - } - - { - ifstream f(modLocation + "\\Scripts\\OlympusCommand.lua"); - string str; - log("Reading OlympusCommand.lua from " + modLocation + "\\Scripts\\OlympusCommand.lua"); - if (f) { - ostringstream ss; - ss << f.rdbuf(); - str = ss.str(); - log("OlympusCommand.lua read succesfully"); - } - else - { - log("Error reading OlympusCommand.lua"); - return; - } - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, str.c_str()); - - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error registering OlympusCommand.lua"); - } - else - { - log("OlympusCommand.lua registered successfully"); - } - } - - { - ifstream f(modLocation + "\\Scripts\\unitPayloads.lua"); - string str; - log("Reading unitPayloads.lua from " + modLocation + "\\Scripts\\unitPayloads.lua"); - if (f) { - ostringstream ss; - ss << f.rdbuf(); - str = ss.str(); - log("unitPayloads.lua read succesfully"); - } - else - { - log("Error reading unitPayloads.lua"); - return; - } - - lua_getglobal(L, "net"); - lua_getfield(L, -1, "dostring_in"); - lua_pushstring(L, "server"); - lua_pushstring(L, str.c_str()); - - if (lua_pcall(L, 2, 0, 0) != 0) - { - log("Error registering unitPayloads.lua"); - } - else - { - log("unitPayloads.lua registered successfully"); - } - } - + executeLuaScript(L, modLocation + "\\Scripts\\mist_4_4_90.lua"); + executeLuaScript(L, modLocation + "\\Scripts\\OlympusCommand.lua"); + executeLuaScript(L, modLocation + "\\Scripts\\unitPayloads.lua"); } \ No newline at end of file diff --git a/src/dcstools/include/dcstools.h b/src/dcstools/include/dcstools.h index 128618b4..b4866db4 100644 --- a/src/dcstools/include/dcstools.h +++ b/src/dcstools/include/dcstools.h @@ -6,6 +6,6 @@ void DllExport LogInfo(lua_State* L, string message); void DllExport LogWarning(lua_State* L, string message); void DllExport LogError(lua_State* L, string message); void DllExport Log(lua_State* L, string message, int level); - +int DllExport dostring_in(lua_State* L, string target, string command); map DllExport getAllUnits(lua_State* L); diff --git a/src/dcstools/src/dcstools.cpp b/src/dcstools/src/dcstools.cpp index 1cd73022..3995f9b5 100644 --- a/src/dcstools/src/dcstools.cpp +++ b/src/dcstools/src/dcstools.cpp @@ -92,4 +92,14 @@ map getAllUnits(lua_State* L) exit: STACK_CLEAN; return units; +} + + +int dostring_in(lua_State* L, string target, string command) +{ + lua_getglobal(L, "net"); + lua_getfield(L, -1, "dostring_in"); + lua_pushstring(L, target.c_str()); + lua_pushstring(L, command.c_str()); + return lua_pcall(L, 2, 0, 0); } \ No newline at end of file diff --git a/www/css/AirbaseMarker.css b/www/css/AirbaseMarker.css new file mode 100644 index 00000000..05499fac --- /dev/null +++ b/www/css/AirbaseMarker.css @@ -0,0 +1,37 @@ +.airbasemarker-container-table { + height: 60px; + width: 60px; + left: -30px; + top: -30px; + border: 1px transparent solid; + position: absolute; +} + +.airbasemarker-icon-img { + height: 60px; + width: 60px; + left: 0px; + top: 0px; + display: block; + opacity: 0.8; + position: absolute; + filter: drop-shadow(1px 1px 0 white) drop-shadow(1px -1px 0 white) drop-shadow(-1px 1px 0 white) drop-shadow(-1px -1px 0 white); +} + +.airbasemarker-icon-img-blue { + filter: invert(37%) sepia(21%) saturate(7402%) hue-rotate(193deg) brightness(103%) contrast(104%) drop-shadow(1px 1px 0 white) drop-shadow(1px -1px 0 white) drop-shadow(-1px 1px 0 white) drop-shadow(-1px -1px 0 white); +} + +.airbasemarker-icon-img-red { + filter: invert(21%) sepia(96%) saturate(4897%) hue-rotate(353deg) brightness(108%) contrast(90%) drop-shadow(1px 1px 0 white) drop-shadow(1px -1px 0 white) drop-shadow(-1px 1px 0 white) drop-shadow(-1px -1px 0 white); +} + +.airbasemarker-name-div { + bottom: -20px; + position: absolute; + text-align: center; + font: 800 14px Arial; + white-space: nowrap; + -webkit-text-fill-color: white; + -webkit-text-stroke: 1px; +} diff --git a/www/css/index.css b/www/css/index.css index 277383eb..b7afb9cf 100644 --- a/www/css/index.css +++ b/www/css/index.css @@ -40,3 +40,47 @@ body{ color: rgb(255, 154, 154); text-shadow: 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000; } + + /* The snackbar - position it at the bottom and in the middle of the screen */ + #snackbar { + visibility: hidden; /* Hidden by default. Visible on click */ + min-width: 250px; /* Set a default minimum width */ + background-color: #2d3e50; /* Black background color */ + color: #fff; /* White text color */ + text-align: center; /* Centered text */ + border-radius: 2px; /* Rounded borders */ + padding: 16px; /* Padding */ + position: fixed; /* Sit on top of the screen */ + z-index: 1000; /* Add a z-index if needed */ + top: 120px; /* 30px from the bottom */ +} + +/* Show the snackbar when clicking on a button (class added with JavaScript) */ +#snackbar.show { + visibility: visible; /* Show the snackbar */ + /* Add animation: Take 0.5 seconds to fade in and out the snackbar. + However, delay the fade out process for 2.5 seconds */ + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +/* Animations to fade the snackbar in and out */ +@-webkit-keyframes fadein { + from {top: 0; opacity: 0;} + to {top: 120px; opacity: 1;} +} + +@keyframes fadein { + from {top: 0; opacity: 0;} + to {top: 120px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {top: 120px; opacity: 1;} + to {top: 0; opacity: 0;} +} + +@keyframes fadeout { + from {top: 120px; opacity: 1;} + to {top: 0; opacity: 0;} +} \ No newline at end of file diff --git a/www/css/map.css b/www/css/map.css index 3a5597ff..bb554e89 100644 --- a/www/css/map.css +++ b/www/css/map.css @@ -6,3 +6,23 @@ cursor: crosshair; } +.leaflet-container.move-cursor-enabled { + cursor: crosshair; +} + +.leaflet-container.attack-cursor-enabled { + cursor: url("../img/buttons/attack.png") 25 15,auto; +} + +.leaflet-marker-icon.attack-cursor-enabled { + cursor: url("../img/buttons/attack.png") 25 15,auto; +} + +.leaflet-container.formation-cursor-enabled { + cursor: url("../img/buttons/formation.png") 25 15,auto; +} + +.leaflet-marker-icon.formation-cursor-enabled { + cursor: url("../img/buttons/formation.png") 25 15,auto; +} + diff --git a/www/css/panels.css b/www/css/panels.css index 539955a0..802ea210 100644 --- a/www/css/panels.css +++ b/www/css/panels.css @@ -8,14 +8,14 @@ position: fixed; z-index: 1000; left: 10px; - bottom: -80px; + bottom: -102px; transition: bottom 0.2s; } .unit-control-panel { background-color: #202831; height: 40px; - width: 400px; + width: 200px; border: solid white 1px; font-size: 12px; position: fixed; @@ -28,7 +28,23 @@ justify-content: space-evenly; } -.control-panel { +.action-panel { + background-color: #202831; + height: 400px; + width: 40px; + border: solid white 1px; + font-size: 12px; + position: fixed; + z-index: 1000; + left: 10px; + top: 50px; + transition: height 0.2s; + display: flex; + align-content: flex-start; + flex-direction: column; +} + +.settings-panel { background-color: #202831; height: 40px; width: 100%; @@ -38,6 +54,19 @@ justify-content: left; } +.formation-control-panel { + background-color: #202831; + height: 100px; + width: 300px; + border: solid white 1px; + font-size: 12px; + position: fixed; + z-index: 1000; + left: 830px; + bottom: -102px; + transition: bottom 0.2s; +} + .panel-table { text-shadow: 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000; height: 100%; @@ -70,8 +99,6 @@ display: flex; align-items: center; justify-content: space-evenly; - transition: font-size 0.05s; - text-shadow: 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000; } .panel-button:hover { @@ -80,6 +107,7 @@ .panel-button:active { color: white; + filter: brightness(110%); } .panel-button-disabled { diff --git a/www/img/airbase.png b/www/img/airbase.png new file mode 100644 index 00000000..874174d6 Binary files /dev/null and b/www/img/airbase.png differ diff --git a/www/img/buttons/attack.png b/www/img/buttons/attack.png new file mode 100644 index 00000000..82a3733f Binary files /dev/null and b/www/img/buttons/attack.png differ diff --git a/www/img/buttons/bomb.png b/www/img/buttons/bomb.png new file mode 100644 index 00000000..823ddf6c Binary files /dev/null and b/www/img/buttons/bomb.png differ diff --git a/www/img/buttons/carpet.png b/www/img/buttons/carpet.png new file mode 100644 index 00000000..1fe51a1b Binary files /dev/null and b/www/img/buttons/carpet.png differ diff --git a/www/img/buttons/formation.png b/www/img/buttons/formation.png new file mode 100644 index 00000000..66b46a46 Binary files /dev/null and b/www/img/buttons/formation.png differ diff --git a/www/img/buttons/land.png b/www/img/buttons/land.png new file mode 100644 index 00000000..525c34cf Binary files /dev/null and b/www/img/buttons/land.png differ diff --git a/www/img/buttons/spawnAWACS.png b/www/img/buttons/spawnAWACS.png new file mode 100644 index 00000000..b325f4ad Binary files /dev/null and b/www/img/buttons/spawnAWACS.png differ diff --git a/www/img/buttons/spawnCAP.png b/www/img/buttons/spawnCAP.png new file mode 100644 index 00000000..ae78e677 Binary files /dev/null and b/www/img/buttons/spawnCAP.png differ diff --git a/www/img/buttons/spawnCAS.png b/www/img/buttons/spawnCAS.png new file mode 100644 index 00000000..315bd363 Binary files /dev/null and b/www/img/buttons/spawnCAS.png differ diff --git a/www/img/buttons/spawnDrone.png b/www/img/buttons/spawnDrone.png new file mode 100644 index 00000000..98beca17 Binary files /dev/null and b/www/img/buttons/spawnDrone.png differ diff --git a/www/img/buttons/spawnStrike.png b/www/img/buttons/spawnStrike.png new file mode 100644 index 00000000..1cba2589 Binary files /dev/null and b/www/img/buttons/spawnStrike.png differ diff --git a/www/img/buttons/spawnTanker.png b/www/img/buttons/spawnTanker.png new file mode 100644 index 00000000..374fb248 Binary files /dev/null and b/www/img/buttons/spawnTanker.png differ diff --git a/www/img/buttons/spawnTransport.png b/www/img/buttons/spawnTransport.png new file mode 100644 index 00000000..b5932d58 Binary files /dev/null and b/www/img/buttons/spawnTransport.png differ diff --git a/www/img/buttons/wheelButtons.xcf b/www/img/buttons/wheelButtons.xcf index d7bea0bd..f1e3791a 100644 Binary files a/www/img/buttons/wheelButtons.xcf and b/www/img/buttons/wheelButtons.xcf differ diff --git a/www/index.html b/www/index.html index 0440874d..3a1c9d27 100644 --- a/www/index.html +++ b/www/index.html @@ -7,6 +7,7 @@ + @@ -18,36 +19,48 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
-
-
+
+
+
+
+
ASD
diff --git a/www/js/DCSCommands.js b/www/js/DCS/DCSCommands.js similarity index 70% rename from www/js/DCSCommands.js rename to www/js/DCS/DCSCommands.js index 49e9b9cb..ca29c51b 100644 --- a/www/js/DCSCommands.js +++ b/www/js/DCS/DCSCommands.js @@ -32,7 +32,7 @@ function spawnGroundUnit(type, latlng, coalition) xhr.send(JSON.stringify(data)); } -function spawnAirUnit(type, latlng, coalition, payloadName) +function spawnAircraft(type, latlng, coalition, payloadName = "", airbaseName = "") { var xhr = new XMLHttpRequest(); xhr.open("PUT", RESTaddress); @@ -43,25 +43,42 @@ function spawnAirUnit(type, latlng, coalition, payloadName) } }; - var command = {"type": type, "location": latlng, "coalition": coalition, "payloadName": payloadName}; + var command = {"type": type, "location": latlng, "coalition": coalition, "payloadName": payloadName, "airbaseName": airbaseName}; var data = {"spawnAir": command} xhr.send(JSON.stringify(data)); } -function attackUnit(unitID, targetID) +function attackUnit(ID, targetID) { var xhr = new XMLHttpRequest(); xhr.open("PUT", RESTaddress); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { - console.log("Unit " + unitsManager.getUnit(unitID).unitName + " attack " + unitsManager.getUnit(targetID).unitName ); + console.log("Unit " + unitsManager.getUnitByID(ID).unitName + " attack " + unitsManager.getUnitByID(targetID).unitName ); } }; - var command = {"unitID": unitID, "targetID": targetID}; + var command = {"ID": ID, "targetID": targetID}; var data = {"attackUnit": command} + xhr.send(JSON.stringify(data)); +} + +function cloneUnit(ID) +{ + var xhr = new XMLHttpRequest(); + xhr.open("PUT", RESTaddress); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + console.log("Unit " + unitsManager.getUnitByID(ID).unitName + " cloned"); + } + }; + + var command = {"ID": ID}; + var data = {"cloneUnit": command} + xhr.send(JSON.stringify(data)); } \ No newline at end of file diff --git a/www/js/payloadNames.js b/www/js/DCS/payloadNames.js similarity index 100% rename from www/js/payloadNames.js rename to www/js/DCS/payloadNames.js diff --git a/www/js/Map.js b/www/js/Map/Map.js similarity index 63% rename from www/js/Map.js rename to www/js/Map/Map.js index 6945bb29..5db4471d 100644 --- a/www/js/Map.js +++ b/www/js/Map/Map.js @@ -17,7 +17,10 @@ class Map this._map.on('movestart', () => {this.removeSelectionWheel(); this.removeSelectionScroll();}); this._map.on('zoomstart', () => {this.removeSelectionWheel(); this.removeSelectionScroll();}); this._map.on('selectionend', (e) => unitsManager.selectFromBounds(e.selectionBounds)); - + this._map.on('keyup', (e) => unitsManager.handleKeyEvent(e)); + + this._map._container.classList.add("action-cursor"); + this.setState("IDLE"); this._selectionWheel = undefined; @@ -52,14 +55,42 @@ class Map setState(newState) { this._state = newState; + + var cursorElements = document.getElementsByClassName("action-cursor"); + for (let item of cursorElements) + { + item.classList.remove("move-cursor-enabled", "attack-cursor-enabled", "formation-cursor-enabled"); + } if (this._state === "IDLE") { - L.DomUtil.removeClass(this._map._container, 'move-cursor-enabled'); + } - else if (this._state === "UNIT_SELECTED") + else if (this._state === "MOVE_UNIT") { - L.DomUtil.addClass(this._map._container, 'move-cursor-enabled'); + for (let item of cursorElements) + { + item.classList.add("move-cursor-enabled"); + } } + else if (this._state === "ATTACK") + { + for (let item of cursorElements) + { + item.classList.add("attack-cursor-enabled"); + } + } + else if (this._state === "FORMATION") + { + for (let item of cursorElements) + { + item.classList.add("formation-cursor-enabled"); + } + } + } + + getState() + { + return this._state; } /* Set the active coalition (for persistency) */ @@ -77,6 +108,7 @@ class Map // Right click _onContextMenu(e) { + this.setState("IDLE"); unitsManager.deselectAllUnits(); this.removeSelectionWheel(); this.removeSelectionScroll(); @@ -90,7 +122,7 @@ class Map { } - else if (this._state === "UNIT_SELECTED") + else if (this._state === "MOVE_UNIT") { if (!e.originalEvent.ctrlKey) { @@ -102,13 +134,16 @@ class Map _onDoubleClick(e) { - var options = [ - {'tooltip': 'Air unit', 'src': 'spawnAir.png', 'callback': () => this._unitSelectAir(e)}, - {'tooltip': 'Ground unit', 'src': 'spawnGround.png', 'callback': () => this._groundSpawnMenu(e)}, - {'tooltip': 'Smoke', 'src': 'spawnSmoke.png', 'callback': () => this._smokeSpawnMenu(e)}, - {'tooltip': 'Explosion', 'src': 'spawnExplosion.png', 'callback': () => this._explosionSpawnMenu(e)} - ] - this._selectionWheel = new SelectionWheel(e.originalEvent.x, e.originalEvent.y, options); + if (this._state == 'IDLE') + { + var options = [ + {'tooltip': 'Air unit', 'src': 'spawnAir.png', 'callback': () => this._aircraftSpawnMenu(e)}, + {'tooltip': 'Ground unit', 'src': 'spawnGround.png', 'callback': () => this._groundUnitSpawnMenu(e)}, + {'tooltip': 'Smoke', 'src': 'spawnSmoke.png', 'callback': () => this._smokeSpawnMenu(e)}, + {'tooltip': 'Explosion', 'src': 'spawnExplosion.png', 'callback': () => this._explosionSpawnMenu(e)} + ] + this._selectionWheel = new SelectionWheel(e.originalEvent.x, e.originalEvent.y, options); + } } /* Selection wheel and selection scroll functions */ @@ -130,30 +165,42 @@ class Map } } - /* Spawn a new air unit selection wheel (TODO, divide units by type, like bomber, fighter, tanker etc)*/ - _airSpawnMenu(e) + /* Show unit selection for air units */ + spawnFromAirbase(e) + { + this._selectAircraft(e); + } + + /* Spawn a new ground unit selection wheel */ + _aircraftSpawnMenu(e) { this.removeSelectionWheel(); this.removeSelectionScroll(); var options = [ - + {'coalition': true, 'tooltip': 'CAP', 'src': 'spawnCAP.png', 'callback': () => this._selectAircraft(e, "CAP")}, + {'coalition': true, 'tooltip': 'CAS', 'src': 'spawnCAS.png', 'callback': () => this._selectAircraft(e, "CAS")}, + {'coalition': true, 'tooltip': 'Tanker', 'src': 'spawnTanker.png', 'callback': () => this._selectAircraft(e, "tanker")}, + {'coalition': true, 'tooltip': 'AWACS', 'src': 'spawnAWACS.png', 'callback': () => this._selectAircraft(e, "awacs")}, + {'coalition': true, 'tooltip': 'Strike', 'src': 'spawnStrike.png', 'callback': () => this._selectAircraft(e, "strike")}, + {'coalition': true, 'tooltip': 'Drone', 'src': 'spawnDrone.png', 'callback': () => this._selectAircraft(e, "drone")}, + {'coalition': true, 'tooltip': 'Transport', 'src': 'spawnTransport.png','callback': () => this._selectAircraft(e, "transport")}, ] this._selectionWheel = new SelectionWheel(e.originalEvent.x, e.originalEvent.y, options); } /* Spawn a new ground unit selection wheel */ - _groundSpawnMenu(e) + _groundUnitSpawnMenu(e) { this.removeSelectionWheel(); this.removeSelectionScroll(); var options = [ - {'coalition': true, 'tooltip': 'Howitzer', 'src': 'spawnHowitzer.png', 'callback': () => this._unitSelectGround(e, "Howitzers")}, - {'coalition': true, 'tooltip': 'SAM', 'src': 'spawnSAM.png', 'callback': () => this._unitSelectGround(e, "SAM")}, - {'coalition': true, 'tooltip': 'IFV', 'src': 'spawnIFV.png', 'callback': () => this._unitSelectGround(e, "IFV")}, - {'coalition': true, 'tooltip': 'Tank', 'src': 'spawnTank.png', 'callback': () => this._unitSelectGround(e, "Tanks")}, - {'coalition': true, 'tooltip': 'MLRS', 'src': 'spawnMLRS.png', 'callback': () => this._unitSelectGround(e, "MLRS")}, - {'coalition': true, 'tooltip': 'Radar', 'src': 'spawnRadar.png', 'callback': () => this._unitSelectGround(e, "Radar")}, - {'coalition': true, 'tooltip': 'Unarmed', 'src': 'spawnUnarmed.png', 'callback': () => this._unitSelectGround(e, "Unarmed")} + {'coalition': true, 'tooltip': 'Howitzer', 'src': 'spawnHowitzer.png', 'callback': () => this._selectGroundUnit(e, "Howitzers")}, + {'coalition': true, 'tooltip': 'SAM', 'src': 'spawnSAM.png', 'callback': () => this._selectGroundUnit(e, "SAM")}, + {'coalition': true, 'tooltip': 'IFV', 'src': 'spawnIFV.png', 'callback': () => this._selectGroundUnit(e, "IFV")}, + {'coalition': true, 'tooltip': 'Tank', 'src': 'spawnTank.png', 'callback': () => this._selectGroundUnit(e, "Tanks")}, + {'coalition': true, 'tooltip': 'MLRS', 'src': 'spawnMLRS.png', 'callback': () => this._selectGroundUnit(e, "MLRS")}, + {'coalition': true, 'tooltip': 'Radar', 'src': 'spawnRadar.png', 'callback': () => this._selectGroundUnit(e, "Radar")}, + {'coalition': true, 'tooltip': 'Unarmed', 'src': 'spawnUnarmed.png', 'callback': () => this._selectGroundUnit(e, "Unarmed")} ] this._selectionWheel = new SelectionWheel(e.originalEvent.x, e.originalEvent.y, options); } @@ -185,11 +232,11 @@ class Map } /* Show unit selection for air units */ - _unitSelectAir(e) + _selectAircraft(e, group) { this.removeSelectionWheel(); this.removeSelectionScroll(); - var options = unitTypes.air; + var options = unitTypes.air[group]; options.sort(); this._selectionScroll = new SelectionScroll(e.originalEvent.x, e.originalEvent.y, options, (unitType) => { this.removeSelectionWheel(); @@ -205,16 +252,23 @@ class Map this.removeSelectionScroll(); var options = []; options = payloadNames[unitType] - options.sort(); - this._selectionScroll = new SelectionScroll(e.originalEvent.x, e.originalEvent.y, options, (payloadName) => { - this.removeSelectionWheel(); - this.removeSelectionScroll(); - spawnAirUnit(unitType, e.latlng, this._activeCoalition, payloadName); - }); + if (options != undefined && options.length > 0) + { + options.sort(); + this._selectionScroll = new SelectionScroll(e.originalEvent.x, e.originalEvent.y, options, (payloadName) => { + this.removeSelectionWheel(); + this.removeSelectionScroll(); + spawnAircraft(unitType, e.latlng, this._activeCoalition, payloadName, e.airbaseName); + }); + } + else + { + spawnAircraft(unitType, e.latlng, this._activeCoalition); + } } /* Show unit selection for ground units */ - _unitSelectGround(e, group) + _selectGroundUnit(e, group) { this.removeSelectionWheel(); this.removeSelectionScroll(); diff --git a/www/js/SelectionScroll.js b/www/js/Map/SelectionScroll.js similarity index 100% rename from www/js/SelectionScroll.js rename to www/js/Map/SelectionScroll.js diff --git a/www/js/SelectionWheel.js b/www/js/Map/SelectionWheel.js similarity index 98% rename from www/js/SelectionWheel.js rename to www/js/Map/SelectionWheel.js index 0fba7d52..66a241b0 100644 --- a/www/js/SelectionWheel.js +++ b/www/js/Map/SelectionWheel.js @@ -36,6 +36,7 @@ class SelectionWheel var image = document.createElement("img"); image.classList.add("selection-wheel-image"); image.src = `img/buttons/${this._options[id].src}` + image.title = this._options[id].tooltip; if ('tint' in this._options[id]) { button.style.setProperty('background-color', this._options[id].tint); diff --git a/www/js/MissionData.js b/www/js/MissionData.js deleted file mode 100644 index 4caf60cb..00000000 --- a/www/js/MissionData.js +++ /dev/null @@ -1,39 +0,0 @@ -class MissionData -{ - constructor() - { - this._bullseye = undefined; - this._bullseyeMarker = undefined; - } - - update(data) - { - this._bullseye = data.missionData.bullseye; - this._unitsData = data.missionData.unitsData; - this._drawBullseye(); - } - - getUnitData(ID) - { - if (ID in this._unitsData) - { - return this._unitsData[ID]; - } - else - { - return undefined; - } - } - - _drawBullseye() - { - if (this._bullseyeMarker === undefined) - { - this._bullseyeMarker = L.marker([this._bullseye.lat, this._bullseye.lng]).addTo(map.getMap()); - } - else - { - this._bullseyeMarker .setLatLng(new L.LatLng(this._bullseye.lat, this._bullseye.lng)); - } - } -} \ No newline at end of file diff --git a/www/js/Other/AirbaseMarker.js b/www/js/Other/AirbaseMarker.js new file mode 100644 index 00000000..ee858536 --- /dev/null +++ b/www/js/Other/AirbaseMarker.js @@ -0,0 +1,53 @@ +L.Marker.AirbaseMarker = L.Marker.extend( + { + options: { + name: "No name", + position: undefined, + coalitionID: 2, + iconSrc: "img/airbase.png" + }, + + // Marker constructor + initialize: function(latlng, options) { + this._latlng = latlng; + if (options != undefined) + { + L.setOptions(this, options); + } + var icon = new L.DivIcon({ + html: ` + + + +
+ +
${this.options.name}
+
`, + className: 'airbase-marker-icon'}); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + }, + + setCoalitionID: function(coalitionID) + { + this.options.coalitionID = coalitionID; + // Set the coalitionID + var img = this._icon.querySelector("#icon-img"); + img.classList.remove("airbasemarker-icon-img-blue"); + img.classList.remove("airbasemarker-icon-img-red"); + if (this.options.coalitionID == 2) + { + img.classList.add("airbasemarker-icon-img-blue"); + } + else if (this.options.coalitionID == 1) + { + img.classList.add("airbasemarker-icon-img-red"); + } + } + } +) + +// By default markers can be hovered and clicked +L.Marker.AirbaseMarker.addInitHook(function() +{ + +}); diff --git a/www/js/Other/MissionData.js b/www/js/Other/MissionData.js new file mode 100644 index 00000000..b1daae89 --- /dev/null +++ b/www/js/Other/MissionData.js @@ -0,0 +1,67 @@ +class MissionData +{ + constructor() + { + this._bullseye = undefined; + this._bullseyeMarker = undefined; + this._airbasesMarkers = {}; + } + + update(data) + { + this._bullseye = data.missionData.bullseye; + this._unitsData = data.missionData.unitsData; + this._airbases = data.missionData.airbases; + this._drawBullseye(); + this._drawAirbases(); + } + + getUnitData(ID) + { + if (ID in this._unitsData) + { + return this._unitsData[ID]; + } + else + { + return undefined; + } + } + + _drawBullseye() + { + if (this._bullseyeMarker === undefined) + { + this._bullseyeMarker = new L.Marker([this._bullseye.lat, this._bullseye.lng]).addTo(map.getMap()); + } + else + { + this._bullseyeMarker.setLatLng(new L.LatLng(this._bullseye.lat, this._bullseye.lng)); + } + } + + _drawAirbases() + { + for (let idx in this._airbases) + { + var airbase = this._airbases[idx] + if (this._airbasesMarkers[idx] === undefined) + { + this._airbasesMarkers[idx] = new L.Marker.AirbaseMarker(new L.LatLng(airbase.lat, airbase.lng), {name: airbase.callsign}).addTo(map.getMap()); + this._airbasesMarkers[idx].on('click', (e) => this._onAirbaseClick(e)); + } + else + { + this._airbasesMarkers[idx].setCoalitionID(airbase.coalition); + this._airbasesMarkers[idx].setLatLng(new L.LatLng(airbase.lat, airbase.lng)); + } + } + } + + _onAirbaseClick(e) + { + e.airbaseName = e.sourceTarget.options.name; + e.coalitionID = e.sourceTarget.coalitionID; + map.spawnFromAirbase(e); + } +} \ No newline at end of file diff --git a/www/js/Utils.js b/www/js/Other/Utils.js similarity index 100% rename from www/js/Utils.js rename to www/js/Other/Utils.js diff --git a/www/js/Panels/ActionPanel.js b/www/js/Panels/ActionPanel.js new file mode 100644 index 00000000..d0d8b67b --- /dev/null +++ b/www/js/Panels/ActionPanel.js @@ -0,0 +1,30 @@ +class ActionPanel +{ + constructor(id) + { + this._panel = document.getElementById(id); + + this._attackButton = new PanelButton(this._panel, "img/buttons/attack.png", "Attack unit"); + this._bombButton = new PanelButton(this._panel, "img/buttons/bomb.png", "Precision bombing"); + this._carpetButton = new PanelButton(this._panel, "img/buttons/carpet.png", "Carpet bombing"); + this._landButton = new PanelButton(this._panel, "img/buttons/land.png", "Land here"); + this._formationButton = new PanelButton(this._panel, "img/buttons/formation.png", "Create formation"); + + this._attackButton.addCallback(() => map.setState("ATTACK")); + this._bombButton.addCallback(() => map.setState("BOMB")); + this._carpetButton.addCallback(() => map.setState("CARPET_BOMB")); + this._landButton.addCallback(() => map.setState("LAND")); + this._formationButton.addCallback(() => map.setState("FORMATION")); + + this.setEnabled(false); + } + + setEnabled(enabled) + { + this._attackButton.setEnabled(enabled); + this._bombButton.setEnabled(false); + this._carpetButton.setEnabled(false); + this._landButton.setEnabled(false); + this._formationButton.setEnabled(enabled); + } +} diff --git a/www/js/Panels/FormationControlPanel.js b/www/js/Panels/FormationControlPanel.js new file mode 100644 index 00000000..922fe07c --- /dev/null +++ b/www/js/Panels/FormationControlPanel.js @@ -0,0 +1,82 @@ +class FormationControlPanel +{ + constructor(id) + { + this._panel = document.getElementById(id); + + this._formations = ["", "Echelon", "Line abreast", "Box", "Trail", "Finger tip", "Tactical line abreast", "Fluid four", "Spread four"]; + } + + update(selectedUnits) + { + if (selectedUnits.length == 1) + { + // Don't update if user is editing + if (selectedUnits[0].leader && !this._editing) + { + this._panel.style.bottom = "15px"; + this._showFormationControls(selectedUnits[0]); + } + } + else + { + this._panel.style.bottom = (-this._panel.offsetHeight - 2) + "px"; + this._showFormationControls(); // Empty, cleans the panel + } + } + + _showFormationControls(selectedUnit) + { + if (selectedUnit !== undefined) + { + this._panel.innerHTML = ` +
+ + + + + + + + + + + + +
+ FORMATION CONTROL +
+ Formation: + + ${selectedUnit.formationID} +
+ Formation type: + + +
+
+ `; + + var select = document.getElementById("formation-type-select"); + for(var i = 0; i < this._formations.length; i++) { + var opt = this._formations[i]; + var el = document.createElement("option"); + el.textContent = opt; + el.value = opt; + select.appendChild(el); + } + + select.addEventListener("focus", () => this._editing = true) + select.addEventListener("blur", () => this._editing = false) + object.addEventListener("change", () => leader.setformation()); + + select.value = selectedUnit.formation; + } + else + { + this._panel.innerHTML = ``; + } + } +} + + \ No newline at end of file diff --git a/www/js/PanelButton.js b/www/js/Panels/PanelButton.js similarity index 82% rename from www/js/PanelButton.js rename to www/js/Panels/PanelButton.js index c824f4dd..fcf9df09 100644 --- a/www/js/PanelButton.js +++ b/www/js/Panels/PanelButton.js @@ -1,9 +1,9 @@ class PanelButton { - constructor(parent, icon) + constructor(parent, icon, tooltip) { this._div = document.createElement("div"); - this.setIcon(icon); + this.setIcon(icon, tooltip); this.setSlashed(false); this._div.classList.add("panel-button"); @@ -38,9 +38,16 @@ class PanelButton this._callbacks = []; } - setIcon(icon) + setIcon(icon, tooltip) { - this._baseIcon = ``; + if (icon.includes("png")) + { + this._baseIcon = ``; + } + else + { + this._baseIcon = ``; + } this._div.innerHTML = this._baseIcon; } diff --git a/www/js/ControlPanel.js b/www/js/Panels/SettingsPanel.js similarity index 94% rename from www/js/ControlPanel.js rename to www/js/Panels/SettingsPanel.js index f886e855..a72463ae 100644 --- a/www/js/ControlPanel.js +++ b/www/js/Panels/SettingsPanel.js @@ -1,20 +1,20 @@ -class ControlPanel +class SettingsPanel { constructor(id) { this._panel = document.getElementById(id); - /* Create all buttons, disabled by default */ + /* Create all buttons, disabled by default */ this._humanIcon = "fa-user"; this._AIIcon = "fa-desktop"; this._weaponsIcon = "fa-bomb"; this._labelsIcon = "fa-font"; this._deadIcon = "fa-skull"; - this._humanButton = new PanelButton(this._panel, this._humanIcon); - this._AIButton = new PanelButton(this._panel, this._AIIcon); - this._weaponsButton = new PanelButton(this._panel, this._weaponsIcon); - this._deadAliveButton = new PanelButton(this._panel, this._deadIcon); + this._humanButton = new PanelButton(this._panel, this._humanIcon, "Player visibility"); + this._AIButton = new PanelButton(this._panel, this._AIIcon, "AI visibility"); + this._weaponsButton = new PanelButton(this._panel, this._weaponsIcon, "Weapons visibility"); + this._deadAliveButton = new PanelButton(this._panel, this._deadIcon, "Dead units visibility"); this._humanButton.addCallback(() => this._onHumanButton()); this._AIButton.addCallback(() => this._onAIButton()); diff --git a/www/js/Panels/UnitControlPanel.js b/www/js/Panels/UnitControlPanel.js new file mode 100644 index 00000000..6c155dcb --- /dev/null +++ b/www/js/Panels/UnitControlPanel.js @@ -0,0 +1,35 @@ +class UnitControlPanel +{ + constructor(id) + { + this._panel = document.getElementById(id); + + /* Create all buttons, disabled by default */ + //this._moveButton = new PanelButton(this._panel, "fa-play"); + //this._stopButton = new PanelButton(this._panel, "fa-pause"); + this._slowButton = new PanelButton(this._panel, "fa-angle-right", "Decelerate"); + this._fastButton = new PanelButton(this._panel, "fa-angle-double-right", "Accelerate"); + this._descendButton = new PanelButton(this._panel, "fa-arrow-down", "Descend"); + this._climbButton = new PanelButton(this._panel, "fa-arrow-up", "Climb"); + //this._repeatButton = new PanelButton(this._panel, "fa-undo"); + + this.setEnabled(false); + + //this._moveButton.addCallback(unitsManager.selectedUnitsMove); + //this._stopButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('stop')); + this._slowButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('slow')); + this._fastButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('fast')); + this._descendButton.addCallback(() => unitsManager.selectedUnitsChangeAltitude('descend')); + this._climbButton.addCallback(() => unitsManager.selectedUnitsChangeAltitude('climb')); + } + + setEnabled(enabled) + { + //this._moveButton.setEnabled(true); + //this._stopButton.setEnabled(true); + this._slowButton.setEnabled(enabled); + this._fastButton.setEnabled(enabled); + this._descendButton.setEnabled(enabled); + this._climbButton.setEnabled(enabled); + } +} diff --git a/www/js/UnitInfoPanel.js b/www/js/Panels/UnitInfoPanel.js similarity index 86% rename from www/js/UnitInfoPanel.js rename to www/js/Panels/UnitInfoPanel.js index 8affa9a9..20f46ed2 100644 --- a/www/js/UnitInfoPanel.js +++ b/www/js/Panels/UnitInfoPanel.js @@ -7,23 +7,15 @@ class UnitInfoPanel update(selectedUnits) { - if (selectedUnits.length > 0) + if (selectedUnits.length == 1) { this._panel.style.bottom = "15px"; - if (selectedUnits.length == 1) - { - this._showUnitData(selectedUnits[0]); - } - else - { - this._showUnitData(); - this._panel.style.bottom = "-80px"; - } + this._showUnitData(selectedUnits[0]); } else { - this._showUnitData(); - this._panel.style.bottom = "-80px"; + this._panel.style.bottom = (-this._panel.offsetHeight - 2) + "px"; + this._showUnitData(); // Empty, cleans the panel } } @@ -40,7 +32,7 @@ class UnitInfoPanel var displayName = ammo.desc.displayName; var amount = ammo.count; loadout += amount + "x" + displayName; - if (parseInt(index) < Object.keys(selectedUnit.missionData.ammo).length - 1) + if (parseInt(index) < Object.keys(selectedUnit.missionData.ammo).length) { loadout += ", "; } @@ -143,15 +135,7 @@ class UnitInfoPanel } else { - this._panel.innerHTML = ` - - - - -
- UNIT INFO -
- `; + this._panel.innerHTML = ``; } } } \ No newline at end of file diff --git a/www/js/UnitControlPanel.js b/www/js/UnitControlPanel.js deleted file mode 100644 index 631c1d70..00000000 --- a/www/js/UnitControlPanel.js +++ /dev/null @@ -1,47 +0,0 @@ -class UnitControlPanel -{ - constructor(id) - { - this._panel = document.getElementById(id); - - /* Create all buttons, disabled by default */ - this._moveButton = new PanelButton(this._panel, "fa-play"); - this._stopButton = new PanelButton(this._panel, "fa-pause"); - this._slowButton = new PanelButton(this._panel, "fa-angle-right"); - this._fastButton = new PanelButton(this._panel, "fa-angle-double-right"); - this._descendButton = new PanelButton(this._panel, "fa-arrow-down"); - this._climbButton = new PanelButton(this._panel, "fa-arrow-up"); - this._repeatButton = new PanelButton(this._panel, "fa-undo"); - - this._moveButton.addCallback(unitsManager.selectedUnitsMove); - this._stopButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('stop')); - this._slowButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('slow')); - this._fastButton.addCallback(() => unitsManager.selectedUnitsChangeSpeed('fast')); - this._descendButton.addCallback(() => unitsManager.selectedUnitsChangeAltitude('descend')); - this._climbButton.addCallback(() => unitsManager.selectedUnitsChangeAltitude('climb')); - } - - enableButtons(enableAltitudeButtons) - { - this._moveButton.setEnabled(true); - this._stopButton.setEnabled(true); - this._slowButton.setEnabled(true); - this._fastButton.setEnabled(true); - if (enableAltitudeButtons) - { - this._descendButton.setEnabled(true); - this._climbButton.setEnabled(true); - } - - } - - disableButtons() - { - this._moveButton.setEnabled(false); - this._stopButton.setEnabled(false); - this._slowButton.setEnabled(false); - this._fastButton.setEnabled(false); - this._descendButton.setEnabled(false); - this._climbButton.setEnabled(false); - } -} diff --git a/www/js/Unit.js b/www/js/Units/Unit.js similarity index 72% rename from www/js/Unit.js rename to www/js/Units/Unit.js index 624e387b..7729304b 100644 --- a/www/js/Unit.js +++ b/www/js/Units/Unit.js @@ -8,7 +8,6 @@ class Unit // The marker is set by the inherited class this.marker = marker; this.marker.on('click', (e) => this.onClick(e)); - this.marker.on('contextmenu', (e) => this.onRightClick(e)); this._selected = false; @@ -18,6 +17,10 @@ class Unit this._pathPolyline.addTo(map.getMap()); this._targetsPolylines = []; + + this.leader = true; + this.wingmen = []; + this.formation = undefined; } update(response) @@ -37,6 +40,19 @@ class Unit this.speed = response["speed"]; this.currentTask = response["currentTask"]; + this.leader = response["leader"]; + this.wingman = response["wingman"]; + + this.wingmen = []; + if (response["wingmenIDs"] != undefined) + { + for (let ID of response["wingmenIDs"]) + { + this.wingmen.push(unitsManager.getUnitByID(ID)); + } + } + this.formation = response["formation"]; + this.missionData = missionData.getUnitData(this.ID) this.setSelected(this.getSelected() & this.alive) @@ -113,22 +129,28 @@ class Unit onClick(e) { - if (!e.originalEvent.ctrlKey) + if (map.getState() === 'IDLE' || map.getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { - unitsManager.deselectAllUnits(); + if (!e.originalEvent.ctrlKey) + { + unitsManager.deselectAllUnits(); + } + this.setSelected(true); + } + else if (map.getState() === 'ATTACK') + { + unitsManager.attackUnit(this.ID); + } + else if (map.getState() === 'FORMATION') + { + unitsManager.createFormation(this.ID); } - this.setSelected(true); - } - - onRightClick(e) - { - unitsManager.onUnitRightClick(this.ID); } drawMarker(settings) { // Hide the marker if disabled - if ((settings === 'none' || (controlPanel.getSettings().deadAlive === "alive" && !this.alive))) + if ((settings === 'none' || (settingsPanel.getSettings().deadAlive === "alive" && !this.alive))) { // Remove the marker if present if (map.getMap().hasLayer(this.marker)) @@ -204,30 +226,32 @@ class Unit for (let index in this.missionData.targets[typeIndex]) { var targetData = this.missionData.targets[typeIndex][index]; - var target = unitsManager.getUnit(targetData.object["id_"]) - var startLatLng = new L.LatLng(this.latitude, this.longitude) - var endLatLng = new L.LatLng(target.latitude, target.longitude) - - var color; - if (typeIndex === "radar") - { - color = "#FFFF00"; + var target = unitsManager.getUnitByID(targetData.object["id_"]) + if (target != undefined){ + var startLatLng = new L.LatLng(this.latitude, this.longitude) + var endLatLng = new L.LatLng(target.latitude, target.longitude) + + var color; + if (typeIndex === "radar") + { + color = "#FFFF00"; + } + else if (typeIndex === "visual") + { + color = "#FF00FF"; + } + else if (typeIndex === "rwr") + { + color = "#00FF00"; + } + else + { + color = "#FFFFFF"; + } + var targetPolyline = new L.Polyline([startLatLng, endLatLng], {color: color, weight: 3, opacity: 1, smoothFactor: 1}); + targetPolyline.addTo(map.getMap()); + this._targetsPolylines.push(targetPolyline) } - else if (typeIndex === "visual") - { - color = "#FF00FF"; - } - else if (typeIndex === "rwr") - { - color = "#00FF00"; - } - else - { - color = "#FFFFFF"; - } - var targetPolyline = new L.Polyline([startLatLng, endLatLng], {color: color, weight: 3, opacity: 1, smoothFactor: 1}); - targetPolyline.addTo(map.getMap()); - this._targetsPolylines.push(targetPolyline) } } } @@ -243,7 +267,14 @@ class Unit attackUnit(targetID) { // Call DCS attackUnit function - attackUnit(this.ID, targetID); + if (this.ID != targetID) + { + attackUnit(this.ID, targetID); + } + else + { + // TODO: show a message + } } changeSpeed(speedChange) @@ -281,19 +312,55 @@ class Unit xhr.send(JSON.stringify(data)); } + + setformation(formation) + { + // TODO move in dedicated file + var xhr = new XMLHttpRequest(); + xhr.open("PUT", RESTaddress); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + console.log(this.unitName + " formation change: " + formation); + } + }; + + var command = {"ID": this.ID, "formation": formation} + var data = {"setFormation": command} + + xhr.send(JSON.stringify(data)); + } + + setLeader(wingmenIDs) + { + // TODO move in dedicated file + var xhr = new XMLHttpRequest(); + xhr.open("PUT", RESTaddress); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + console.log(this.unitName + " created formation with: " + wingmenIDs); + } + }; + + var command = {"ID": this.ID, "wingmenIDs": wingmenIDs} + var data = {"setLeader": command} + + xhr.send(JSON.stringify(data)); + } } class AirUnit extends Unit { drawMarker() { - if (this.flags.human) + if (this.flags.Human) { - super.drawMarker(controlPanel.getSettings().human); + super.drawMarker(settingsPanel.getSettings().human); } else { - super.drawMarker(controlPanel.getSettings().AI); + super.drawMarker(settingsPanel.getSettings().AI); } } } @@ -344,7 +411,7 @@ class GroundUnit extends Unit drawMarker() { - super.drawMarker(controlPanel.getSettings().AI); + super.drawMarker(settingsPanel.getSettings().AI); } } @@ -364,7 +431,7 @@ class NavyUnit extends Unit drawMarker() { - super.drawMarker(controlPanel.getSettings().AI); + super.drawMarker(settingsPanel.getSettings().AI); } } @@ -372,25 +439,20 @@ class Weapon extends Unit { constructor(ID, data) { - // Weapons can not be selected - self.selectable = false; super(ID, data); + // Weapons can not be selected + this.selectable = false; } drawMarker() { - super.drawMarker(controlPanel.getSettings().weapons); + super.drawMarker(settingsPanel.getSettings().weapons); } onClick(e) { // Weapons can not be clicked } - - onRightClick(e) - { - // Weapons can not be clicked - } } class Missile extends Weapon diff --git a/www/js/UnitMarker.js b/www/js/Units/UnitMarker.js similarity index 90% rename from www/js/UnitMarker.js rename to www/js/Units/UnitMarker.js index fdc9fc1e..9d1c8bda 100644 --- a/www/js/UnitMarker.js +++ b/www/js/Units/UnitMarker.js @@ -25,7 +25,7 @@ L.Marker.UnitMarker = L.Marker.extend( `, - className: 'unit-marker-icon'}); // Set the unit marker, className must be set to avoid white square + className: 'action-cursor'}); // Set the unit marker, className must be set to avoid white square this.setIcon(icon); }, @@ -38,18 +38,6 @@ L.Marker.UnitMarker = L.Marker.extend( this._icon.style.outline = "transparent"; // Removes the rectangular outline - // Set the unit name in the marker - var unitNameDiv = this._icon.querySelector("#unitName"); - if (this.options.human) - { - unitNameDiv.innerHTML = ` ${this.options.unitName}`; - } - else - { - unitNameDiv.innerHTML = `${this.options.unitName}`; - } - unitNameDiv.style.left = (-(unitNameDiv.offsetWidth - this._icon.querySelector("#icon-img").height) / 2) + "px"; - // Set the unit name in the marker var nameDiv = this._icon.querySelector("#name"); nameDiv.innerHTML = this.options.name; @@ -70,6 +58,7 @@ L.Marker.UnitMarker = L.Marker.extend( // If the unit is not alive it is drawn with darker colours setAlive: function(alive) { + this.alive = alive var table = this._icon.querySelector("#container-table"); if (alive) { @@ -114,7 +103,7 @@ L.Marker.UnitMarker = L.Marker.extend( } else { - if (img.classList.contains("unitmarker-icon-img-hovered")) img.classList.remove("unitmarker-icon-img-hovered"); + img.classList.remove("unitmarker-icon-img-hovered"); } } }, @@ -151,8 +140,11 @@ L.Marker.UnitMarker = L.Marker.extend( setLabelsVisibility(visibility) { - this._icon.querySelector("#unitName").style.opacity = visibility ? "1": "0"; - this._icon.querySelector("#unitName").innerHTML = visibility ? this.options.unitName : ""; + var unitNameDiv = this._icon.querySelector("#unitName"); + unitNameDiv.style.opacity = visibility ? "1": "0"; + var unitName = this.options.human ? ` ${this.options.unitName}` : `${this.options.unitName}`; + unitNameDiv.innerHTML = visibility ? unitName : ""; + unitNameDiv.style.left = (-(unitNameDiv.offsetWidth - this._icon.querySelector("#icon-img").height) / 2) + "px"; //this._icon.querySelector("#name").style.opacity = visibility ? "1": "0"; //this._icon.querySelector("#name").innerHTML = visibility ? this.options.name : ""; this._icon.querySelector("#altitude-div").style.opacity = visibility ? "1": "0"; @@ -225,8 +217,9 @@ L.Marker.UnitMarker.WeaponMarker = L.Marker.UnitMarker.extend({}) L.Marker.UnitMarker.WeaponMarker.addInitHook(function() { // Weapons are not selectable - this.on('mouseover', function(e) {}); - this.on('mouseout', function(e) {}); + this.on('mouseover', function(e) { + e.target.setHovered(false); + }); }); // Missile diff --git a/www/js/UnitsManager.js b/www/js/Units/UnitsManager.js similarity index 52% rename from www/js/UnitsManager.js rename to www/js/Units/UnitsManager.js index c2e88f26..4be0e428 100644 --- a/www/js/UnitsManager.js +++ b/www/js/Units/UnitsManager.js @@ -3,6 +3,7 @@ class UnitsManager constructor() { this._units = {}; + this._copiedUnits = []; } addUnit(ID, data) @@ -15,7 +16,7 @@ class UnitsManager } } - getUnit(ID) + getUnitByID(ID) { return this._units[ID]; } @@ -50,22 +51,15 @@ class UnitsManager { if (this.getSelectedUnits().length > 0) { - map.setState("UNIT_SELECTED"); - unitControlPanel.enableButtons(true); + map.setState("MOVE_UNIT"); + unitControlPanel.setEnabled(true); + actionPanel.setEnabled(true); } else { map.setState("IDLE"); - unitControlPanel.disableButtons(); - } - } - - onUnitRightClick(ID) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].attackUnit(ID); + unitControlPanel.setEnabled(false); + actionPanel.setEnabled(false); } } @@ -135,4 +129,102 @@ class UnitsManager selectedUnits[idx].changeAltitude(altitudeChange); } } + + handleKeyEvent(e) + { + if (e.originalEvent.code === 'KeyC' && e.originalEvent.ctrlKey) + { + this.copyUnits(); + } + else if (e.originalEvent.code === 'KeyV' && e.originalEvent.ctrlKey) + { + this.pasteUnits(); + } + } + + copyUnits() + { + this._copiedUnits = this.getSelectedUnits(); + } + + pasteUnits() + { + for (let idx in this._copiedUnits) + { + var unit = this._copiedUnits[idx]; + cloneUnit(unit.ID); + } + } + + attackUnit(ID) + { + var selectedUnits = this.getSelectedUnits(); + for (let idx in selectedUnits) + { + selectedUnits[idx].attackUnit(ID); + } + } + + createFormation(ID) + { + var selectedUnits = this.getSelectedUnits(); + var wingmenIDs = []; + for (let idx in selectedUnits) + { + if (selectedUnits[idx].wingman) + { + showMessage(selectedUnits[idx].unitName + " is already in a formation."); + return; + } + else if (selectedUnits[idx].leader) + { + showMessage(selectedUnits[idx].unitName + " is already in a formation."); + return; + } + else + { + if (selectedUnits[idx].category !== this.getUnitByID(ID).category) + { + showMessage("All units must be of the same category to create a formation."); + } + if (selectedUnits[idx].ID != ID) + { + wingmenIDs.push(selectedUnits[idx].ID); + } + } + } + if (wingmenIDs.length > 0) + { + this.getUnitByID(ID).setLeader(wingmenIDs); + } + else + { + showMessage("At least 2 units must be selected to create a formation."); + } + } + + getUnitsByFormationID(formationID) + { + var formationUnits = []; + for (let ID in this._units) + { + if (this._units[ID].formationID == formationID) + { + formationUnits.push(this._units[ID]); + } + } + return formationUnits; + } + + getLeaderByFormationID(formationID) + { + var formationUnits = this.getUnitsByFormationID(formationID); + for (let unit of formationUnits) + { + if (unit.leader) + { + return unit; + } + } + } } \ No newline at end of file diff --git a/www/js/unitTypes.js b/www/js/Units/unitTypes.js similarity index 93% rename from www/js/unitTypes.js rename to www/js/Units/unitTypes.js index dbbe824e..6c35b4c7 100644 --- a/www/js/unitTypes.js +++ b/www/js/Units/unitTypes.js @@ -206,68 +206,70 @@ unitTypes.vehicles.Unarmed = [ ] /* AIRPLANES */ -unitTypes.air = [ - "Tornado GR4", - "Tornado IDS", - "F/A-18A", +unitTypes.air = {} + +unitTypes.air.CAP = [ + "F-4E", "F/A-18C", "MiG-29S", - "MiG-29A", "F-14A", - "Tu-22M3", - "F-4E", - "B-52H", - "MiG-27K", - "F-111F", - "A-10A", "Su-27", - "MiG-29G", "MiG-23MLD", - "Su-25", - "Su-25TM", - "Su-25T", "Su-33", - "MiG-25PD", "MiG-25RBT", "Su-30", + "MiG-31", + "Mirage 2000-5", + "F-15C", + "F-5E", + "F-16C bl.52d", +] + +unitTypes.air.CAS = [ + "Tornado IDS", + "F-4E", + "F/A-18C", + "MiG-27K", + "A-10C", + "Su-25", "Su-34", "Su-17M4", - "MiG-31", + "F-15E", +] + +unitTypes.air.strike = [ + "Tu-22M3", + "B-52H", + "F-111F", "Tu-95MS", "Su-24M", "Tu-160", "F-117A", "B-1B", - "S-3B", - "S-3B Tanker", - "Mirage 2000-5", - "F-15C", - "F-15E", - "F-5E", - "MiG-29K", "Tu-142", +] + +unitTypes.air.tank = [ + "S-3B Tanker", + "KC-135", + "IL-78M", +] + +unitTypes.air.awacs = [ + "A-50", + "E-3A", + "E-2D", +] + +unitTypes.air.drone = [ + "MQ-1A Predator", + "MQ-9 Reaper", +] + +unitTypes.air.transport = [ "C-130", "An-26B", "An-30M", "C-17A", - "A-50", - "E-3A", - "IL-78M", - "E-2D", "IL-76MD", - "F-16C bl.50", - "F-16C bl.52d", - "Su-24MR", - "F-16A", - "F-16A MLU", - "MQ-1A Predator", - "Yak-40", - "A-10C", - "KC-135", - "L-39ZA", - "P-51D", - "MQ-9 Reaper", - "FW-190D9", - "TF-51D" -] - +] \ No newline at end of file diff --git a/www/js/index.js b/www/js/index.ts similarity index 60% rename from www/js/index.js rename to www/js/index.ts index 885f4149..38117e01 100644 --- a/www/js/index.js +++ b/www/js/index.ts @@ -1,8 +1,10 @@ var missionData; -var controlPanel; +var settingsPanel; var unitsManager; var unitInfoPanel; var unitControlPanel; +var unitActionPanel; +var formationControlPanel; var map; var RESTaddress = "http://localhost:30000/restdemo"; @@ -12,10 +14,11 @@ function setup() missionData = new MissionData(); unitsManager = new UnitsManager(); - unitInfoPanel = new UnitInfoPanel("left-panel"); - unitControlPanel = new UnitControlPanel("top-panel"); - controlPanel = new ControlPanel("top-control-panel"); - + unitInfoPanel = new UnitInfoPanel("unit-info-panel"); + unitControlPanel = new UnitControlPanel("unit-control-panel"); + formationControlPanel = new FormationControlPanel("formation-control-panel"); + settingsPanel = new SettingsPanel("settings-panel"); + actionPanel = new ActionPanel("action-panel") map = new Map(); // Main update rate. 250ms is minimum time, equal to server update time. @@ -26,7 +29,9 @@ function resize() { var unitControlPanelHeight = document.getElementById("header").offsetHeight; document.getElementById("map").style.height = `${window.innerHeight - unitControlPanelHeight - 10}px`; - document.getElementById("top-panel").style.left = `${window.innerWidth / 2 - document.getElementById("top-panel").offsetWidth / 2}px` + document.getElementById("unit-control-panel").style.left = `${window.innerWidth / 2 - document.getElementById("unit-control-panel").offsetWidth / 2}px` + document.getElementById("action-panel").style.top = `${window.innerHeight / 2 - document.getElementById("action-panel").offsetHeight / 2}px` + document.getElementById("snackbar").style.left = `${window.innerWidth / 2 - document.getElementById("snackbar").offsetWidth / 2}px` } /* GET new data from the server */ @@ -42,6 +47,7 @@ function update() missionData.update(data); unitsManager.update(data); unitInfoPanel.update(unitsManager.getSelectedUnits()); + formationControlPanel.update(unitsManager.getSelectedUnits()); }; xmlHttp.onerror = function () { @@ -77,4 +83,18 @@ window.console = { }, lastMessage: "none" +} + +function showMessage(message) +{ + // Get the snackbar DIV + var x = document.getElementById("snackbar"); + + // Add the "show" class to DIV + x.className = "show"; + x.innerHTML = message; + + // After 3 seconds, remove the show class from DIV + setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000); + } \ No newline at end of file diff --git a/www/server.bat b/www/server.bat deleted file mode 100644 index 19091d6b..00000000 --- a/www/server.bat +++ /dev/null @@ -1 +0,0 @@ -python -m http.server \ No newline at end of file diff --git a/www/tsconfig.json b/www/tsconfig.json new file mode 100644 index 00000000..8de604d3 --- /dev/null +++ b/www/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "allowJs": true, + "target": "es5" + }, + "include": [ + "./js/*", + "./js/**/*" + ] +} \ No newline at end of file