diff --git a/.gitignore b/.gitignore index 5ded2391..12d0171d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ core.user core.vcxproj.user *.user Output -server node_modules /client/TODO.txt /client/public/javascripts/bundle.js diff --git a/README.md b/README.md index 7db8c48e..b124d30c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Important note: DCS Olympus is in alpha state. No official release has been produced yet. The first public version is planned for Q2 2023. + # DCS Olympus *A real-time web interface to spawn and control units in DCS World* diff --git a/client/.vscode/launch.json b/client/.vscode/launch.json index 206b6a0c..e184155a 100644 --- a/client/.vscode/launch.json +++ b/client/.vscode/launch.json @@ -4,6 +4,17 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Attach to Chrome", + "port": 9222, + "urlFilter": "http://localhost:3000/*", + "request": "attach", + "type": "chrome", + "webRoot": "${workspaceFolder}/public/", + "sourceMapPathOverrides": { + "src/*": "${workspaceFolder}/src/*" + } + }, { "type": "chrome", "request": "launch", diff --git a/client/.vscode/settings.json b/client/.vscode/settings.json new file mode 100644 index 00000000..7a73a41b --- /dev/null +++ b/client/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/client/TODO.txt b/client/TODO.txt deleted file mode 100644 index 43ba41c2..00000000 --- a/client/TODO.txt +++ /dev/null @@ -1,9 +0,0 @@ -RTB -tanker -scenario dropdown -explosion -wrong name for ground units -improve map zIndex -fuel is wrong (either 0 or 1, its is casting it to int somewhere) -weapons should not be selectable -human symbol if user \ No newline at end of file diff --git a/client/app.js b/client/app.js index 7e9f140e..85d5c31d 100644 --- a/client/app.js +++ b/client/app.js @@ -2,9 +2,15 @@ var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); +var fs = require('fs'); +var basicAuth = require('express-basic-auth') -var indexRouter = require('./routes/index'); -var usersRouter = require('./routes/users'); +var atcRouter = require('./routes/api/atc'); +var airbasesRouter = require('./routes/api/airbases'); +var indexRouter = require('./routes/index'); +var uikitRouter = require('./routes/uikit'); +var usersRouter = require('./routes/users'); +var resourcesRouter = require('./routes/resources'); var app = express(); @@ -15,8 +21,31 @@ app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); +app.use('/api/atc', atcRouter); +app.use('/api/airbases', airbasesRouter); app.use('/users', usersRouter); +app.use('/uikit', uikitRouter); +app.use('/resources', resourcesRouter); app.set('view engine', 'ejs'); +let rawdata = fs.readFileSync('../olympus.json'); +let config = JSON.parse(rawdata); +if (config["server"] != undefined) + app.get('/config', (req, res) => res.send(config["server"])); + module.exports = app; + +const DemoDataGenerator = require('./demo.js'); +var demoDataGenerator = new DemoDataGenerator(10); +app.get('/demo/units', (req, res) => demoDataGenerator.units(req, res)); +app.get('/demo/logs', (req, res) => demoDataGenerator.logs(req, res)); +app.get('/demo/bullseyes', (req, res) => demoDataGenerator.bullseyes(req, res)); +app.get('/demo/airbases', (req, res) => demoDataGenerator.airbases(req, res)); +app.get('/demo/mission', (req, res) => demoDataGenerator.mission(req, res)); + +app.use('/demo', basicAuth({ + users: { 'admin': 'socks' } +})) + + diff --git a/client/copy.bat b/client/copy.bat new file mode 100644 index 00000000..f163f70e --- /dev/null +++ b/client/copy.bat @@ -0,0 +1,2 @@ +copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet\\leaflet.css +copy .\\node_modules\\leaflet.nauticscale\\dist\\leaflet.nauticscale.js .\\public\\javascripts\\leaflet.nauticscale.js diff --git a/client/debug.bat b/client/debug.bat index be565780..f57b8be0 100644 --- a/client/debug.bat +++ b/client/debug.bat @@ -1,2 +1,2 @@ start cmd /k "npm run start" -start cmd /k "watchify .\src\index.ts --debug -p [ tsify --noImplicitAny ] -o .\public\javascripts\bundle.js" +start cmd /k "watchify .\src\index.ts --debug -o .\public\javascripts\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ] diff --git a/client/demo.js b/client/demo.js new file mode 100644 index 00000000..5a76f8d1 --- /dev/null +++ b/client/demo.js @@ -0,0 +1,705 @@ + +const DEMO_UNIT_DATA = { + ["1"]:{ + baseData: { + AI: false, + name: "KC-135", + unitName: "Olympus 1-1 aka Mr. Very long name", + groupName: "Group 2", + alive: true, + category: "Aircraft", + }, + flightData: { + latitude: 37.20, + longitude: -115.80, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 50, + flags: {Human: false}, + ammo: [ + { + count: 4, + desc: { + displayName: "AIM-120" + } + }, + { + count: 2, + desc: { + displayName: "AIM-7" + } + } + ], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Holding", + currentState: "Idle", + activePath: undefined, + desiredSpeed: 400, + desiredSpeedType: "CAS", + desiredAltitude: 3000, + desiredAltitudeType: "ASL", + isTanker: false, + + }, + optionsData: { + ROE: "Designated", + reactionToThreat: "Abort", + } + }, + ["2"]:{ + baseData: { + AI: true, + name: "KC-135", + unitName: "Olympus 1-2", + groupName: "Group 3", + alive: true, + category: "Aircraft", + }, + flightData: { + latitude: 37.2, + longitude: -115.75, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 300, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "Designated", + reactionToThreat: "Abort", + } + }, + ["3"]:{ + baseData: { + AI: true, + name: "M-60", + unitName: "Olympus 1-3", + groupName: "Group 4", + alive: true, + category: "GroundUnit", + }, + flightData: { + latitude: 37.175, + longitude: -115.8, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000, + onOff: false + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["4"]:{ + baseData: { + AI: true, + name: "2S6 Tunguska", + unitName: "Olympus 1-4", + alive: true, + category: "GroundUnit", + }, + flightData: { + latitude: 37.175, + longitude: -115.75, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["5"]:{ + baseData: { + AI: true, + name: "M-60", + unitName: "Olympus 1-3", + groupName: "Group 1", + alive: true, + category: "GroundUnit", + }, + flightData: { + latitude: 37.15, + longitude: -115.8, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["6"]:{ + baseData: { + AI: true, + name: "M-60", + unitName: "Olympus 1-4", + groupName: "Group 1", + alive: true, + category: "GroundUnit", + }, + flightData: { + latitude: 37.15, + longitude: -115.75, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["7"]:{ + baseData: { + AI: true, + name: "CVN-75 Very long name", + unitName: "Olympus 1-7", + groupName: "Group 1", + alive: true, + category: "NavyUnit", + }, + flightData: { + latitude: 37.125, + longitude: -115.8, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["8"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-8", + groupName: "Group 1", + alive: true, + category: "NavyUnit", + }, + flightData: { + latitude: 37.125, + longitude: -115.75, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["9"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-9", + groupName: "Group 1", + alive: true, + category: "Aircraft", + }, + flightData: { + latitude: 37.10, + longitude: -115.75, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["10"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-10", + groupName: "Group 1", + alive: true, + category: "Aircraft", + }, + flightData: { + latitude: 37.10, + longitude: -115.8, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["11"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-11", + groupName: "Group 1", + alive: true, + category: "Missile", + }, + flightData: { + latitude: 37.075, + longitude: -115.80, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["12"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-12", + groupName: "Group 1", + alive: true, + category: "Missile", + }, + flightData: { + latitude: 37.075, + longitude: -115.75, + altitude: 2000, + heading: 0.6, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["13"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-11", + groupName: "Group 1", + alive: true, + category: "Bomb", + }, + flightData: { + latitude: 37.05, + longitude: -115.8, + altitude: 2000, + heading: 0.5, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "blue" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + }, + ["14"]:{ + baseData: { + AI: true, + name: "CVN-75", + unitName: "Olympus 1-12", + groupName: "Group 1", + alive: true, + category: "Bomb", + }, + flightData: { + latitude: 37.05, + longitude: -115.75, + altitude: 2000, + heading: 0.6, + speed: 300 + }, + missionData: { + fuel: 0.5, + flags: {human: false}, + ammo: [], + targets: [], + hasTask: true, + coalition: "red" + }, + formationData: { + formation: "Echelon", + isLeader: false, + isWingman: false, + leaderID: null, + wingmen: [], + wingmenIDs: [] + }, + taskData: { + currentTask: "Example task", + activePath: undefined, + desiredSpeed: 400, + desiredAltitude: 3000 + }, + optionsData: { + ROE: "None", + reactionToThreat: "None", + } + } +} + +class DemoDataGenerator { + constructor(unitsNumber) + { + this.demoUnits = this.generateRandomUnitsDemoData(unitsNumber); + } + + units(req, res){ + var ret = this.demoUnits; + for (let ID in this.demoUnits["units"]){ + this.demoUnits["units"][ID].flightData.latitude += 0.00001; + } + ret.time = Date.now(); + res.send(JSON.stringify(ret)); + }; + + logs(req, res){ + var ret = {logs: {}}; + ret.time = Date.now(); + res.send(JSON.stringify(ret)); + }; + + airbases(req, res){ + var ret = {airbases: { + ["0"]: { + callsign: "Neutral", + latitude: 37.3, + longitude: -115.8, + coalition: "neutral" + }, + ["1"]: { + callsign: "Red", + latitude: 37.3, + longitude: -115.75, + coalition: "red" + }, + ["2"]: { + callsign: "Blue", + latitude: 37.3, + longitude: -115.7, + coalition: "blue" + } + }}; + ret.time = Date.now(); + res.send(JSON.stringify(ret)); + }; + + bullseyes(req, res){ + var ret = {bullseyes: { + "0": { + latitude: 37.25, + longitude: -115.8, + coalition: "neutral" + }, + "1": { + latitude: 37.25, + longitude: -115.75, + coalition: "red" + }, + "2": { + latitude: 37.25, + longitude: -115.7, + coalition: "blue" + } + }}; + ret.time = Date.now(); + res.send(JSON.stringify(ret)); + }; + + mission(req, res){ + var ret = {mission: {theatre: "Nevada"}}; + ret.time = Date.now(); + res.send(JSON.stringify(ret)); + } + + generateRandomUnitsDemoData(unitsNumber) + { + return {"units": DEMO_UNIT_DATA}; + } +} + +module.exports = DemoDataGenerator; \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index df336cd1..2fc981a0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,38 +1,1858 @@ { "name": "DCSOlympus", - "version": "0.0.0", + "version": "v0.3.0-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "DCSOlympus", - "version": "0.0.0", + "version": "v0.3.0-alpha", "dependencies": { + "@types/formatcoords": "^1.1.0", "@types/geojson": "^7946.0.10", "@types/leaflet": "^1.9.0", + "@types/svg-injector": "^0.0.29", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "^3.1.8", "express": "~4.16.1", + "formatcoords": "^1.1.3", "leaflet": "^1.9.3", - "milsymbol": "^2.0.0", + "leaflet-control-mini-map": "^0.4.0", + "leaflet.nauticscale": "^1.1.0", "morgan": "~1.9.1", "save": "^2.9.0" }, "devDependencies": { + "@babel/preset-env": "^7.21.4", + "@tanem/svg-injector": "^10.1.55", + "@types/gtag.js": "^0.0.12", + "@types/node": "^18.16.1", + "@types/sortablejs": "^1.15.0", + "babelify": "^10.0.0", "browserify": "^17.0.0", "concurrently": "^7.6.0", + "cp": "^0.2.0", + "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", + "sortablejs": "^1.15.0", "tsify": "^5.0.4", "typescript": "^4.9.4", "watchify": "^4.0.0" } }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", + "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.4", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.4", + "@babel/types": "^7.21.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", + "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.4", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", + "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz", + "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz", + "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", + "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", + "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", + "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", + "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", + "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", + "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", + "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", + "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", + "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", + "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", + "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", + "@babel/plugin-proposal-async-generator-functions": "^7.20.7", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.21.0", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.20.7", + "@babel/plugin-transform-async-to-generator": "^7.20.7", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.21.0", + "@babel/plugin-transform-classes": "^7.21.0", + "@babel/plugin-transform-computed-properties": "^7.20.7", + "@babel/plugin-transform-destructuring": "^7.21.3", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.21.0", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "@babel/plugin-transform-modules-systemjs": "^7.20.11", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.21.3", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.20.5", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.20.7", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.21.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", + "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.4", + "@babel/types": "^7.21.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@tanem/svg-injector": { + "version": "10.1.55", + "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-10.1.55.tgz", + "integrity": "sha512-xh8ejdvjDaH1eddZC0CdI45eeid4BIU2ppjNEhiTiWMYcLGT19KWjbES/ttDS4mq9gIAQfXx57g5zimEVohqYA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.21.5", + "content-type": "^1.0.5", + "tslib": "^2.5.0" + } + }, + "node_modules/@types/formatcoords": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/formatcoords/-/formatcoords-1.1.0.tgz", + "integrity": "sha512-T20OHYACraNS/xCoBEmvtUYLc/eYjXaGGDpPFVAyuwarE9KHSGX8DVb3KNBKZ5RQz7fB7qXazZj13520eDSODw==" + }, "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, + "node_modules/@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "dev": true + }, "node_modules/@types/leaflet": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", @@ -41,6 +1861,23 @@ "@types/geojson": "*" } }, + "node_modules/@types/node": { + "version": "18.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz", + "integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==", + "dev": true + }, + "node_modules/@types/sortablejs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz", + "integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==", + "dev": true + }, + "node_modules/@types/svg-injector": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/svg-injector/-/svg-injector-0.0.29.tgz", + "integrity": "sha512-tNvoN0Xk2si6IfxQI/PqInipOuCyXkDZhCh9Vc4aFv/l1DhIBfTFbXRXYUHBAGXaUNMOCHEtg9475O9J+4NXOg==" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -198,6 +2035,232 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-import-to-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-import-to-require/-/babel-plugin-import-to-require-1.0.0.tgz", + "integrity": "sha512-dc843CwrFivjO8AVgxcHvxl0cb7J7Ed8ZGFP8+PjH3X1CnyzYtAU1WL1349m9Wc/+oqk4ETx2+cIEO2jlp3XyQ==", + "dev": true, + "dependencies": { + "babel-template": "^6.26.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse/node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babel-types/node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babelify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", + "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -497,6 +2560,34 @@ "pako": "~1.0.5" } }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", @@ -552,6 +2643,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -729,9 +2840,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -767,12 +2878,39 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", + "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cp/-/cp-0.2.0.tgz", + "integrity": "sha512-4ftCvShHjIZG/zzomHyunNpBof3sOFTTmU6s6q9DdqAL/ANqrKV3pr6Z6kVfBI4hjn59DFLImrBqn7GuuMqSZA==", + "dev": true + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -989,6 +3127,12 @@ "node": ">=0.10.0" } }, + "node_modules/electron-to-chromium": { + "version": "1.4.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz", + "integrity": "sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw==", + "dev": true + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -1053,6 +3197,42 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esmify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esmify/-/esmify-2.1.1.tgz", + "integrity": "sha512-GyOVgjG7sNyYB5Mbo15Ll4aGrcXZzZ3LI22rbLOjCI7L/wYelzQpBHRZkZkqbPNZ/QIRilcaHqzgNCLcEsi1lQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "babel-plugin-import-to-require": "^1.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "^1.6.2", + "duplexer2": "^0.1.4", + "through2": "^2.0.5" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1134,6 +3314,15 @@ "node": ">= 0.10.0" } }, + "node_modules/express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "dependencies": { + "basic-auth": "^2.0.1" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -1213,6 +3402,11 @@ "is-callable": "^1.1.3" } }, + "node_modules/formatcoords": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/formatcoords/-/formatcoords-1.1.3.tgz", + "integrity": "sha512-Ittg5/AsYFCOtcFGPLSmVpP56a8Ionmv4Ys69UmCAdvBnqVzvVVbkZMnjWEmXrZvnmoGQE8YI3RhnxbMQFdYSw==" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1260,6 +3454,15 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -1321,6 +3524,15 @@ "node": ">= 6" } }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1345,6 +3557,27 @@ "node": ">= 0.4.0" } }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1566,6 +3799,15 @@ "insert-module-globals": "bin/cmd.js" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1740,6 +3982,36 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -1780,6 +4052,19 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" }, + "node_modules/leaflet-control-mini-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/leaflet-control-mini-map/-/leaflet-control-mini-map-0.4.0.tgz", + "integrity": "sha512-qnetYIYFqlrAbX7MaGsAkBsuA2fg3Z4xDRlEXJN1wSrnhNsIhQUzXt7Z3vapdEzx4r7VUqWTaqHYd7eY5C6Gfw==", + "peerDependencies": { + "leaflet": ">=1.7.0" + } + }, + "node_modules/leaflet.nauticscale": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/leaflet.nauticscale/-/leaflet.nauticscale-1.1.0.tgz", + "integrity": "sha512-kJqOVuY0bk3CjSWb1CUYsjXiM+W1K0TrlJmMjl/wVubKK2y0PZ+XxkIyD6cxVsKQGlJvLGMvrSqaYdj5LW1O1Q==" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1791,12 +4076,39 @@ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -1853,11 +4165,6 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, - "node_modules/milsymbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/milsymbol/-/milsymbol-2.0.0.tgz", - "integrity": "sha512-GcBFrcIUr8jScaZqZb0SI2W6AbnUrPCTHu2kqHxduQjN2DIN8q5pY6ksSWfnJ4HlcIAWQhyotbdPIr1bBxFbwQ==" - }, "node_modules/mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -1985,6 +4292,12 @@ "node": ">= 0.6" } }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, "node_modules/nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -2241,6 +4554,12 @@ "node": ">=0.12" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2425,6 +4744,77 @@ "node": ">=8.10.0" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2614,6 +5004,12 @@ "semver": "bin/semver.js" } }, + "node_modules/sortablejs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", + "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==", + "dev": true + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -2883,6 +5279,15 @@ "node": ">=0.6.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2950,9 +5355,9 @@ } }, "node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==", "dev": true }, "node_modules/tty-browserify": { @@ -3023,6 +5428,46 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3031,6 +5476,36 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -3193,6 +5668,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yargs": { "version": "17.6.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", @@ -3222,11 +5703,1295 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", + "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==", + "dev": true + }, + "@babel/core": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.4", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.4", + "@babel/types": "^7.21.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", + "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "dev": true, + "requires": { + "@babel/types": "^7.21.4", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", + "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.18.6", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", + "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-validator-option": "^7.21.0", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz", + "integrity": "sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-member-expression-to-functions": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/helper-split-export-declaration": "^7.18.6" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz", + "integrity": "sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "regexpu-core": "^5.3.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", + "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-function-name": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz", + "integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==", + "dev": true, + "requires": { + "@babel/types": "^7.21.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.21.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", + "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", + "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", + "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-wrap-function": "^7.18.9", + "@babel/types": "^7.18.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz", + "integrity": "sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-member-expression-to-functions": "^7.20.7", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "requires": { + "@babel/types": "^7.20.2" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", + "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", + "dev": true, + "requires": { + "@babel/types": "^7.20.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz", + "integrity": "sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.19.0", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" + } + }, + "@babel/helpers": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", + "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "dev": true, + "requires": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", + "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz", + "integrity": "sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-proposal-optional-chaining": "^7.20.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.20.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz", + "integrity": "sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", + "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.19.0" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz", + "integrity": "sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz", + "integrity": "sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", + "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz", + "integrity": "sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz", + "integrity": "sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-optimise-call-expression": "^7.18.6", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-replace-supers": "^7.20.7", + "@babel/helper-split-export-declaration": "^7.18.6", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz", + "integrity": "sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/template": "^7.20.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz", + "integrity": "sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", + "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", + "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", + "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz", + "integrity": "sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", + "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-function-name": "^7.18.9", + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", + "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", + "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz", + "integrity": "sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz", + "integrity": "sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-simple-access": "^7.20.2" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz", + "integrity": "sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-identifier": "^7.19.1" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", + "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz", + "integrity": "sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.20.5", + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", + "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", + "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-replace-supers": "^7.18.6" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.21.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz", + "integrity": "sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", + "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz", + "integrity": "sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "regenerator-transform": "^0.15.1" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", + "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", + "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz", + "integrity": "sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", + "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", + "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", + "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", + "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", + "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/preset-env": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.21.4.tgz", + "integrity": "sha512-2W57zHs2yDLm6GD5ZpvNn71lZ0B/iypSdIeq25OurDKji6AdzV07qp4s3n1/x5BqtiGaTrPN3nerlSCaC5qNTw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-validator-option": "^7.21.0", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.20.7", + "@babel/plugin-proposal-async-generator-functions": "^7.20.7", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-class-static-block": "^7.21.0", + "@babel/plugin-proposal-dynamic-import": "^7.18.6", + "@babel/plugin-proposal-export-namespace-from": "^7.18.9", + "@babel/plugin-proposal-json-strings": "^7.18.6", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", + "@babel/plugin-proposal-numeric-separator": "^7.18.6", + "@babel/plugin-proposal-object-rest-spread": "^7.20.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "@babel/plugin-proposal-private-property-in-object": "^7.21.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.20.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.20.7", + "@babel/plugin-transform-async-to-generator": "^7.20.7", + "@babel/plugin-transform-block-scoped-functions": "^7.18.6", + "@babel/plugin-transform-block-scoping": "^7.21.0", + "@babel/plugin-transform-classes": "^7.21.0", + "@babel/plugin-transform-computed-properties": "^7.20.7", + "@babel/plugin-transform-destructuring": "^7.21.3", + "@babel/plugin-transform-dotall-regex": "^7.18.6", + "@babel/plugin-transform-duplicate-keys": "^7.18.9", + "@babel/plugin-transform-exponentiation-operator": "^7.18.6", + "@babel/plugin-transform-for-of": "^7.21.0", + "@babel/plugin-transform-function-name": "^7.18.9", + "@babel/plugin-transform-literals": "^7.18.9", + "@babel/plugin-transform-member-expression-literals": "^7.18.6", + "@babel/plugin-transform-modules-amd": "^7.20.11", + "@babel/plugin-transform-modules-commonjs": "^7.21.2", + "@babel/plugin-transform-modules-systemjs": "^7.20.11", + "@babel/plugin-transform-modules-umd": "^7.18.6", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.20.5", + "@babel/plugin-transform-new-target": "^7.18.6", + "@babel/plugin-transform-object-super": "^7.18.6", + "@babel/plugin-transform-parameters": "^7.21.3", + "@babel/plugin-transform-property-literals": "^7.18.6", + "@babel/plugin-transform-regenerator": "^7.20.5", + "@babel/plugin-transform-reserved-words": "^7.18.6", + "@babel/plugin-transform-shorthand-properties": "^7.18.6", + "@babel/plugin-transform-spread": "^7.20.7", + "@babel/plugin-transform-sticky-regex": "^7.18.6", + "@babel/plugin-transform-template-literals": "^7.18.9", + "@babel/plugin-transform-typeof-symbol": "^7.18.9", + "@babel/plugin-transform-unicode-escapes": "^7.18.10", + "@babel/plugin-transform-unicode-regex": "^7.18.6", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.21.4", + "babel-plugin-polyfill-corejs2": "^0.3.3", + "babel-plugin-polyfill-corejs3": "^0.6.0", + "babel-plugin-polyfill-regenerator": "^0.4.1", + "core-js-compat": "^3.25.1", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + } + }, + "@babel/traverse": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", + "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.21.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.21.4", + "@babel/types": "^7.21.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + } + } + }, + "@tanem/svg-injector": { + "version": "10.1.55", + "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-10.1.55.tgz", + "integrity": "sha512-xh8ejdvjDaH1eddZC0CdI45eeid4BIU2ppjNEhiTiWMYcLGT19KWjbES/ttDS4mq9gIAQfXx57g5zimEVohqYA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.21.5", + "content-type": "^1.0.5", + "tslib": "^2.5.0" + } + }, + "@types/formatcoords": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/formatcoords/-/formatcoords-1.1.0.tgz", + "integrity": "sha512-T20OHYACraNS/xCoBEmvtUYLc/eYjXaGGDpPFVAyuwarE9KHSGX8DVb3KNBKZ5RQz7fB7qXazZj13520eDSODw==" + }, "@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, + "@types/gtag.js": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", + "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", + "dev": true + }, "@types/leaflet": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz", @@ -3235,6 +7000,23 @@ "@types/geojson": "*" } }, + "@types/node": { + "version": "18.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz", + "integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==", + "dev": true + }, + "@types/sortablejs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz", + "integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==", + "dev": true + }, + "@types/svg-injector": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/svg-injector/-/svg-injector-0.0.29.tgz", + "integrity": "sha512-tNvoN0Xk2si6IfxQI/PqInipOuCyXkDZhCh9Vc4aFv/l1DhIBfTFbXRXYUHBAGXaUNMOCHEtg9475O9J+4NXOg==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3366,6 +7148,202 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + } + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-import-to-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-import-to-require/-/babel-plugin-import-to-require-1.0.0.tgz", + "integrity": "sha512-dc843CwrFivjO8AVgxcHvxl0cb7J7Ed8ZGFP8+PjH3X1CnyzYtAU1WL1349m9Wc/+oqk4ETx2+cIEO2jlp3XyQ==", + "dev": true, + "requires": { + "babel-template": "^6.26.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.3.3", + "semver": "^6.1.1" + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", + "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3", + "core-js-compat": "^3.25.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.3" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + } + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true + } + } + }, + "babelify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/babelify/-/babelify-10.0.0.tgz", + "integrity": "sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==", + "dev": true, + "requires": {} + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3615,6 +7593,18 @@ "pako": "~1.0.5" } }, + "browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + } + }, "buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", @@ -3664,6 +7654,12 @@ "get-intrinsic": "^1.0.2" } }, + "caniuse-lite": { + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", + "dev": true + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3798,9 +7794,9 @@ "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==" }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "convert-source-map": { "version": "1.1.3", @@ -3827,12 +7823,33 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, + "core-js-compat": { + "version": "3.30.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.1.tgz", + "integrity": "sha512-d690npR7MC6P0gq4npTl5n2VQeNAmUrJ90n+MHiKS7W2+xno4o3F5GDEuylSdi6EJ3VssibSGXOa1r3YXD3Mhw==", + "dev": true, + "requires": { + "browserslist": "^4.21.5" + } + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "cp": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/cp/-/cp-0.2.0.tgz", + "integrity": "sha512-4ftCvShHjIZG/zzomHyunNpBof3sOFTTmU6s6q9DdqAL/ANqrKV3pr6Z6kVfBI4hjn59DFLImrBqn7GuuMqSZA==", + "dev": true + }, "create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -4018,6 +8035,12 @@ "jake": "^10.8.5" } }, + "electron-to-chromium": { + "version": "1.4.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz", + "integrity": "sha512-jlBzY4tFcJaiUjzhRTCWAqRvTO/fWzjA3Bls0mykzGZ7zvcMP7h05W6UcgzfT9Ca1SW2xyKDOFRyI0pQeRNZGw==", + "dev": true + }, "elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", @@ -4078,6 +8101,36 @@ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "esmify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esmify/-/esmify-2.1.1.tgz", + "integrity": "sha512-GyOVgjG7sNyYB5Mbo15Ll4aGrcXZzZ3LI22rbLOjCI7L/wYelzQpBHRZkZkqbPNZ/QIRilcaHqzgNCLcEsi1lQ==", + "dev": true, + "requires": { + "@babel/core": "^7.2.2", + "@babel/plugin-syntax-async-generators": "^7.2.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-object-rest-spread": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "babel-plugin-import-to-require": "^1.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "^1.6.2", + "duplexer2": "^0.1.4", + "through2": "^2.0.5" + } + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -4157,6 +8210,15 @@ } } }, + "express-basic-auth": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz", + "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==", + "dev": true, + "requires": { + "basic-auth": "^2.0.1" + } + }, "fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -4221,6 +8283,11 @@ "is-callable": "^1.1.3" } }, + "formatcoords": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/formatcoords/-/formatcoords-1.1.3.tgz", + "integrity": "sha512-Ittg5/AsYFCOtcFGPLSmVpP56a8Ionmv4Ys69UmCAdvBnqVzvVVbkZMnjWEmXrZvnmoGQE8YI3RhnxbMQFdYSw==" + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -4255,6 +8322,12 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -4301,6 +8374,12 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4319,6 +8398,23 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true + } + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4481,6 +8577,15 @@ "xtend": "^4.0.0" } }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4604,6 +8709,24 @@ "minimatch": "^3.0.4" } }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -4635,6 +8758,17 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==" }, + "leaflet-control-mini-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/leaflet-control-mini-map/-/leaflet-control-mini-map-0.4.0.tgz", + "integrity": "sha512-qnetYIYFqlrAbX7MaGsAkBsuA2fg3Z4xDRlEXJN1wSrnhNsIhQUzXt7Z3vapdEzx4r7VUqWTaqHYd7eY5C6Gfw==", + "requires": {} + }, + "leaflet.nauticscale": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/leaflet.nauticscale/-/leaflet.nauticscale-1.1.0.tgz", + "integrity": "sha512-kJqOVuY0bk3CjSWb1CUYsjXiM+W1K0TrlJmMjl/wVubKK2y0PZ+XxkIyD6cxVsKQGlJvLGMvrSqaYdj5LW1O1Q==" + }, "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -4646,12 +8780,36 @@ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "lodash.memoize": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, "map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", @@ -4701,11 +8859,6 @@ } } }, - "milsymbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/milsymbol/-/milsymbol-2.0.0.tgz", - "integrity": "sha512-GcBFrcIUr8jScaZqZb0SI2W6AbnUrPCTHu2kqHxduQjN2DIN8q5pY6ksSWfnJ4HlcIAWQhyotbdPIr1bBxFbwQ==" - }, "mime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", @@ -4806,6 +8959,12 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, "nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -5012,6 +9171,12 @@ "sha.js": "^2.4.8" } }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -5169,6 +9334,67 @@ "picomatch": "^2.2.1" } }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "requires": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true + } + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5316,6 +9542,12 @@ } } }, + "sortablejs": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz", + "integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==", + "dev": true + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5538,6 +9770,12 @@ "process": "~0.11.0" } }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5589,9 +9827,9 @@ } }, "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==", "dev": true }, "tty-browserify": { @@ -5646,11 +9884,49 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -5784,6 +10060,12 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "yargs": { "version": "17.6.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", diff --git a/client/package.json b/client/package.json index 8ef758d9..5030d648 100644 --- a/client/package.json +++ b/client/package.json @@ -2,29 +2,43 @@ "name": "DCSOlympus", "node-main": "./bin/www", "main": "http://localhost:3000", - "version": "0.0.0", + "version": "v0.3.0-alpha", "private": true, "scripts": { + "copy": "copy.bat", "start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"", - "copy": "copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet.css", - "watch": "watchify .\\src\\index.ts --debug -p [ tsify --noImplicitAny ] -o .\\public\\javascripts\\bundle.js" + "watch": "watchify .\\src\\index.ts --debug -o .\\public\\javascripts\\bundle.js -t [ babelify --global true --presets [ @babel/preset-env ] --extensions '.js'] -p [ tsify --noImplicitAny ]" }, "dependencies": { + "@types/formatcoords": "^1.1.0", "@types/geojson": "^7946.0.10", "@types/leaflet": "^1.9.0", + "@types/svg-injector": "^0.0.29", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "^3.1.8", "express": "~4.16.1", + "formatcoords": "^1.1.3", "leaflet": "^1.9.3", - "milsymbol": "^2.0.0", + "leaflet-control-mini-map": "^0.4.0", + "leaflet.nauticscale": "^1.1.0", "morgan": "~1.9.1", "save": "^2.9.0" }, "devDependencies": { + "@babel/preset-env": "^7.21.4", + "@tanem/svg-injector": "^10.1.55", + "@types/gtag.js": "^0.0.12", + "@types/node": "^18.16.1", + "@types/sortablejs": "^1.15.0", + "babelify": "^10.0.0", "browserify": "^17.0.0", "concurrently": "^7.6.0", + "cp": "^0.2.0", + "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", + "sortablejs": "^1.15.0", "tsify": "^5.0.4", "typescript": "^4.9.4", "watchify": "^4.0.0" diff --git a/client/prepare.bat b/client/prepare.bat new file mode 100644 index 00000000..f163f70e --- /dev/null +++ b/client/prepare.bat @@ -0,0 +1,2 @@ +copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet\\leaflet.css +copy .\\node_modules\\leaflet.nauticscale\\dist\\leaflet.nauticscale.js .\\public\\javascripts\\leaflet.nauticscale.js diff --git a/client/public/images/BEBlue.png b/client/public/images/BEBlue.png deleted file mode 100644 index 93338e8a..00000000 Binary files a/client/public/images/BEBlue.png and /dev/null differ diff --git a/client/public/images/BERed.png b/client/public/images/BERed.png deleted file mode 100644 index 367984f9..00000000 Binary files a/client/public/images/BERed.png and /dev/null differ diff --git a/client/public/images/airbase.png b/client/public/images/airbase.png deleted file mode 100644 index 874174d6..00000000 Binary files a/client/public/images/airbase.png and /dev/null differ diff --git a/client/public/images/banner.xcf b/client/public/images/banner.xcf deleted file mode 100644 index e6031764..00000000 Binary files a/client/public/images/banner.xcf and /dev/null differ diff --git a/client/public/images/bullseye.png b/client/public/images/bullseye.png deleted file mode 100644 index f7024954..00000000 Binary files a/client/public/images/bullseye.png and /dev/null differ diff --git a/client/public/images/bullseye.xcf b/client/public/images/bullseye.xcf deleted file mode 100644 index 097722f6..00000000 Binary files a/client/public/images/bullseye.xcf and /dev/null differ diff --git a/client/public/images/bullseye0.png b/client/public/images/bullseye0.png deleted file mode 100644 index a51fe39f..00000000 Binary files a/client/public/images/bullseye0.png and /dev/null differ diff --git a/client/public/images/bullseye1.png b/client/public/images/bullseye1.png deleted file mode 100644 index 6d574bbd..00000000 Binary files a/client/public/images/bullseye1.png and /dev/null differ diff --git a/client/public/images/bullseye2.png b/client/public/images/bullseye2.png deleted file mode 100644 index 4e99f79e..00000000 Binary files a/client/public/images/bullseye2.png and /dev/null differ diff --git a/client/public/images/buttons/ai-full.svg b/client/public/images/buttons/ai-full.svg deleted file mode 100644 index dc1537a2..00000000 --- a/client/public/images/buttons/ai-full.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - diff --git a/client/public/images/buttons/ai-hidden.svg b/client/public/images/buttons/ai-hidden.svg deleted file mode 100644 index 41af77be..00000000 --- a/client/public/images/buttons/ai-hidden.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - diff --git a/client/public/images/buttons/ai-none.svg b/client/public/images/buttons/ai-none.svg deleted file mode 100644 index 71dccfeb..00000000 --- a/client/public/images/buttons/ai-none.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - diff --git a/client/public/images/buttons/ai-partial.svg b/client/public/images/buttons/ai-partial.svg deleted file mode 100644 index 342f746c..00000000 --- a/client/public/images/buttons/ai-partial.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - diff --git a/client/public/images/buttons/ai.svg b/client/public/images/buttons/ai.svg deleted file mode 100644 index 75075b45..00000000 --- a/client/public/images/buttons/ai.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/buttons/attack.png b/client/public/images/buttons/attack.png deleted file mode 100644 index c1a3e62a..00000000 Binary files a/client/public/images/buttons/attack.png and /dev/null differ diff --git a/client/public/images/buttons/climb.svg b/client/public/images/buttons/climb.svg deleted file mode 100644 index 234a1248..00000000 --- a/client/public/images/buttons/climb.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - diff --git a/client/public/images/buttons/create.svg b/client/public/images/buttons/create.svg deleted file mode 100644 index bc76cb21..00000000 --- a/client/public/images/buttons/create.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/buttons/dead-hidden.svg b/client/public/images/buttons/dead-hidden.svg deleted file mode 100644 index 28299e3b..00000000 --- a/client/public/images/buttons/dead-hidden.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - diff --git a/client/public/images/buttons/dead.svg b/client/public/images/buttons/dead.svg deleted file mode 100644 index ae7e8e54..00000000 --- a/client/public/images/buttons/dead.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - diff --git a/client/public/images/buttons/descend.svg b/client/public/images/buttons/descend.svg deleted file mode 100644 index dd8958c6..00000000 --- a/client/public/images/buttons/descend.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - diff --git a/client/public/images/buttons/erase.svg b/client/public/images/buttons/erase.svg deleted file mode 100644 index f6588839..00000000 --- a/client/public/images/buttons/erase.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/buttons/fast.svg b/client/public/images/buttons/fast.svg deleted file mode 100644 index e62b23f0..00000000 --- a/client/public/images/buttons/fast.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/buttons/formation.png b/client/public/images/buttons/formation.png deleted file mode 100644 index e75ccea1..00000000 Binary files a/client/public/images/buttons/formation.png and /dev/null differ diff --git a/client/public/images/buttons/rtb.png b/client/public/images/buttons/rtb.png deleted file mode 100644 index e35be5e8..00000000 Binary files a/client/public/images/buttons/rtb.png and /dev/null differ diff --git a/client/public/images/buttons/slow.svg b/client/public/images/buttons/slow.svg deleted file mode 100644 index 9579b19b..00000000 --- a/client/public/images/buttons/slow.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/buttons/spawnAWACS.png b/client/public/images/buttons/spawnAWACS.png deleted file mode 100644 index b325f4ad..00000000 Binary files a/client/public/images/buttons/spawnAWACS.png and /dev/null differ diff --git a/client/public/images/buttons/spawnAir.png b/client/public/images/buttons/spawnAir.png deleted file mode 100644 index 292ffe3e..00000000 Binary files a/client/public/images/buttons/spawnAir.png and /dev/null differ diff --git a/client/public/images/buttons/spawnCAP.png b/client/public/images/buttons/spawnCAP.png deleted file mode 100644 index ae78e677..00000000 Binary files a/client/public/images/buttons/spawnCAP.png and /dev/null differ diff --git a/client/public/images/buttons/spawnCAS.png b/client/public/images/buttons/spawnCAS.png deleted file mode 100644 index 315bd363..00000000 Binary files a/client/public/images/buttons/spawnCAS.png and /dev/null differ diff --git a/client/public/images/buttons/spawnDrone.png b/client/public/images/buttons/spawnDrone.png deleted file mode 100644 index 98beca17..00000000 Binary files a/client/public/images/buttons/spawnDrone.png and /dev/null differ diff --git a/client/public/images/buttons/spawnExplosion.png b/client/public/images/buttons/spawnExplosion.png deleted file mode 100644 index d61cdf53..00000000 Binary files a/client/public/images/buttons/spawnExplosion.png and /dev/null differ diff --git a/client/public/images/buttons/spawnGround.png b/client/public/images/buttons/spawnGround.png deleted file mode 100644 index 1c421d0f..00000000 Binary files a/client/public/images/buttons/spawnGround.png and /dev/null differ diff --git a/client/public/images/buttons/spawnHowitzer.png b/client/public/images/buttons/spawnHowitzer.png deleted file mode 100644 index 6197a0c6..00000000 Binary files a/client/public/images/buttons/spawnHowitzer.png and /dev/null differ diff --git a/client/public/images/buttons/spawnIFV.png b/client/public/images/buttons/spawnIFV.png deleted file mode 100644 index 77c89ad3..00000000 Binary files a/client/public/images/buttons/spawnIFV.png and /dev/null differ diff --git a/client/public/images/buttons/spawnMLRS.png b/client/public/images/buttons/spawnMLRS.png deleted file mode 100644 index 213befc7..00000000 Binary files a/client/public/images/buttons/spawnMLRS.png and /dev/null differ diff --git a/client/public/images/buttons/spawnRadar.png b/client/public/images/buttons/spawnRadar.png deleted file mode 100644 index d87470c3..00000000 Binary files a/client/public/images/buttons/spawnRadar.png and /dev/null differ diff --git a/client/public/images/buttons/spawnSAM.png b/client/public/images/buttons/spawnSAM.png deleted file mode 100644 index 9d7451e1..00000000 Binary files a/client/public/images/buttons/spawnSAM.png and /dev/null differ diff --git a/client/public/images/buttons/spawnSmoke.png b/client/public/images/buttons/spawnSmoke.png deleted file mode 100644 index 53d82f8f..00000000 Binary files a/client/public/images/buttons/spawnSmoke.png and /dev/null differ diff --git a/client/public/images/buttons/spawnStrike.png b/client/public/images/buttons/spawnStrike.png deleted file mode 100644 index 1cba2589..00000000 Binary files a/client/public/images/buttons/spawnStrike.png and /dev/null differ diff --git a/client/public/images/buttons/spawnTank.png b/client/public/images/buttons/spawnTank.png deleted file mode 100644 index f016b64e..00000000 Binary files a/client/public/images/buttons/spawnTank.png and /dev/null differ diff --git a/client/public/images/buttons/spawnTanker.png b/client/public/images/buttons/spawnTanker.png deleted file mode 100644 index 374fb248..00000000 Binary files a/client/public/images/buttons/spawnTanker.png and /dev/null differ diff --git a/client/public/images/buttons/spawnTransport.png b/client/public/images/buttons/spawnTransport.png deleted file mode 100644 index b5932d58..00000000 Binary files a/client/public/images/buttons/spawnTransport.png and /dev/null differ diff --git a/client/public/images/buttons/spawnUnarmed.png b/client/public/images/buttons/spawnUnarmed.png deleted file mode 100644 index 8aabc9e7..00000000 Binary files a/client/public/images/buttons/spawnUnarmed.png and /dev/null differ diff --git a/client/public/images/buttons/tanker.png b/client/public/images/buttons/tanker.png deleted file mode 100644 index 86677ffe..00000000 Binary files a/client/public/images/buttons/tanker.png and /dev/null differ diff --git a/client/public/images/buttons/user-full.svg b/client/public/images/buttons/user-full.svg deleted file mode 100644 index b629abe4..00000000 --- a/client/public/images/buttons/user-full.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - diff --git a/client/public/images/buttons/user-hidden.svg b/client/public/images/buttons/user-hidden.svg deleted file mode 100644 index e2bcf458..00000000 --- a/client/public/images/buttons/user-hidden.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - diff --git a/client/public/images/buttons/user-none.svg b/client/public/images/buttons/user-none.svg deleted file mode 100644 index 55bc1337..00000000 --- a/client/public/images/buttons/user-none.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - diff --git a/client/public/images/buttons/user-partial.svg b/client/public/images/buttons/user-partial.svg deleted file mode 100644 index 503b92e1..00000000 --- a/client/public/images/buttons/user-partial.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - diff --git a/client/public/images/buttons/weapon-hidden.svg b/client/public/images/buttons/weapon-hidden.svg deleted file mode 100644 index 0bfa5905..00000000 --- a/client/public/images/buttons/weapon-hidden.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - diff --git a/client/public/images/buttons/weapon-none.svg b/client/public/images/buttons/weapon-none.svg deleted file mode 100644 index ad4d9a98..00000000 --- a/client/public/images/buttons/weapon-none.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - diff --git a/client/public/images/buttons/weapon-partial.svg b/client/public/images/buttons/weapon-partial.svg deleted file mode 100644 index c925639b..00000000 --- a/client/public/images/buttons/weapon-partial.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - diff --git a/client/public/images/buttons/wheelButtons.xcf b/client/public/images/buttons/wheelButtons.xcf deleted file mode 100644 index 5ef9f1f3..00000000 Binary files a/client/public/images/buttons/wheelButtons.xcf and /dev/null differ diff --git a/client/public/images/icon-round.png b/client/public/images/icon-round.png new file mode 100644 index 00000000..0244ffa7 Binary files /dev/null and b/client/public/images/icon-round.png differ diff --git a/client/public/images/icon.png b/client/public/images/icon.png new file mode 100644 index 00000000..dc5c994e Binary files /dev/null and b/client/public/images/icon.png differ diff --git a/client/public/images/icons/formation.png b/client/public/images/icons/formation.png deleted file mode 100644 index b140e9f6..00000000 Binary files a/client/public/images/icons/formation.png and /dev/null differ diff --git a/client/public/images/icons/leader.png b/client/public/images/icons/leader.png deleted file mode 100644 index 8744386f..00000000 Binary files a/client/public/images/icons/leader.png and /dev/null differ diff --git a/client/public/images/icons/singleton.png b/client/public/images/icons/singleton.png deleted file mode 100644 index 06644361..00000000 Binary files a/client/public/images/icons/singleton.png and /dev/null differ diff --git a/client/public/images/olympus-4112x4112.png b/client/public/images/olympus-4112x4112.png new file mode 100644 index 00000000..fedbfca1 Binary files /dev/null and b/client/public/images/olympus-4112x4112.png differ diff --git a/client/public/images/olympus-500x500.png b/client/public/images/olympus-500x500.png new file mode 100644 index 00000000..42c1b326 Binary files /dev/null and b/client/public/images/olympus-500x500.png differ diff --git a/client/public/images/patch.png b/client/public/images/patch.png deleted file mode 100644 index 46b43691..00000000 Binary files a/client/public/images/patch.png and /dev/null differ diff --git a/client/public/images/pin.png b/client/public/images/pin.png deleted file mode 100644 index c6222cd2..00000000 Binary files a/client/public/images/pin.png and /dev/null differ diff --git a/client/public/images/pin.svg b/client/public/images/pin.svg deleted file mode 100644 index 19e92ec2..00000000 --- a/client/public/images/pin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/images/unit.png b/client/public/images/unit.png deleted file mode 100644 index ae72bc19..00000000 Binary files a/client/public/images/unit.png and /dev/null differ diff --git a/client/public/images/units/a-10.png b/client/public/images/units/a-10.png new file mode 100644 index 00000000..748be4cb Binary files /dev/null and b/client/public/images/units/a-10.png differ diff --git a/client/public/images/units/a-20.png b/client/public/images/units/a-20.png new file mode 100644 index 00000000..d0fb8a0c Binary files /dev/null and b/client/public/images/units/a-20.png differ diff --git a/client/public/images/units/a-29.png b/client/public/images/units/a-29.png new file mode 100644 index 00000000..8f2ead6e Binary files /dev/null and b/client/public/images/units/a-29.png differ diff --git a/client/public/images/units/a-4.png b/client/public/images/units/a-4.png new file mode 100644 index 00000000..dadcb05c Binary files /dev/null and b/client/public/images/units/a-4.png differ diff --git a/client/public/images/units/a-400.png b/client/public/images/units/a-400.png new file mode 100644 index 00000000..71863ac1 Binary files /dev/null and b/client/public/images/units/a-400.png differ diff --git a/client/public/images/units/a-50.png b/client/public/images/units/a-50.png new file mode 100644 index 00000000..a14b837a Binary files /dev/null and b/client/public/images/units/a-50.png differ diff --git a/client/public/images/units/a-6.png b/client/public/images/units/a-6.png new file mode 100644 index 00000000..8c198a7e Binary files /dev/null and b/client/public/images/units/a-6.png differ diff --git a/client/public/images/units/ah-1.png b/client/public/images/units/ah-1.png new file mode 100644 index 00000000..4d1b3b40 Binary files /dev/null and b/client/public/images/units/ah-1.png differ diff --git a/client/public/images/units/ah-64.png b/client/public/images/units/ah-64.png new file mode 100644 index 00000000..ba5f43b6 Binary files /dev/null and b/client/public/images/units/ah-64.png differ diff --git a/client/public/images/units/airUnit.png b/client/public/images/units/airUnit.png new file mode 100644 index 00000000..6578221b Binary files /dev/null and b/client/public/images/units/airUnit.png differ diff --git a/client/public/images/units/aircraft.png b/client/public/images/units/aircraft.png deleted file mode 100644 index 70c8db7e..00000000 Binary files a/client/public/images/units/aircraft.png and /dev/null differ diff --git a/client/public/images/units/airliner2engine.png b/client/public/images/units/airliner2engine.png new file mode 100644 index 00000000..ae27a626 Binary files /dev/null and b/client/public/images/units/airliner2engine.png differ diff --git a/client/public/images/units/an-26.png b/client/public/images/units/an-26.png new file mode 100644 index 00000000..2976fdee Binary files /dev/null and b/client/public/images/units/an-26.png differ diff --git a/client/public/images/units/av8bna.png b/client/public/images/units/av8bna.png new file mode 100644 index 00000000..bb3fc845 Binary files /dev/null and b/client/public/images/units/av8bna.png differ diff --git a/client/public/images/units/b-1.png b/client/public/images/units/b-1.png new file mode 100644 index 00000000..dda58612 Binary files /dev/null and b/client/public/images/units/b-1.png differ diff --git a/client/public/images/units/b-17.png b/client/public/images/units/b-17.png new file mode 100644 index 00000000..a1515b20 Binary files /dev/null and b/client/public/images/units/b-17.png differ diff --git a/client/public/images/units/b-2.png b/client/public/images/units/b-2.png new file mode 100644 index 00000000..e68ac71f Binary files /dev/null and b/client/public/images/units/b-2.png differ diff --git a/client/public/images/units/b-52.png b/client/public/images/units/b-52.png new file mode 100644 index 00000000..ebc22079 Binary files /dev/null and b/client/public/images/units/b-52.png differ diff --git a/client/public/images/units/b707.png b/client/public/images/units/b707.png new file mode 100644 index 00000000..f8496dd2 Binary files /dev/null and b/client/public/images/units/b707.png differ diff --git a/client/public/images/units/bf109.png b/client/public/images/units/bf109.png new file mode 100644 index 00000000..e0d3691c Binary files /dev/null and b/client/public/images/units/bf109.png differ diff --git a/client/public/images/units/bomb.png b/client/public/images/units/bomb.png deleted file mode 100644 index 81416cbb..00000000 Binary files a/client/public/images/units/bomb.png and /dev/null differ diff --git a/client/public/images/units/c-101.png b/client/public/images/units/c-101.png new file mode 100644 index 00000000..e4372e90 Binary files /dev/null and b/client/public/images/units/c-101.png differ diff --git a/client/public/images/units/c-130.png b/client/public/images/units/c-130.png new file mode 100644 index 00000000..74f98897 Binary files /dev/null and b/client/public/images/units/c-130.png differ diff --git a/client/public/images/units/c-17.png b/client/public/images/units/c-17.png new file mode 100644 index 00000000..4339ab18 Binary files /dev/null and b/client/public/images/units/c-17.png differ diff --git a/client/public/images/units/c-5.png b/client/public/images/units/c-5.png new file mode 100644 index 00000000..00fccd57 Binary files /dev/null and b/client/public/images/units/c-5.png differ diff --git a/client/public/images/units/ch-47.png b/client/public/images/units/ch-47.png new file mode 100644 index 00000000..9a1ecd22 Binary files /dev/null and b/client/public/images/units/ch-47.png differ diff --git a/client/public/images/units/ch-53.png b/client/public/images/units/ch-53.png new file mode 100644 index 00000000..64a2b083 Binary files /dev/null and b/client/public/images/units/ch-53.png differ diff --git a/client/public/images/units/e-2.png b/client/public/images/units/e-2.png new file mode 100644 index 00000000..fc797e9e Binary files /dev/null and b/client/public/images/units/e-2.png differ diff --git a/client/public/images/units/e-3.png b/client/public/images/units/e-3.png new file mode 100644 index 00000000..1967a347 Binary files /dev/null and b/client/public/images/units/e-3.png differ diff --git a/client/public/images/units/eurofighter.png b/client/public/images/units/eurofighter.png new file mode 100644 index 00000000..f18076e1 Binary files /dev/null and b/client/public/images/units/eurofighter.png differ diff --git a/client/public/images/units/f-111.png b/client/public/images/units/f-111.png new file mode 100644 index 00000000..c424ad6b Binary files /dev/null and b/client/public/images/units/f-111.png differ diff --git a/client/public/images/units/f-117.png b/client/public/images/units/f-117.png new file mode 100644 index 00000000..306133d4 Binary files /dev/null and b/client/public/images/units/f-117.png differ diff --git a/client/public/images/units/f-14.png b/client/public/images/units/f-14.png new file mode 100644 index 00000000..d7af7621 Binary files /dev/null and b/client/public/images/units/f-14.png differ diff --git a/client/public/images/units/f-15.png b/client/public/images/units/f-15.png new file mode 100644 index 00000000..60e26c1b Binary files /dev/null and b/client/public/images/units/f-15.png differ diff --git a/client/public/images/units/f-16c.png b/client/public/images/units/f-16c.png new file mode 100644 index 00000000..a74de3c0 Binary files /dev/null and b/client/public/images/units/f-16c.png differ diff --git a/client/public/images/units/f-22.png b/client/public/images/units/f-22.png new file mode 100644 index 00000000..5f210d30 Binary files /dev/null and b/client/public/images/units/f-22.png differ diff --git a/client/public/images/units/f-35.png b/client/public/images/units/f-35.png new file mode 100644 index 00000000..196283fc Binary files /dev/null and b/client/public/images/units/f-35.png differ diff --git a/client/public/images/units/f-4.png b/client/public/images/units/f-4.png new file mode 100644 index 00000000..21784012 Binary files /dev/null and b/client/public/images/units/f-4.png differ diff --git a/client/public/images/units/f-5.png b/client/public/images/units/f-5.png new file mode 100644 index 00000000..087bff8c Binary files /dev/null and b/client/public/images/units/f-5.png differ diff --git a/client/public/images/units/f-86.png b/client/public/images/units/f-86.png new file mode 100644 index 00000000..ce576340 Binary files /dev/null and b/client/public/images/units/f-86.png differ diff --git a/client/public/images/units/fa-18c.png b/client/public/images/units/fa-18c.png new file mode 100644 index 00000000..d6856033 Binary files /dev/null and b/client/public/images/units/fa-18c.png differ diff --git a/client/public/images/units/fw190.png b/client/public/images/units/fw190.png new file mode 100644 index 00000000..8bc95546 Binary files /dev/null and b/client/public/images/units/fw190.png differ diff --git a/client/public/images/units/general1.png b/client/public/images/units/general1.png new file mode 100644 index 00000000..bf0bc2bd Binary files /dev/null and b/client/public/images/units/general1.png differ diff --git a/client/public/images/units/gripen.png b/client/public/images/units/gripen.png new file mode 100644 index 00000000..0d5812b2 Binary files /dev/null and b/client/public/images/units/gripen.png differ diff --git a/client/public/images/units/ground.png b/client/public/images/units/ground.png deleted file mode 100644 index 8c6ef4ad..00000000 Binary files a/client/public/images/units/ground.png and /dev/null differ diff --git a/client/public/images/units/h-6.png b/client/public/images/units/h-6.png new file mode 100644 index 00000000..9fb48950 Binary files /dev/null and b/client/public/images/units/h-6.png differ diff --git a/client/public/images/units/hawk.png b/client/public/images/units/hawk.png new file mode 100644 index 00000000..5cd07de5 Binary files /dev/null and b/client/public/images/units/hawk.png differ diff --git a/client/public/images/units/helicopter1.png b/client/public/images/units/helicopter1.png new file mode 100644 index 00000000..454152ff Binary files /dev/null and b/client/public/images/units/helicopter1.png differ diff --git a/client/public/images/units/i-16.png b/client/public/images/units/i-16.png new file mode 100644 index 00000000..eea45594 Binary files /dev/null and b/client/public/images/units/i-16.png differ diff --git a/client/public/images/units/il-76.png b/client/public/images/units/il-76.png new file mode 100644 index 00000000..d0a3cf55 Binary files /dev/null and b/client/public/images/units/il-76.png differ diff --git a/client/public/images/units/j-10.png b/client/public/images/units/j-10.png new file mode 100644 index 00000000..6fa2dd21 Binary files /dev/null and b/client/public/images/units/j-10.png differ diff --git a/client/public/images/units/j-20.png b/client/public/images/units/j-20.png new file mode 100644 index 00000000..7e5b34f4 Binary files /dev/null and b/client/public/images/units/j-20.png differ diff --git a/client/public/images/units/j-7.png b/client/public/images/units/j-7.png new file mode 100644 index 00000000..f1bcfd95 Binary files /dev/null and b/client/public/images/units/j-7.png differ diff --git a/client/public/images/units/jf-17.png b/client/public/images/units/jf-17.png new file mode 100644 index 00000000..2bfde2f3 Binary files /dev/null and b/client/public/images/units/jf-17.png differ diff --git a/client/public/images/units/ju-88.png b/client/public/images/units/ju-88.png new file mode 100644 index 00000000..59768856 Binary files /dev/null and b/client/public/images/units/ju-88.png differ diff --git a/client/public/images/units/ka-27.png b/client/public/images/units/ka-27.png new file mode 100644 index 00000000..be2b243d Binary files /dev/null and b/client/public/images/units/ka-27.png differ diff --git a/client/public/images/units/ka-50.png b/client/public/images/units/ka-50.png new file mode 100644 index 00000000..71a3b4b4 Binary files /dev/null and b/client/public/images/units/ka-50.png differ diff --git a/client/public/images/units/kc-10.png b/client/public/images/units/kc-10.png new file mode 100644 index 00000000..94868685 Binary files /dev/null and b/client/public/images/units/kc-10.png differ diff --git a/client/public/images/units/kc-135.png b/client/public/images/units/kc-135.png new file mode 100644 index 00000000..64014a04 Binary files /dev/null and b/client/public/images/units/kc-135.png differ diff --git a/client/public/images/units/l-159.png b/client/public/images/units/l-159.png new file mode 100644 index 00000000..e80c6259 Binary files /dev/null and b/client/public/images/units/l-159.png differ diff --git a/client/public/images/units/l-39.png b/client/public/images/units/l-39.png new file mode 100644 index 00000000..0a9558be Binary files /dev/null and b/client/public/images/units/l-39.png differ diff --git a/client/public/images/units/m2000.png b/client/public/images/units/m2000.png new file mode 100644 index 00000000..03e618f9 Binary files /dev/null and b/client/public/images/units/m2000.png differ diff --git a/client/public/images/units/mi-24.png b/client/public/images/units/mi-24.png new file mode 100644 index 00000000..8708f4a7 Binary files /dev/null and b/client/public/images/units/mi-24.png differ diff --git a/client/public/images/units/mi-26.png b/client/public/images/units/mi-26.png new file mode 100644 index 00000000..4ad1cfd8 Binary files /dev/null and b/client/public/images/units/mi-26.png differ diff --git a/client/public/images/units/mi-28.png b/client/public/images/units/mi-28.png new file mode 100644 index 00000000..9e5aefff Binary files /dev/null and b/client/public/images/units/mi-28.png differ diff --git a/client/public/images/units/mi-8.png b/client/public/images/units/mi-8.png new file mode 100644 index 00000000..6cb33921 Binary files /dev/null and b/client/public/images/units/mi-8.png differ diff --git a/client/public/images/units/mig-15.png b/client/public/images/units/mig-15.png new file mode 100644 index 00000000..1f34bc37 Binary files /dev/null and b/client/public/images/units/mig-15.png differ diff --git a/client/public/images/units/mig-19.png b/client/public/images/units/mig-19.png new file mode 100644 index 00000000..e3ad0cb7 Binary files /dev/null and b/client/public/images/units/mig-19.png differ diff --git a/client/public/images/units/mig-21.png b/client/public/images/units/mig-21.png new file mode 100644 index 00000000..09c98941 Binary files /dev/null and b/client/public/images/units/mig-21.png differ diff --git a/client/public/images/units/mig-23.png b/client/public/images/units/mig-23.png new file mode 100644 index 00000000..d1d70ccf Binary files /dev/null and b/client/public/images/units/mig-23.png differ diff --git a/client/public/images/units/mig-25.png b/client/public/images/units/mig-25.png new file mode 100644 index 00000000..48448104 Binary files /dev/null and b/client/public/images/units/mig-25.png differ diff --git a/client/public/images/units/mig-29.png b/client/public/images/units/mig-29.png new file mode 100644 index 00000000..1e16dfa0 Binary files /dev/null and b/client/public/images/units/mig-29.png differ diff --git a/client/public/images/units/missile.png b/client/public/images/units/missile.png deleted file mode 100644 index ef547853..00000000 Binary files a/client/public/images/units/missile.png and /dev/null differ diff --git a/client/public/images/units/mosquito.png b/client/public/images/units/mosquito.png new file mode 100644 index 00000000..eba239e5 Binary files /dev/null and b/client/public/images/units/mosquito.png differ diff --git a/client/public/images/units/oh-58.png b/client/public/images/units/oh-58.png new file mode 100644 index 00000000..e9747103 Binary files /dev/null and b/client/public/images/units/oh-58.png differ diff --git a/client/public/images/units/p-47.png b/client/public/images/units/p-47.png new file mode 100644 index 00000000..72fc236e Binary files /dev/null and b/client/public/images/units/p-47.png differ diff --git a/client/public/images/units/p-51.png b/client/public/images/units/p-51.png new file mode 100644 index 00000000..cc67b897 Binary files /dev/null and b/client/public/images/units/p-51.png differ diff --git a/client/public/images/units/rafale.png b/client/public/images/units/rafale.png new file mode 100644 index 00000000..b2370ad4 Binary files /dev/null and b/client/public/images/units/rafale.png differ diff --git a/client/public/images/units/rq-1.png b/client/public/images/units/rq-1.png new file mode 100644 index 00000000..cf3b0ad4 Binary files /dev/null and b/client/public/images/units/rq-1.png differ diff --git a/client/public/images/units/rq-4.png b/client/public/images/units/rq-4.png new file mode 100644 index 00000000..090074de Binary files /dev/null and b/client/public/images/units/rq-4.png differ diff --git a/client/public/images/units/s-3.png b/client/public/images/units/s-3.png new file mode 100644 index 00000000..ec65bb2e Binary files /dev/null and b/client/public/images/units/s-3.png differ diff --git a/client/public/images/units/sa-342.png b/client/public/images/units/sa-342.png new file mode 100644 index 00000000..fe846183 Binary files /dev/null and b/client/public/images/units/sa-342.png differ diff --git a/client/public/images/units/spitfire.png b/client/public/images/units/spitfire.png new file mode 100644 index 00000000..d6b9d2ab Binary files /dev/null and b/client/public/images/units/spitfire.png differ diff --git a/client/public/images/units/su-17.png b/client/public/images/units/su-17.png new file mode 100644 index 00000000..32d8aa37 Binary files /dev/null and b/client/public/images/units/su-17.png differ diff --git a/client/public/images/units/su-24.png b/client/public/images/units/su-24.png new file mode 100644 index 00000000..d94b7af7 Binary files /dev/null and b/client/public/images/units/su-24.png differ diff --git a/client/public/images/units/su-25.png b/client/public/images/units/su-25.png new file mode 100644 index 00000000..fa85f62f Binary files /dev/null and b/client/public/images/units/su-25.png differ diff --git a/client/public/images/units/su-27.png b/client/public/images/units/su-27.png new file mode 100644 index 00000000..cc2ce07a Binary files /dev/null and b/client/public/images/units/su-27.png differ diff --git a/client/public/images/units/su-34.png b/client/public/images/units/su-34.png new file mode 100644 index 00000000..110a3c60 Binary files /dev/null and b/client/public/images/units/su-34.png differ diff --git a/client/public/images/units/su-57.png b/client/public/images/units/su-57.png new file mode 100644 index 00000000..c4e83c98 Binary files /dev/null and b/client/public/images/units/su-57.png differ diff --git a/client/public/images/units/tornado.png b/client/public/images/units/tornado.png new file mode 100644 index 00000000..c66219ea Binary files /dev/null and b/client/public/images/units/tornado.png differ diff --git a/client/public/images/units/tu-160.png b/client/public/images/units/tu-160.png new file mode 100644 index 00000000..0391ce41 Binary files /dev/null and b/client/public/images/units/tu-160.png differ diff --git a/client/public/images/units/tu-22.png b/client/public/images/units/tu-22.png new file mode 100644 index 00000000..cb43ca76 Binary files /dev/null and b/client/public/images/units/tu-22.png differ diff --git a/client/public/images/units/tu-95.png b/client/public/images/units/tu-95.png new file mode 100644 index 00000000..cfa72a60 Binary files /dev/null and b/client/public/images/units/tu-95.png differ diff --git a/client/public/images/units/u-28.png b/client/public/images/units/u-28.png new file mode 100644 index 00000000..6e194456 Binary files /dev/null and b/client/public/images/units/u-28.png differ diff --git a/client/public/images/units/uh-1.png b/client/public/images/units/uh-1.png new file mode 100644 index 00000000..0d1102a4 Binary files /dev/null and b/client/public/images/units/uh-1.png differ diff --git a/client/public/images/units/uh-60.png b/client/public/images/units/uh-60.png new file mode 100644 index 00000000..145fa141 Binary files /dev/null and b/client/public/images/units/uh-60.png differ diff --git a/client/public/images/units/viggen.png b/client/public/images/units/viggen.png new file mode 100644 index 00000000..76c5eda0 Binary files /dev/null and b/client/public/images/units/viggen.png differ diff --git a/client/public/images/units/yak-40.png b/client/public/images/units/yak-40.png new file mode 100644 index 00000000..42a56792 Binary files /dev/null and b/client/public/images/units/yak-40.png differ diff --git a/client/public/images/units/yak-52.png b/client/public/images/units/yak-52.png new file mode 100644 index 00000000..1201b6d0 Binary files /dev/null and b/client/public/images/units/yak-52.png differ diff --git a/client/public/javascripts/leaflet.nauticscale.js b/client/public/javascripts/leaflet.nauticscale.js new file mode 100644 index 00000000..ccf19ac3 --- /dev/null +++ b/client/public/javascripts/leaflet.nauticscale.js @@ -0,0 +1,38 @@ +L.Control.ScaleNautic = L.Control.Scale.extend({ + options: { + nautic: false + }, + + _addScales: function(options, className, container) { + L.Control.Scale.prototype._addScales.call(this, options, className, container); + L.setOptions(options); + if (this.options.nautic) { + this._nScale = L.DomUtil.create('div', className, container); + } + }, + + _updateScales: function (maxMeters) { + L.Control.Scale.prototype._updateScales.call(this, maxMeters); + if (this.options.nautic && maxMeters) { + this._updateNautic(maxMeters); + } + }, + + _updateNautic: function (maxMeters) { + var scale = this._nScale, + maxNauticalMiles = maxMeters / 1852, nauticalMiles; + + if(maxMeters >= 1852) { + nauticalMiles = L.Control.Scale.prototype._getRoundNum.call(this, maxNauticalMiles); + } else { + nauticalMiles = maxNauticalMiles > 0.1 ? Math.round(maxNauticalMiles * 10) / 10 : Math.round(maxNauticalMiles * 100) / 100; + } + + scale.style.width = Math.round(this.options.maxWidth * (nauticalMiles / maxNauticalMiles)) - 10 + 'px'; + scale.innerHTML = nauticalMiles + ' nm'; + } +}); + +L.control.scalenautic = function (options) { + return new L.Control.ScaleNautic(options); +}; diff --git a/client/public/stylesheets/aic/aic.css b/client/public/stylesheets/aic/aic.css new file mode 100644 index 00000000..ea972d71 --- /dev/null +++ b/client/public/stylesheets/aic/aic.css @@ -0,0 +1,194 @@ +/**************************************/ + +.olympus-dialog { + align-self: center; + background:white; + border-radius: 10px; + display: flex; + flex-direction: column; + justify-self: center; + padding:10px; + position:absolute; + width:fit-content; + z-index: 9999; +} + +.olympus-dialog-close { + cursor:pointer; + position:absolute; + right:10px; + top:5px; +} + +.olympus-dialog-header { + font-weight:bold; +} + + +/**************************************/ + + +/***** AIC *****/ + +.aic-panel { + z-index: 9999; +} + +#aic-control-panel { + bottom:30px; + position: absolute; + left:30px; +} + + +#aic-control-panel .olympus-button img { + max-width: 32px; +} + + +#aic-toolbox, #aic-callsign-panel { + align-items: flex-start; + align-self: center; + flex-direction: column; + row-gap: 10px; + display:none; + position:absolute; +} + +.aic-panel { + background:#eaeaea; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + justify-self: left; + padding:5px 10px; +} + +.aic-enabled #aic-toolbox, .aic-enabled #aic-callsign-panel { + display:flex; +} + +.aic-enabled #aic-callsign-panel { + align-self: auto; + top: 100px; +} + +.aic-panel h2 { + font-size:90%; + margin:0; + padding:0; + text-align: center; +} + +#aic-callsign-display { + text-align: center; +} + +#aic-formation-list { + display:flex; + flex-direction: column; + justify-content: center; +} + +#aic-formation-list > div { + align-items: center; + cursor: pointer; + display:flex; + flex-direction: column; + justify-content: center; + margin-top:10px; + position:relative; +} + +#aic-formation-list .aic-formation-image img { + border: 1px solid #ccc; + border-radius: 10px; + max-width: 50px; +} + +#aic-formation-list .aic-formation-name { + font-size:90%; +} + +#aic-formation-list .aic-formation-descriptor { + background:white; + border-radius: 10px; + left:100px; + padding:5px; + position:absolute; + width: max-content; +} + +#aic-teleprompt { + background-color: white; + border:2px solid black; + border-radius: 10px; + bottom: 50px; + color: black; + display: none; + justify-content: center; + justify-self: center; + padding: 10px; + position: absolute; + width: fit-content; + z-index: 9999; +} + +.aic-enabled #aic-teleprompt { + display:flex; +} + +#aic-descriptor { + display:flex; + flex-direction: row; +} + +#aic-descriptor .aic-descriptor-section { + display:flex; + flex-direction: column; + margin:0 10px; +} + +#aic-descriptor .aic-descriptor-section-label { + background-color:#eaeaea; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding:.25em; + text-align: center; +} + +#aic-descriptor .aic-descriptor-phrase { + border-bottom: 1px solid #ccc; + display:flex; + flex-direction: row; + margin-bottom:5px; + padding-bottom:2px; +} + +#aic-descriptor .aic-descriptor-phrase:last-of-type { + margin-bottom: 0; +} + +#aic-descriptor .aic-descriptor-components .aic-descriptor-component { + margin:0 5px; + text-align: center; +} + +#aic-descriptor .aic-descriptor-component-label { + display:none; +} + +#aic-descriptor .aic-descriptor-component-value:after { + content:","; + margin-right:5px; +} + +#aic-descriptor .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after { + content:"; "; +} + +#aic-descriptor .aic-descriptor-section:last-of-type .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after { + content:"."; +} + + +/**************************************/ \ No newline at end of file diff --git a/client/public/stylesheets/airbasemarker.css b/client/public/stylesheets/airbasemarker.css deleted file mode 100644 index e102e3aa..00000000 --- a/client/public/stylesheets/airbasemarker.css +++ /dev/null @@ -1,36 +0,0 @@ -.airbasemarker-container { - height: 60px; - width: 60px; - left: -30px; - top: -30px; - border: 1px transparent solid; - position: absolute; -} - -.airbasemarker-icon { - height: 60px; - width: 60px; - left: 0px; - top: 0px; - display: block; - 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-blue { - filter: invert(69%) sepia(84%) saturate(264%) hue-rotate(162deg) brightness(100%) contrast(102%) drop-shadow(0px 1px 0 #000A) drop-shadow(1px 0px 0 #000A) drop-shadow(-1px 0px 0 #000A) drop-shadow(0px -1px 0 #000A); -} - -.airbasemarker-icon-red { - filter: invert(68%) sepia(85%) saturate(2385%) hue-rotate(313deg) brightness(108%) contrast(102%) drop-shadow(0px 1px 0 #000A) drop-shadow(1px 0px 0 #000A) drop-shadow(-1px 0px 0 #000A) drop-shadow(0px -1px 0 #000A); -} - -.airbasemarker-name { - 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/client/public/stylesheets/atc/atc.css b/client/public/stylesheets/atc/atc.css new file mode 100644 index 00000000..e9fa7441 --- /dev/null +++ b/client/public/stylesheets/atc/atc.css @@ -0,0 +1,205 @@ +.ol-strip-board .ol-dialog-header { + align-items: center; + display:flex; + justify-content: space-between; +} + +.ol-strip-board-strips { + display:flex; + flex-direction: column; + row-gap: 4px; +} + +.ol-strip-board-strip { + align-items: center; + border-radius: var( --border-radius-sm ); + column-gap: 4px; + display:flex; + flex-flow: row nowrap; + row-gap:4px; +} + +.ol-strip-board-strip[data-flight-status="checkedin"] { + background-color: #ffffff2A; +} + +.ol-strip-board-strip[data-flight-status="readytotaxi"] { + background-color: #ffff0063; +} + +.ol-strip-board-strip[data-flight-status="clearedtotaxi"] { + background-color: #00ff0030; +} + +.ol-strip-board-strip[data-flight-status="halted"] { + background-color: #FF000040; +} + +.ol-strip-board-strip[data-flight-status="terminated"] { + background-color: black; +} + +.ol-strip-board-headers { + column-gap: 4px; + display:flex; + flex-flow:row nowrap; + text-align: center; +} + +.ol-strip-board-headers > *, .ol-strip-board-strip > [data-point] { + padding: 4px; + text-overflow: ellipsis; + white-space: nowrap; + width:80px; +} + +.ol-strip-board-strip input[type="text"] { + appearance: none; + background-color: transparent; + border:1px solid #ffffff30; + border-radius: var( --border-radius-sm ); + color:white; + font-size:12px; + font-weight:normal; + outline:none; + padding: 4px 0; + text-align: center; + width:100%; +} + +.ol-strip-board-strip[data-time-warning="level-1"] [data-point="timeToGo"] { + border:1px solid #cc0000; +} + +.ol-strip-board-headers :nth-child(1) { + width:12px; +} + +.ol-strip-board-headers :nth-child(2), +.ol-strip-board-strip :nth-child(2), +[data-board-type="ground"] .ol-strip-board-headers :nth-child(3), +[data-board-type="ground"] .ol-strip-board-strip :nth-child(3) { + width:130px; +} + +[data-board-type="ground"] .ol-strip-board-strip :nth-child(5) { + text-align: center; +} + + + +.ol-strip-board-headers :last-child, +.ol-strip-board-strip :last-child { + width:20px; +} + + + + + + +[data-board-type="tower"] .ol-strip-board-strip > * { + text-align: center; +} + +[data-board-type="tower"] .ol-strip-board-strip a { + color:white; +} + +[data-board-type="tower"] .ol-strip-board-strip > :nth-child(2) { + text-align: left; +} + +[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) input, +[data-board-type="tower"] .ol-strip-board-strip :nth-child(5) input { + width:30px; +} + +[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) { + font-size:10px; +} + + +[data-altitude-assigned] [data-point="assignedAltitude"] input, +[data-speed-assigned] [data-point="assignedSpeed"] input { + background-color:#ffffffbb; + color: black; + font-weight: var( --font-weight-bolder ); +} + +[data-warning-altitude] [data-point="altitude"], +[data-warning-speed] [data-point="speed"] { + background:#cc0000; + border-radius: var( --border-radius-sm ); +} + + + +.ol-strip-board-strip > [data-point="name"] { + text-overflow: ellipsis; + overflow:hidden; +} + +.ol-strip-board-strip .ol-select-value { + opacity: .85; +} + + +.ol-strip-board-add-flight { + display:flex; + flex-flow: row nowrap; + position:relative; +} + + +.ol-strip-board-add-flight > * { + border:none; + outline: none; + padding:4px 8px; +} + +.add-flight-by-click img { + filter:invert(); + height: 12px; +} + +.ol-strip-board-add-flight input { + border-radius: var( --border-radius-sm ); +} + +.ol-strip-board-add-flight .ol-auto-suggest { + background:white; + border-radius: var(--border-radius-sm ); + color:black; + display:none; + flex-direction: column; + left:0; + margin:0; + position:absolute; + translate:0 -100%; + top:0; +} + +.ol-strip-board-add-flight .ol-auto-suggest[data-has-suggestions] { + display:flex; + row-gap: 4px; +} + +.ol-strip-board-add-flight .ol-auto-suggest[data-has-suggestions] a { + cursor: pointer; +} + + +[data-board-type="ground"] { + bottom:20px; +} + +[data-board-type="tower"] { + right:10px; + top:10px; +} + +[data-board-type="tower"] .ol-auto-suggest { + top:30px; + translate:0; +} \ No newline at end of file diff --git a/client/public/stylesheets/atc/unitdatatable.css b/client/public/stylesheets/atc/unitdatatable.css new file mode 100644 index 00000000..e16e1bdd --- /dev/null +++ b/client/public/stylesheets/atc/unitdatatable.css @@ -0,0 +1,27 @@ +#unit-list { + display:flex; + flex-direction: column; + font-size:13px; + height: 250px; + width:fit-content; +} + +#unit-list > div { + display:flex; + flex-direction: row; + flex-wrap: nowrap; +} + +#unit-list > div > div { + text-overflow: ellipsis; + white-space: nowrap; + width:100px; +} + +#unit-list > div:first-of-type { + text-align: center; +} + +#unit-list > div > div:nth-of-type( 4 ) { + text-align: center; +} \ No newline at end of file diff --git a/client/public/stylesheets/button.css b/client/public/stylesheets/button.css deleted file mode 100644 index 5540c792..00000000 --- a/client/public/stylesheets/button.css +++ /dev/null @@ -1,17 +0,0 @@ -.olympus-button { - width: 24px; - height: 24px; - background-color: transparent; - cursor: pointer; - display: flex; - align-items: center; -} - -.olympus-button img { - width: 24px; - height: 24px; -} - -.olympus-button:hover {} - -.olympus-button:active {} \ No newline at end of file diff --git a/client/public/stylesheets/connectionstatuspanel.css b/client/public/stylesheets/connectionstatuspanel.css deleted file mode 100644 index fce5a084..00000000 --- a/client/public/stylesheets/connectionstatuspanel.css +++ /dev/null @@ -1,33 +0,0 @@ -#connection-status-panel { - display: flex; - align-items: center; - padding-left: 15px; - padding-right: 4px; -} - -#status-string { - font-size: 14px; - color: white; -} - -.olympus-status-disconnected::after { - content: ""; - position: absolute; - right: 5px; - top: 5px; - border-radius: 50%; - width: 20px; - height: 20px; - background-color: red; -} - -.olympus-status-connected::after { - content: ""; - position: absolute; - right: 5px; - top: 5px; - border-radius: 50%; - width: 20px; - height: 20px; - background-color: 00FF00; -} \ No newline at end of file diff --git a/client/public/stylesheets/dropdown.css b/client/public/stylesheets/dropdown.css deleted file mode 100644 index a9faaa44..00000000 --- a/client/public/stylesheets/dropdown.css +++ /dev/null @@ -1,90 +0,0 @@ -.olympus-dropdown { - width: 100%; - min-width: 100px; - height: 30px; - position: relative; - background-color: #DDDD; - z-index: 1000; - border-radius: 15px; - font-family: Verdana, Geneva, Tahoma, sans-serif; - color: var(--background-color-dark); - padding-left: 5px; - align-items: center; - cursor: pointer; - font-size: 13px; - display: flex; - text-shadow: none; - box-shadow: 0px 2px 5px #000A; - padding-left: 15px; -} - -.olympus-dropdown::before { - content: ""; - position: absolute; - height: 30px; - width: 30px; - top: 0px; - right: 0px; - background-color: var(--background-color-dark); - z-index: 1000; - border-top-right-radius: 15px; - border-bottom-right-radius: 15px; -} - -.olympus-dropdown-open { - border-bottom-left-radius: 0px; -} - -.olympus-dropdown-open::after { - content: ""; - position: absolute; - top: 13px; - right: 11px; - height: 1px; - width: 1px; - border: solid white; - border-width: 0 3px 3px 0; - padding: 3px; - z-index: 1000; - transform: rotate(-135deg); - -webkit-transform: rotate(-135deg); -} - -.olympus-dropdown-closed::after { - content: ""; - position: absolute; - top: 9px; - right: 11px; - height: 1px; - width: 1px; - border: solid white; - border-width: 0 3px 3px 0; - padding: 3px; - z-index: 1000; - transform: rotate(45deg); - -webkit-transform: rotate(45deg); -} - -.olympus-dropdown-content { - position: fixed; - /*overflow: visible; - overflow-y: scroll;*/ - background-color: #DDDD; - z-index: 2000; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; -} - -.olympus-dropdown-element { - margin: 2px; - font-family: Verdana, Geneva, Tahoma, sans-serif; - color: var(--background-color-dark); - cursor: pointer; - opacity: 1; - font-size: 13px; - padding-left: 5px; -} - -.olympus-dropdown-element:hover { - background-color: var(--highlight-color); -} \ No newline at end of file diff --git a/client/public/stylesheets/elements.css b/client/public/stylesheets/elements.css deleted file mode 100644 index 1ac11c9f..00000000 --- a/client/public/stylesheets/elements.css +++ /dev/null @@ -1,14 +0,0 @@ -.olympus-element-1 { - background-color: #247be2; - height: 28; - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; - color: white; - font-size: 14px; -} - -.bottom-separator { - border-bottom: 1px solid gray; -} \ No newline at end of file diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css deleted file mode 100644 index 10c37c90..00000000 --- a/client/public/stylesheets/layout.css +++ /dev/null @@ -1,102 +0,0 @@ - -/* Page style */ -body { - padding: 0; - margin: 0; -} - -html, -body { - height: 100%; - width: 100%; -} - -#map-container { - height: 100%; - width: 100%; - min-width: 820px; -} - -#unit-info-panel { - position: fixed; - height: 160px; - width: 800px; - left: 10px; - bottom: 10px; - z-index: 1000; -} - -#map-source-dropdown { - position: absolute; - left: 10px; - top: 10px; - width: 200px; - color: black; -} - -#scenario-dropdown { - position: absolute; - left: 220px; - top: 10px; - width: 200px; -} - -#visibility-control-panel { - position: absolute; - left: 430px; - height: 30px; - width: 110; - top: 10px; - z-index: 1000; - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 10px; - padding-right: 10px; -} - -#unit-control-buttons { - position: fixed; - top: 10px; - height: fit-content; - width: fit-content; - right: 270px; - z-index: 1000; -} - -#unit-control-panel { - position: absolute; - top: 10px; - height: fit-content; - width: 250px; - right: 10px; - z-index: 1000; -} - -#connection-status-panel { - position: absolute; - height: 30px; - width: 160px; - bottom: 10px; - right: 10px; - z-index: 1000; -} - -#mouse-info-panel { - position: absolute; - height: fit-content; - width: 160px; - bottom: 50px; - right: 10px; - z-index: 1000; -} - -@media only screen and (max-width: 1000px) { - #unit-control-buttons { - top: 50px; - } - - #unit-control-panel { - top: 50px; - } -} \ No newline at end of file diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css new file mode 100644 index 00000000..7ae3c7b5 --- /dev/null +++ b/client/public/stylesheets/layout/layout.css @@ -0,0 +1,84 @@ +/* Page style */ +#map-container { + height: 100%; + min-width: 820px; + width: 100%; +} + +#primary-toolbar { + align-items: center; + display: flex; + left: 10px; + position: absolute; + top: 10px; + z-index: 9999; +} + +#app-icon>.ol-select-options { + width: fit-content; +} + +#toolbar-summary { + background-image: url("/images/icon-round.png"); + background-position: 20px 22px; + background-repeat: no-repeat; + background-size: 45px 45px; + display: flex; + flex-direction: column; + padding: 20px; + text-indent: 60px; +} + +#toolbar-summary { + white-space: nowrap; +} + +#connection-status-panel { + bottom: 20px; + font-size: 12px; + position: absolute; + right: 10px; + width: 180px; + z-index: 9999; +} + +#mouse-info-panel { + bottom: 60px; + display: flex; + flex-direction: column; + height: fit-content; + position: absolute; + right: 10px; + row-gap: 10px; + width: 180px; + z-index: 9999; +} + +#unit-control-panel { + height: fit-content; + left: 10px; + position: absolute; + top: 80px; + width: 320px; + z-index: 9999; +} + +#unit-info-panel { + bottom: 20px; + font-size: 12px; + left: 10px; + position: absolute; + width: fit-content; + z-index: 9999; + padding: 24px 30px; +} + +#info-popup { + position: absolute; + width: fit-content; + height: fit-content; + top: 100px; + left: 50%; + translate: -50% 0%; + z-index: 9999; +} \ No newline at end of file diff --git a/client/public/stylesheets/leaflet.css b/client/public/stylesheets/leaflet/leaflet.css similarity index 100% rename from client/public/stylesheets/leaflet.css rename to client/public/stylesheets/leaflet/leaflet.css diff --git a/client/public/stylesheets/markers/airbase.css b/client/public/stylesheets/markers/airbase.css new file mode 100644 index 00000000..a9640cf3 --- /dev/null +++ b/client/public/stylesheets/markers/airbase.css @@ -0,0 +1,20 @@ +.airbase-icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + position: relative; +} + +.airbase-icon[data-coalition="red"] svg * { + stroke: var(--unit-background-red); +} + +.airbase-icon[data-coalition="blue"] svg * { + stroke: var(--unit-background-blue); +} + +.airbase-icon[data-coalition="neutral"] svg * { + stroke: var(--unit-background-neutral); +} + diff --git a/client/public/stylesheets/markers/bullseye.css b/client/public/stylesheets/markers/bullseye.css new file mode 100644 index 00000000..81663b09 --- /dev/null +++ b/client/public/stylesheets/markers/bullseye.css @@ -0,0 +1,22 @@ +.bullseye-icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + position: relative; +} + +.bullseye-icon[data-coalition="red"] svg * { + stroke: var(--unit-background-red); + fill: var(--unit-background-red); +} + +.bullseye-icon[data-coalition="blue"] svg * { + stroke: var(--unit-background-blue); + fill: var(--unit-background-blue); +} + +.bullseye-icon[data-coalition="neutral"] svg * { + stroke: var(--unit-background-neutral); + fill: var(--unit-background-neutral); +} diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css new file mode 100644 index 00000000..65ec1d4b --- /dev/null +++ b/client/public/stylesheets/markers/units.css @@ -0,0 +1,314 @@ +/*** Unit marker elements ***/ +[data-object|="unit"] { + align-items: center; + cursor: pointer; + display: flex; + height: 100%; + justify-content: center; + position: relative; + width: 100%; +} + +.unit-vvi { + align-self: center; + background: var(--secondary-gunmetal-grey); + display: flex; + justify-self: center; + padding-bottom: calc((var(--unit-aircraft-width) / 2) + var(--unit-stroke-width)); + position: absolute; + transform-origin: bottom; + translate: 0 -50%; + width: var(--unit-aircraft-vvi-width); +} + +.unit-hotgroup { + align-content: center; + background-color: var(--background-steel); + border-radius: var(--border-radius-xs); + display: none; + height: 15px; + justify-content: center; + position: absolute; + transform: rotate(-45deg); + translate: 0 -200%; + width: 15px; +} + +.unit-hotgroup-id { + background-color: transparent; + color: white; + font-size: 9px; + font-weight: bolder; + transform: rotate(45deg); + translate: -1px 1px; +} + +.unit-icon { + height: var(--unit-height); + position: absolute; + transform-origin: center; + width: var(--unit-width); +} + +[data-is-selected] .unit-icon::before { + background-color: var(--unit-spotlight-fill); + border-radius: 50%; + content: ""; + height: 100%; + position: absolute; + width: 100%; + z-index: -1; +} + +/*** Basic colours ***/ +[data-coalition="blue"] .unit-icon svg>*:first-child { + fill: var(--unit-background-blue); +} + +[data-coalition="red"] .unit-icon svg>*:first-child { + fill: var(--unit-background-red); +} + +[data-coalition="neutral"] .unit-icon svg>*:first-child { + fill: var(--unit-background-neutral); +} + +[data-is-selected] .unit-icon svg>*:first-child { + fill: white; +} + +[data-is-highlighted] .unit-icon svg>*:first-child { + stroke: white; +} + +/*** Cursors ***/ +[data-is-dead], +[data-object|="unit-missile"], +[data-object|="unit-bomb"] { + cursor: default; +} + +/*** Labels ***/ +[data-object|="unit"] .unit-short-label { + color: var(--secondary-gunmetal-grey); + font-size: var(--unit-font-size); + font-weight: var(--unit-font-weight); + line-height: normal; + position: absolute; +} + +/*** Fuel indicator ***/ +[data-object|="unit"] .unit-fuel { + background: white; + border: var(--unit-aircraft-fuel-border-width) solid var(--secondary-dark-steel); + border-radius: var(--border-radius-sm); + display: none; + height: var(--unit-aircraft-fuel-height); + position: absolute; + translate: var(--unit-aircraft-fuel-x) var(--unit-aircraft-fuel-y); + width: var(--unit-aircraft-fuel-width); +} + +[data-object|="unit"] .unit-fuel-level { + background-color: var(--secondary-light-grey); + height: 100%; + width: 100%; +} + +/*** Ammo indicator ***/ +[data-object|="unit"] .unit-ammo { + column-gap: var(--unit-aircraft-ammo-spacing); + display: none; + height: fit-content; + position: absolute; + translate: var(--unit-aircraft-ammo-x) var(--unit-aircraft-ammo-y); + width: fit-content; +} + +[data-object|="unit"] .unit-ammo>* { + background-color: white; + border: var(--unit-aircraft-ammo-border-width) solid var(--secondary-dark-steel); + border-radius: 50%; + padding: var(--unit-aircraft-ammo-radius); +} + +/*** Unit summary ***/ +[data-object|="unit"] .unit-summary { + color: white; + column-gap: 6px; + display: flex; + flex-wrap: wrap; + font-size: 11px; + font-weight: bold; + justify-content: right; + line-height: 12px; + pointer-events: none; + position: absolute; + row-gap: 1px; + text-shadow: + -1px -1px 0 #000, + 1px -1px 0 #000, + -1px 1px 0 #000, + 1px 1px 0 #000; + translate: -60px 0; + width: fit-content; +} + +[data-hide-labels] [data-object|="unit"] .unit-summary { + display: none; +} + +[data-object|="unit"] .unit-summary>* { + padding: 1px; +} + +[data-object|="unit"] .unit-summary .unit-callsign { + color: white; + overflow: hidden; + text-align: right; + transform-origin: right; + white-space: nowrap; + width: 80px; +} + +[data-object|="unit"] .unit-summary .unit-callsign:hover { + direction: rtl; + overflow: visible; +} + +/*** Common ***/ +[data-object|="unit"]:hover .unit-ammo, +[data-object|="unit"]:hover .unit-fuel { + display: flex; +} + +@keyframes pulse { + 50% { + opacity: 0; + } +} + +[data-object|="unit"][data-has-low-fuel] .unit-fuel { + animation: pulse 1.5s linear infinite; +} + +[data-object|="unit"][data-is-in-hotgroup] .unit-hotgroup, +[data-object|="unit"][data-is-selected] .unit-ammo, +[data-object|="unit"][data-is-selected] .unit-fuel, +[data-object|="unit"][data-is-selected] .unit-selected-spotlight { + display: flex; +} + +[data-object|="unit"][data-has-fox-1] .unit-ammo>div:nth-child(1), +[data-object|="unit"][data-has-fox-2] .unit-ammo>div:nth-child(2), +[data-object|="unit"][data-has-fox-3] .unit-ammo>div:nth-child(3), +[data-object|="unit"][data-has-other-ammo] .unit-ammo>div:nth-child(4) { + background-color: var(--secondary-gunmetal-grey); +} + +[data-object|="unit"][data-coalition="blue"][data-is-selected] .unit-short-label { + color: var(--secondary-blue-text); +} + +[data-object|="unit"][data-coalition="blue"] .unit-fuel-level, +[data-object|="unit"][data-coalition="blue"][data-has-fox-1] .unit-ammo>div:nth-child(1), +[data-object|="unit"][data-coalition="blue"][data-has-fox-2] .unit-ammo>div:nth-child(2), +[data-object|="unit"][data-coalition="blue"][data-has-fox-3] .unit-ammo>div:nth-child(3), +[data-object|="unit"][data-coalition="blue"][data-has-other-ammo] .unit-ammo>div:nth-child(4) { + background-color: var(--primary-blue); +} + +[data-object|="unit"][data-coalition="blue"] .unit-vvi { + background-color: var(--secondary-blue-outline); +} + +[data-object|="unit"][data-coalition="red"][data-is-selected] .unit-short-label { + color: var(--secondary-red-text); +} + +[data-object|="unit"][data-coalition="red"] .unit-fuel-level, +[data-object|="unit"][data-coalition="red"][data-has-fox-1] .unit-ammo>div:nth-child(1), +[data-object|="unit"][data-coalition="red"][data-has-fox-2] .unit-ammo>div:nth-child(2), +[data-object|="unit"][data-coalition="red"][data-has-fox-3] .unit-ammo>div:nth-child(3), +[data-object|="unit"][data-coalition="red"][data-has-other-ammo] .unit-ammo>div:nth-child(4) { + background-color: var(--primary-red); +} + +[data-object|="unit"][data-coalition="blue"] .unit-vvi { + background-color: var(--secondary-red-outline); +} + +/*** Unit state ***/ +[data-object|="unit"] .unit-state { + background-repeat: no-repeat; + height: 20px; + position: absolute; + width: 20px; + left: 0px; + top: 0px; +} + +[data-object|="unit"][data-state="rtb"] .unit-state { + background-image: url("/resources/theme/images/states/rtb.svg"); +} + +[data-object|="unit"][data-state="land"] .unit-state { + background-image: url("/resources/theme/images/states/rtb.svg"); +} + +[data-object|="unit"][data-state="idle"] .unit-state { + background-image: url("/resources/theme/images/states/idle.svg"); +} + +[data-object*="groundunit"][data-state="idle"] .unit-state { + background-image: url(""); /* To avoid clutter, dont show the idle state for non flying units */ +} + +[data-object|="unit"][data-state="attack"] .unit-state, +[data-object|="unit"][data-state="bombing point"] .unit-state, +[data-object|="unit"][data-state="carpet bombing"] .unit-state, +[data-object|="unit"][data-state="firing at area"] .unit-state { + background-image: url("/resources/theme/images/states/attack.svg"); +} + +[data-object|="unit"][data-state="follow"] .unit-state { + background-image: url("/resources/theme/images/states/follow.svg"); +} + +[data-object|="unit"][data-state="refuel"] .unit-state { + background-image: url("/resources/theme/images/states/refuel.svg"); +} + +[data-object|="unit"][data-state="human"] .unit-state { + background-image: url("/resources/theme/images/states/human.svg"); +} + +[data-object|="unit"][data-state="dcs"] .unit-state { + background-image: url("/resources/theme/images/states/dcs.svg"); +} + +[data-object|="unit"][data-state="no-task"] .unit-state { + background-image: url("/resources/theme/images/states/no-task.svg"); +} + +/*** Dead unit ***/ +[data-object|="unit-aircraft"][data-is-dead] .unit-selected-spotlight, +[data-object|="unit-aircraft"][data-is-dead] .unit-short-label, +[data-object|="unit-aircraft"][data-is-dead] .unit-vvi, +[data-object|="unit-aircraft"][data-is-dead] .unit-hotgroup, +[data-object|="unit-aircraft"][data-is-dead] .unit-hotgroup-id, +[data-object|="unit-aircraft"][data-is-dead] .unit-state, +[data-object|="unit-aircraft"][data-is-dead] .unit-fuel, +[data-object|="unit-aircraft"][data-is-dead] .unit-ammo, +[data-object|="unit-aircraft"][data-is-dead]:hover .unit-fuel, +[data-object|="unit-aircraft"][data-is-dead]:hover .unit-ammo { + display: none; +} + +[data-object|="unit-aircraft"][data-is-dead] .unit-summary>* { + display: none; +} + +[data-object|="unit-aircraft"][data-is-dead] .unit-summary .unit-callsign { + display: block; +} \ No newline at end of file diff --git a/client/public/stylesheets/mouseinfopanel.css b/client/public/stylesheets/mouseinfopanel.css deleted file mode 100644 index 3bf331ad..00000000 --- a/client/public/stylesheets/mouseinfopanel.css +++ /dev/null @@ -1,31 +0,0 @@ -#mouse-info-panel { - display: flex; - flex-direction: column; - padding: 10px; - row-gap: 5px; -} - -#mouse-info-panel .rectangular-container{ - width: 100%; - font-weight: 600; - font-size: 12px; - display: flex; - align-items: center; - justify-content: space-between; - padding-left: 10px; - padding-right: 10px; - border-radius: 10px; - background-color: #FFF3; -} - -#mouse-info-panel img { - height: 24px; -} - -#measure-position-container{ - display: none; -} - -#unit-position-container{ - display: none; -} \ No newline at end of file diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css new file mode 100644 index 00000000..ef86e765 --- /dev/null +++ b/client/public/stylesheets/olympus.css @@ -0,0 +1,1137 @@ +@import url("layout/layout.css"); +@import url("atc/atc.css"); +@import url("atc/unitdatatable.css"); +@import url("aic/aic.css"); +@import url("panels/connectionstatus.css"); +@import url("panels/mouseinfo.css"); +@import url("panels/unitcontrol.css"); +@import url("panels/unitinfo.css"); +@import url("other/contextmenus.css"); +@import url("other/popup.css"); +@import url("markers/airbase.css"); +@import url("markers/bullseye.css"); +@import url("markers/units.css"); + +* { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +html * { + font-family: 'Open Sans', sans-serif !important; +} + +body { + display: grid; + margin: 0; + padding: 0; +} + +html, +body { + height: 100%; + width: 100%; +} + +.hidden-cursor { + cursor: none !important; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +button { + background-color: var(--background-steel); + border: 1px solid var(--background-steel); + border-radius: var(--border-radius-sm); + color: whitesmoke; + cursor: pointer; + font-weight: var(--font-weight-bolder); + padding: 6px; + column-gap: 5px; +} + +button:hover { + background-color: var(--background-hover); +} + +button[disabled="disabled"] { + color: var(--highlight-color); + cursor: not-allowed; +} + +button>svg:first-child, +button>img:first-child { + position: relative; + aspect-ratio: initial; + height: 100%; + pointer-events: none; +} + +form { + margin: 0; + padding: 0; +} + +form>div { + margin: 20px 0; +} + +.pill { + background-color: var(--background-dark-steel); + border-radius: var(--border-radius-sm); + padding: 4px 8px; + width: fit-content; +} + +.ol-scrollable { + overflow-y: scroll; + scrollbar-color: white transparent; + scrollbar-width: thin; +} + +.ol-scrollable::-webkit-scrollbar { + width: var(--border-radius-md); +} + +.ol-scrollable::-webkit-scrollbar-track { + background-color: transparent; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + margin-top: 0px; +} + +.ol-select .ol-scrollable { + scrollbar-color: white var(--background-grey); +} + +.ol-select .ol-scrollable::-webkit-scrollbar-track { + background-color: var(--background-grey); +} + +.ol-scrollable::-webkit-scrollbar-thumb { + background-color: white; + border-radius: 100px; + margin-top: 10px; + opacity: 0.8; +} + +.ol-panel { + background-color: var(--background-steel); + border-radius: 15px; + box-shadow: 0px 2px 5px #000A; + color: white; + font-size: 12px; + height: fit-content; + padding: 10px; + width: fit-content; +} + +.ol-panel hr { + background-color: var(--secondary-transparent-white); + border: none; + height: 1px; + margin: 10px 0; + width: 100%; +} + +.ol-panel-padding-lg { + padding: 24px 30px; +} + +.ol-select-container { + width: 100%; +} + +.ol-ellipsed { + display: inline-block; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + width: calc(100%); +} + +.ol-select { + color: var(--nav-text); + position: relative; +} + +.ol-select>.ol-select-value { + align-content: center; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + cursor: pointer; + display: flex; + justify-content: left; + min-width: 0; + text-align: center; + white-space: nowrap; + width: 100%; +} + +.ol-select:not(.ol-select-image)>.ol-select-value { + align-items: center; + background-color: var(--background-grey); + border-radius: var(--border-radius-sm); + height: 40px; + overflow: hidden; + padding-left: 20px; + padding-right: 30px; + text-overflow: ellipsis; + + width: calc(100%); +} + +.ol-select.narrow:not(.ol-select-image)>.ol-select-value { + opacity: .9; + padding: 4px 30px 4px 15px; +} + +.ol-select:not(.ol-select-image)>.ol-select-value svg { + margin-right: 10px; +} + +.ol-select:not(.ol-select-image)>.ol-select-value:after { + content: url("/resources/theme/images/icons/chevron-down.svg"); + position: absolute; + right: 10px; +} + +.ol-select>.ol-select-options { + border-radius: var(--border-radius-md); + max-height: 0; + overflow: hidden; + position: absolute; + z-index: 9999; +} + +.ol-select-options.scrollbar-visible { + border-bottom-right-radius: 0px !important; + border-top-right-radius: 0px !important; +} + +.ol-select.ol-select-image>.ol-select-options { + position: absolute; +} + +.ol-select.is-open>.ol-select-options { + max-height: 382px; + min-width: 100%; + overflow: visible; + overflow-y: auto; + translate: 0px 5px; + z-index: 9999; +} + +.ol-select.is-open[data-position="top"]>.ol-select-options { + top: 0; + translate: 0 -100%; +} + +.ol-select>.ol-select-options>div { + background-color: var(--background-grey); + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25); + display: flex; + justify-content: left; + padding: 2px 15px; + width: 100%; +} + +.ol-select>.ol-select-options>div:first-of-type { + padding-top: 12px; +} + +.ol-select>.ol-select-options>div:last-of-type { + padding-bottom: 12px; +} + +.ol-select>.ol-select-options div hr { + background-color: white; + height: 1px; + width: 100%; +} + +.ol-select>.ol-select-options>div a, +.ol-select>.ol-select-options>div button { + background-color: transparent; + border: none; + border-radius: 0; + border-radius: var(--border-radius-sm); + color: white; + display: block; + font-size: 13px; + font-weight: normal; + height: 32px; + padding: 6px 2px; + padding: 5px; + text-align: left; + white-space: nowrap; + width: 100%; +} + +.ol-select>.ol-select-options>div a:hover, +.ol-select>.ol-select-options>div button:hover { + background-color: #FFF3; + text-decoration: none; +} + +.ol-panel-list { + border-radius: var(--border-radius-sm); + display: flex; + flex-direction: column; + height: fit-content; + row-gap: 5px; + text-align: center; + width: fit-content; +} + +.ol-panel-list .list-item { + border-radius: var(--border-radius-md); + display: flex; + justify-content: space-between; + padding: 6px 10px; +} + +.ol-panel-list.sortable>.sortable-item { + align-items: center; + column-gap: 5px; + display: flex; + flex-direction: row; +} + +.ol-panel-list.sortable>.sortable-item>.handle { + cursor: grab; + filter: invert(100); +} + +.ol-panel-list.sortable>.sortable-item>.handle img { + max-width: 16px; +} + +.ol-panel-board { + display: flex; + flex-direction: row; + justify-content: space-evenly; +} + +.ol-panel-board>.panel-section { + border-right: 1px solid #555; + padding: 0 30px; +} + +.ol-panel-board>.panel-section:first-child { + padding-left: 0px; +} + +.ol-panel-board>.panel-section:last-child { + padding-right: 0px; +} + +.ol-panel-board>.panel-section:last-of-type { + border-right-width: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0px; +} + +h1 { + font-size: 36px; + font-weight: 800; +} + +h2 { + font-size: 24px; + font-weight: bold; +} + +h3 { + font-size: 18px; + font-weight: bold; +} + +h4 { + font-size: 14px; + font-weight: normal; +} + +button.ol-button-warning { + border: 1px solid var(--primary-red); + color: var(--primary-red); + font-weight: bold; +} + +button.ol-button-warning>svg:first-child { + stroke: var(--primary-red); + fill: var(--primary-red); +} + +nav.ol-panel { + column-gap: 20px; + display: flex; + flex-direction: row; + height: 58px; +} + +nav.ol-panel> :last-child { + margin-right: 5px; +} + +.ol-panel .ol-group { + align-items: center; + border-radius: var(--border-radius-sm); + column-gap: 10px; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + row-gap: 4px; +} + +.ol-group-header { + text-align: center; + width: 100%; +} + +.ol-panel .ol-group.wrap { + flex-wrap: wrap; +} + +.ol-panel .ol-group-button-toggle { + align-items: center; + column-gap: 15px; + display: flex; + flex-wrap: nowrap; + white-space: nowrap; + width: fit-content; +} + +.ol-panel .ol-group-button-toggle button { + background-position: 5px 50%; + background-repeat: no-repeat; + border: 0; + display: flex; + justify-items: left; + text-indent: 5px; +} + +.ol-panel .ol-group-button-toggle button::before { + background-image: url("/resources/theme/images/icons/square-check-solid.svg"); + background-repeat: no-repeat; + content: ""; + filter: invert(100%); + -webkit-filter: invert(100%); + height: 16px; + width: 16px; +} + +.ol-panel .ol-group-button-toggle button.off::before { + background-image: url("/resources/theme/images/icons/square-regular.svg"); +} + +.highlight-primary { + background-color: var(--secondary-light-grey); +} + +.highlight-coalition, +.highlight-neutral { + background-color: var(--primary-neutral); + color: var(--secondary-gunmetal-grey) +} + +.highlight-coalition[data-coalition="blue"], +.highlight-bluefor { + background-color: var(--primary-blue); + color: white; +} + +.highlight-coalition[data-coalition="red"], +.highlight-redfor { + background-color: var(--primary-red); + color: white; +} + +.accent-green { + color: var(--accent-green); + font-weight: var(--font-weight-bolder); +} + +.accent-light-blue { + color: var(--accent-light-blue); + font-weight: var(--font-weight-bolder); +} + +.accent-bluefor { + color: var(--primary-blue); + font-weight: var(--font-weight-bolder); +} + +.accent-redfor { + color: var(--primary-red); + font-weight: var(--font-weight-bolder); +} + +.accent-neutral { + color: var(--primary-neutral); + font-weight: var(--font-weight-bolder); +} + +.hide { + display: none !important; +} + +.icon-small { + filter: invert(100%); + padding: 2px; + width: 20px; +} + +.ol-data-grid { + display: flex; + flex-direction: column; +} + +.ol-slider-container { + width: 100%; +} + +.ol-slider-container:not(:first-of-type) { + margin-top: 10px; + width: 100%; +} + +.ol-slider { + -webkit-appearance: none; + appearance: none; + background: #d3d3d3; + height: 2px; + margin-bottom: 10px; + margin-top: 15px; + opacity: 0.7; + outline: none; + -webkit-transition: .2s; + transition: opacity .2s; + width: 100%; +} + +.ol-slider:hover { + opacity: 1; +} + +.ol-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + background: var(--background-grey); + border-radius: 999px; + cursor: pointer; + height: 25px; + width: 25px; +} + +.active .ol-slider::-webkit-slider-thumb { + background: radial-gradient(circle at center, var(--accent-light-blue), var(--accent-light-blue) 40%, color-mix(in srgb, var(--accent-light-blue), transparent 66%) 50%); +} + +.ol-slider::-moz-range-thumb { + -moz-appearance: none; + border: 0px solid transparent; + background: var(--background-grey); + border-radius: 999px; + cursor: pointer; + height: 25px; + width: 25px; +} + +.active .ol-slider::-moz-range-thumb { + -moz-appearance: none; + background: radial-gradient(circle at center, var(--accent-light-blue), var(--accent-light-blue) 40%, color-mix(in srgb, var(--accent-light-blue), transparent 66%) 50%); +} + +.ol-slider-min-max { + display: flex; + justify-content: space-between; + color: var(--secondary-light-grey); +} + +.ol-slider-min-max::before { + content: attr(data-min-value); +} + +.ol-slider-min-max::after { + content: attr(data-max-value); +} + +.main-logo { + height: 40px; + width: 40px; +} + +.ol-measure-box { + background-color: var(--background-steel); + border-radius: 999px; + color: var(--background-offwhite); + font-size: 12px; + font-weight: bolder; + height: fit-content; + padding-bottom: 0.2em; + padding-left: 0.5em; + padding-right: 0.5em; + padding-top: 0.2em; + position: absolute; + text-align: center; + width: fit-content; + z-index: 2000; + pointer-events: none; +} + +.ol-sortable .handle { + background-image: url("/resources/theme/images/icons/grip-lines-solid.svg"); + cursor: ns-resize; + filter: invert(); + height: 12px; + width: 12px; +} + +#unit-selection { + display: flex; + flex-direction: column; +} + +#unit-selection #unit-identification { + align-items: center; + display: flex; + margin-bottom: 11px; +} + +#unit-selection #unit-identification [data-object|="unit"] { + height: 28px; + margin-right: 6px; + width: 28px; +} + +#unit-selection #unit-identification [data-object|="unit"] .unit-icon { + background-size: 28px 28px; + height: 28px; + width: 28px; +} + +#unit-visibility-control { + align-items: center; +} + +#unit-visibility-control button { + border: none; + height: 32px; + padding: 0px; + width: 32px; +} + +#unit-visibility-control button svg { + height: 16px; + pointer-events: none; + width: 16px; +} + +#unit-visibility-control button { + background-color: white; + border: 1px solid transparent; +} + +#unit-visibility-control button.off { + background-color: transparent; + border: 1px solid white; +} + +#unit-visibility-control button.off svg * { + fill: white !important; + stroke: white !important; +} + +#unit-visibility-control button svg * { + fill: var(--background-steel) !important; + stroke: var(--background-steel) !important; +} + +#atc-navbar-control { + align-items: center; + display: flex; + flex-direction: column; +} + +#atc-navbar-control button { + background: #ffffff20; + border-radius: var(--border-radius-sm); + padding: 4px; +} + +#roe-buttons-container button, +#reaction-to-threat-buttons-container button, +#emissions-countermeasures-buttons-container button { + align-items: center; + background-color: transparent; + border: 1px solid var(--accent-light-blue); + display: flex; + height: 30px; + justify-content: center; + width: 30px; +} + +#unit-control-panel .ol-option-button button.selected { + background-color: white; + border-color: white; +} + +#unit-control-panel .ol-option-button button.selected svg * { + fill: var(--background-steel); +} + +/****************************************************************************************/ +#splash-screen { + background-image: url("/resources/theme/images/splash/1.png"); + background-position: 100% 50%; + background-size: 60%; + border-radius: var(--border-radius-lg); + overflow: hidden; + width: 1200px; + z-index: 99999; +} + +#splash-content { + background-color: var(--background-steel); + display: flex; + flex-direction: column; + padding: 30px; + position: relative; + row-gap: 10px; + width: 50%; +} + +#splash-content::after { + background-color: var(--background-steel); + content: ""; + display: block; + height: 800px; + position: absolute; + right: 0; + top: 0; + transform: rotate(-23deg); + transform-origin: top right; + width: 200px; + z-index: -1; +} + +#splash-content #app-summary { + background-image: url("/images/olympus-500x500.png"); + background-position: 0 50%; + background-repeat: no-repeat; + background-size: 75px 75px; + content: ""; + display: flex; + flex-direction: column; + justify-content: space-between; + min-height: 75px; + text-indent: 85px; +} + +#splash-content #app-summary>* { + height: fit-content; + line-height: 25px; + padding: 2px; + white-space: nowrap; + width: fit-content; +} + +#splash-content .app-version { + font-size: 11px; +} + +#splash-content #legal-stuff h5 { + text-transform: uppercase; +} + +#splash-content #legal-stuff p { + color: #FFF7; + font-size: 10px; + width: 120%; +} + +#splash-content.ol-dialog-content { + margin: 0px; +} + +.feature-splashScreen #splash-screen { + display: flex; +} + +#gray-out { + background-color: #000A; + height: 100%; + left: 0px; + position: fixed; + top: 0px; + width: 100%; + z-index: 9999; +} + +#authentication-form { + align-items: end; + column-gap: 10px; + display: flex; + flex-direction: row; + margin: 10px 0px; +} + +#authentication-form>div { + align-items: start; + display: flex; + flex-direction: column; + row-gap: 4px; +} + +#authentication-form>div>input { + border: 0px solid transparent; + border-radius: var(--border-radius-sm); + height: 35px; + width: 200px; +} + +#splash-content a { + color: #FFFB; + font-weight: bold; +} + +#connection-status { + margin-bottom: 5px; +} + +#connection-status[data-status="connecting"]::before { + animation: blinker 1s linear infinite; + content: "Connecting..."; +} + +#connection-status[data-status="failed"]::before { + color: var(--primary-red); + content: "Incorrect username/password!"; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + +#hotgroup-panel { + bottom: 40px; + column-gap: 10px; + display: flex; + left: 50%; + position: absolute; + translate: -50%; + z-index: 9999; +} + +#hotgroup-panel>div { + align-items: center; + background-color: var(--background-steel); + border: 0px solid transparent; + border-radius: var(--border-radius-sm); + color: white; + display: flex; + font-weight: bold; + height: 50px; + justify-content: center; + width: 50px; +} + +#hotgroup-panel>div:hover { + border: 2px solid white; + cursor: pointer; +} + +.hotgroup-selector>.unit-hotgroup { + display: flex; + translate: 0% -300%; +} + +.ol-destination-preview-icon { + background-image: url("/resources/theme/images/markers/move.svg"); + height: 52px; + pointer-events: none; + width: 52px; +} + +.ol-target-icon { + background-image: url("/resources/theme/images/markers/target.svg"); + height: 52px; + pointer-events: none; + width: 52px; + z-index: 9999; +} + +dl.ol-data-grid { + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: 0; + row-gap: 4px; +} + +dl.ol-data-grid dd { + width: fit-content; +} + +dl.ol-data-grid dt.icon { + text-indent: 10px; +} + +dl.ol-data-grid dt.icon::before { + content: url("/resources/theme/images/icons/speed.svg"); + display: inline-block; + filter: invert(100%); + translate: -20px 2px; + width: 20px; +} + +dl.ol-data-grid dt.icon-speed::before { + content: url("/images/icons/speed.svg"); +} + +dl.ol-data-grid dt.icon-altitude::before { + content: url("/images/icons/altitude.svg"); +} + +dl.ol-data-grid dd { + display: flex; + justify-content: flex-end; + margin-left: auto; +} + +.ol-button-box { + column-gap: 6px; + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: 5px 0; + row-gap: 5px; +} + +.ol-button-box button { + background-repeat: no-repeat; + ; + border: 1px solid var(--accent-light-blue); + color: var(--accent-light-blue); +} + +.ol-dialog { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--background-slate-blue); + color: white; + position: absolute; + z-index: 9999; +} + +.ol-panel.ol-dialog { + padding: 24px 30px; +} + +.ol-dialog-close { + cursor: pointer; + font-size: 16px; + font-weight: var(--font-weight-bolder); + position: absolute; + right: 20px; + top: 10px; +} + +.ol-dialog-close::before { + content: "\d7"; +} + +.ol-dialog-header { + border-bottom: 1px solid var(--background-grey); + padding-bottom: 10px; +} + +.ol-dialog-content { + margin: 4px 0; +} + +.ol-dialog-footer { + border-top: 1px solid var(--background-grey); + display: flex; + padding-top: 15px; + row-gap: 10px; +} + +.ol-dialog.scrollable .ol-dialog-content { + overflow-y: auto; +} + +.ol-checkbox label { + align-items: center; + cursor: pointer; + display: flex; + flex-wrap: nowrap; + white-space: nowrap; +} + +.ol-checkbox input[type="checkbox"] { + appearance: none; + background-color: transparent; + border: none; + margin: 0; +} + +.ol-checkbox input[type="checkbox"]::before { + align-self: center; + background-image: url("/resources/theme/images/icons/square-regular.svg"); + background-repeat: no-repeat; + content: ""; + display: flex; + filter: invert(100%); + height: 16px; + margin-right: 10px; + width: 16px; +} + +.ol-checkbox input[type="checkbox"]:checked::before { + background-image: url("/resources/theme/images/icons/square-check-solid.svg"); +} + +.ol-text-input input { + background-color: var(--background-grey); + border: 1px solid var(--background-grey); + border-radius: 5px; + border-radius: var(--border-radius-sm); + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + color: var(--background-offwhite); + height: 32px; + text-align: center; +} + +input[type=number] { + -moz-appearance: textfield; + appearance: textfield; + margin: 0; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +[class|="ol-button"] { + align-items: center; + background-repeat: no-repeat; + display: flex; + font-weight: normal; + padding: 8px 10px; + white-space: nowrap; +} + +[class|="ol-button"]::before { + margin-right: 8px; +} + +.ol-button-close { + background: transparent; + border: 1px solid white; +} + +.ol-button-close::before { + content: "\d7"; +} + +.ol-button-apply { + background: transparent; + border: 1px solid white; +} + +.ol-button-apply::before { + content: "\2713"; +} + +.ol-button-settings { + background-color: var(--background-slate-blue); +} + +.ol-button-settings::before { + background-image: url("/resources/theme/images/icons/gears-solid.svg"); + background-position: 0 50%; + background-size: 24px 24px; + content: ""; + display: flex; + filter: invert(100%); + height: 24px; + width: 24px; +} + +.ol-switch { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.ol-switch-input { + display: none; +} + +.ol-switch-fill { + border-radius: 999px; + position: relative; + transition: background-color 0.2s; + height: var(--height); + width: var(--width); +} + +.ol-switch-fill::after { + aspect-ratio : 1 / 1; + background-clip: content-box; + background-color: #ffffff; + border-radius: 999px; + box-sizing: border-box; + content: ""; + height: 100%; + padding: 3px; + position: absolute; + transition: transform 0.2s; + top: 0px; +} + +.ol-switch-fill::before { + align-items: center; + box-sizing: border-box; + color: white; + display: flex; + font-size: 11px; + height: 100%; + padding: 0px 7px; + position: absolute; + transition: transform 0.2s; +} + +.ol-switch[data-value="false"]>.ol-switch-fill::before { + transform: translateX(calc(var(--width) - 100%)); +} + +.ol-switch[data-value="true"]>.ol-switch-fill::after { + transform: translateX(calc(var(--width) - var(--height))); +} + +.ol-switch[data-value="undefined"]>.ol-switch-fill::after { + transform: translateX(calc((var(--width) - var(--height)) * 0.5)); +} \ No newline at end of file diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css new file mode 100644 index 00000000..9f50a4d4 --- /dev/null +++ b/client/public/stylesheets/other/contextmenus.css @@ -0,0 +1,409 @@ +#map-contextmenu { + display: flex; + flex-direction: column; + height: fit-content; + position: absolute; + row-gap: 5px; + width: 280px; + z-index: 9999; +} + +#aircraft-spawn-menu { + height: fit-content; +} + +#ground-unit-spawn-menu { + height: fit-content; +} + +#active-coalition-label { + border-radius: 999px; + color: var(--nav-text); + font-size: 12px; + font-weight: 600; + height: fit-content; + padding: 3px 10px; + padding-bottom: 3px; + position: absolute; + top: -28px; + width: fit-content; +} + +#coalition-switch { + margin-right: 10px; + height: 25px; + width: 50px; +} + +#coalition-switch[data-value="false"]>.ol-switch-fill { + background-color: var(--primary-blue); +} + +#coalition-switch[data-value="true"]>.ol-switch-fill { + background-color: var(--primary-red); +} + +#coalition-switch[data-value="undefined"]>.ol-switch-fill { + background-color: var(--primary-neutral); +} + +#map-contextmenu>div:nth-child(2) { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + padding-right: 0px; +} + +#map-contextmenu>ul { + max-height: 200px; + overflow-x: hidden; + overflow-y: auto; +} + +#map-contextmenu .ol-panel { + border-radius: var(--border-radius-sm); + width: 100%; +} + +#map-contextmenu ul { + margin: 0px; +} + +#map-contextmenu>div:nth-child(n+3) { + align-items: center; + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 5px; +} + +#map-contextmenu .ol-select-container { + align-self: stretch; + flex: 0 0 auto; + width: 100%; +} + +#aircraft-spawn-menu .ol-select.is-open .ol-select-options { + max-height: 300px; +} + +#aircraft-spawn-menu>button, +#ground-unit-spawn-menu>button { + text-align: center; + width: 100%; +} + +#aircraft-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/aircraft.svg"); + background-size: 48px; +} + +#ground-unit-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/ground.svg"); + background-size: 48px; +} + +#smoke-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/smoke.svg"); + background-size: 48px; +} + +#explosion-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/explosion.svg"); + background-size: 48px; +} + +.unit-spawn-button { + border: none; + border-radius: 0px; + height: 48px; + margin-bottom: -10px; + margin-top: -10px; + width: 48px; +} + +.unit-spawn-button:last-of-type { + border-bottom-right-radius: var(--border-radius-sm); + border-top-right-radius: var(--border-radius-sm); +} + +[data-active-coalition="blue"].unit-spawn-button:hover, +[data-active-coalition="blue"].unit-spawn-button.is-open, +[data-active-coalition="blue"]#active-coalition-label, +[data-active-coalition="blue"].deploy-unit-button, +[data-active-coalition="blue"]#spawn-airbase-aircraft-button { + background-color: var(--primary-blue) +} + +[data-active-coalition="red"].unit-spawn-button:hover, +[data-active-coalition="red"].unit-spawn-button.is-open, +[data-active-coalition="red"]#active-coalition-label, +[data-active-coalition="red"].deploy-unit-button, +[data-active-coalition="red"]#spawn-airbase-aircraft-button { + background-color: var(--primary-red) +} + +[data-active-coalition="neutral"].unit-spawn-button:hover, +[data-active-coalition="neutral"].unit-spawn-button.is-open, +[data-active-coalition="neutral"]#active-coalition-label, +[data-active-coalition="neutral"].deploy-unit-button, +[data-active-coalition="neutral"]#spawn-airbase-aircraft-button { + background-color: var(--primary-neutral) +} + +[data-active-coalition="blue"].deploy-unit-button:disabled { + background-color: transparent; + border: 1px solid var(--primary-blue); + cursor: default; +} + +[data-active-coalition="red"].deploy-unit-button:disabled { + background-color: transparent; + border: 1px solid var(--primary-red); + cursor: default; +} + +[data-active-coalition="neutral"].deploy-unit-button:disabled { + background-color: transparent; + border: 1px solid var(--primary-neutral); + cursor: default; +} + +[data-active-coalition="blue"]#active-coalition-label::after { + content: "Create blue unit"; +} + +[data-active-coalition="red"]#active-coalition-label::after { + content: "Create red unit"; +} + +[data-active-coalition="neutral"]#active-coalition-label::after { + content: "Create neutral unit"; +} + +#loadout-preview { + align-content: space-between; + align-items: center; + column-gap: 20px; + display: flex; + flex-direction: row; + width: 100%; +} + +#loadout-list { + align-content: center; + display: flex; + flex-direction: column; + height: 100%; +} + +#unit-image { + filter: invert(100%); + height: 100px; + margin-bottom: 10px; + margin-top: 10px; + width: 100px; +} + +#smoke-spawn-menu { + align-items: center; + display: flex; + flex-direction: column; + text-align: center; +} + +#explosion-menu>button, +#smoke-spawn-menu>button { + align-items: center; + column-gap: 10px; + display: flex; + flex-wrap: wrap; + text-align: left; + width: 100%; +} + +#smoke-spawn-menu>button::before { + border-radius: 999px; + content: ""; + display: block; + height: 10px; + width: 10px; +} + +[data-smoke-color="red"]::before { + background-color: red; +} + +[data-smoke-color="white"]::before { + background-color: white; +} + +[data-smoke-color="blue"]::before { + background-color: blue; +} + +[data-smoke-color="green"]::before { + background-color: green; +} + +[data-smoke-color="orange"]::before { + background-color: orange; +} + +#aircraft-spawn-menu .ol-slider-value { + color: var(--accent-light-blue); + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +#aircraft-spawn-altitude-slider { + padding: 0px 10px; +} + +/* Unit context menu */ +#unit-contextmenu { + display: flex; + flex-direction: column; + height: fit-content; + padding: 15px; + position: absolute; + row-gap: 5px; + width: fit-content; + z-index: 9999; +} + +#unit-contextmenu button { + border: 1px solid var(--background-offwhite); + border-radius: var(--border-radius-sm); + font-weight: normal; + padding: 12px; +} + +#unit-contextmenu div { + align-content: center; + display: flex; + flex-direction: row; +} + +#unit-contextmenu div:before { + display: inline-block; + filter: invert(100%); + height: 16px; + margin-right: 15px; + width: 16px; +} + +#center-map::before { + content: url("/resources/theme/images/icons/arrows-to-eye-solid.svg"); +} + +#refuel::before { + content: url("/resources/theme/images/icons/fuel.svg"); +} + +#attack::before { + content: url("/resources/theme/images/icons/sword.svg"); +} + +#bomb::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + +#carpet-bomb::before { + content: url("/resources/theme/images/icons/explosion-solid.svg"); +} + +#fire-at-area::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + +#follow::before { + content: url("/resources/theme/images/icons/follow.svg"); +} + +#trail::before { + content: url("/resources/theme/images/icons/trail.svg"); +} + +#echelon-lh::before { + content: url("/resources/theme/images/icons/echelon-lh.svg"); +} + +#echelon-rh::before { + content: url("/resources/theme/images/icons/echelon-rh.svg"); +} + +#line-abreast-rh::before, +#line-abreast-lh::before { + content: url("/resources/theme/images/icons/line-abreast.svg"); +} + +#front::before { + content: url("/resources/theme/images/icons/front.svg"); +} + +#diamond::before { + content: url("/resources/theme/images/icons/diamond.svg"); +} + +#custom::before { + content: url("/resources/theme/images/icons/custom.svg"); +} + +#custom-formation-dialog { + width: 250px; +} + +#custom-formation-dialog>.ol-dialog-content { + align-items: center; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + margin-bottom: 10px; + margin-top: 10px; + row-gap: 10px; +} + +#custom-formation-dialog>.ol-dialog-content>.ol-group { + justify-content: space-between; + width: 100%; +} + +#reference-system { + content: url("/images/reference-system.svg"); + display: inline-block; + filter: invert(100%); + position: absolute; + transform: translate(-50%, -50%); + width: 50px; +} + +.formation-position-clock { + height: 100px; + margin: 15px; + position: relative; + width: 100px; +} + +.formation-position-clock>.clock-hand { + align-items: center; + display: flex; + height: 20px; + justify-content: center; + position: absolute; + transform: translate(-50%, -50%); + width: 20px; +} + +/* Airbase context menu */ +#airbase-contextmenu { + display: flex; + flex-direction: column; + height: fit-content; + position: absolute; + row-gap: 5px; + width: 180px; + z-index: 9999; +} diff --git a/client/public/stylesheets/other/popup.css b/client/public/stylesheets/other/popup.css new file mode 100644 index 00000000..521e4cde --- /dev/null +++ b/client/public/stylesheets/other/popup.css @@ -0,0 +1,13 @@ +.ol-popup > div { + padding-left: 15px; + padding-right: 15px; +} + +.visible { + opacity: 1; +} + +.invisible { + opacity: 0; + transition: opacity 2s linear; +} \ No newline at end of file diff --git a/client/public/stylesheets/panels.css b/client/public/stylesheets/panels.css deleted file mode 100644 index 0b00f8a6..00000000 --- a/client/public/stylesheets/panels.css +++ /dev/null @@ -1,8 +0,0 @@ -/* Panels style */ -.olympus-panel { - background-color: var(--background-color-dark); - font-size: 12px; - transition: bottom 0.2s; - border-radius: 15px; - box-shadow: 0px 2px 5px #000A; -} diff --git a/client/public/stylesheets/panels/connectionstatus.css b/client/public/stylesheets/panels/connectionstatus.css new file mode 100644 index 00000000..2b7dcec4 --- /dev/null +++ b/client/public/stylesheets/panels/connectionstatus.css @@ -0,0 +1,19 @@ +#connection-status-panel dt::before { + content: "No connection"; +} + +#connection-status-panel dd::after { + border-radius: 50%; + background: red; + content: " "; + height: 12px; + width: 12px; +} + +#connection-status-panel[data-is-connected] dt::before { + content: "Connected"; +} + +#connection-status-panel[data-is-connected] dd::after { + background: var(--accent-green); +} \ No newline at end of file diff --git a/client/public/stylesheets/panels/mouseinfo.css b/client/public/stylesheets/panels/mouseinfo.css new file mode 100644 index 00000000..df33c28b --- /dev/null +++ b/client/public/stylesheets/panels/mouseinfo.css @@ -0,0 +1,109 @@ +#mouse-info-panel>* { + background-color: var(--background-grey); + border-radius: var(--border-radius-sm); + padding: 6px; +} + +#mouse-info-panel dl { + margin-bottom: 4px; + row-gap: 5px; +} + +#mouse-info-panel dt { + height: fit-content; + width: 30%; +} + +#mouse-info-panel dt::after { + align-items: center; + background-color: white; + border-radius: var(--border-radius-sm); + color: var(--background-steel); + display: flex; + font-size: 15.6px; + font-weight: bolder; + height: 16px; + justify-content: center; + line-height: 16px; + padding: 4px; + text-transform: uppercase; + width: 16px; +} + +#mouse-info-panel #measuring-tool dt { + height: 24px; + width: 24px; + background-color: var(--background-offwhite); + border-radius: var(--border-radius-sm); +} + +#mouse-info-panel #measuring-tool svg { + padding: 3px; + height: 100%; + width: 100%; +} + +#mouse-info-panel #measuring-tool dt svg>* { + fill: black; + stroke: black; +} + +#mouse-info-panel dt[data-label]::after { + content: attr(data-label); +} + +#mouse-info-panel dt[data-coalition="blue"]::after { + background-color: var(--primary-blue); +} + +#mouse-info-panel dt[data-coalition="red"]::after { + background-color: var(--primary-red); +} + +#mouse-info-panel dt[data-tooltip]:hover::before { + background-color: var(--background-grey); + border-radius: 5px; + content: attr(data-tooltip); + display: flex; + flex-wrap: nowrap; + padding: 5px; + position: absolute; + translate: calc(-100% - 15px) 0; + white-space: nowrap; +} + +#mouse-info-panel dd { + width: 70%; +} + +.br-info::after { + content: attr(data-bearing) '\00B0 / ' attr(data-distance) " " attr(data-distance-units); + font-weight: bold; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--background-offwhite); +} + +.br-info[data-coalition="blue"]::after { + color: var(--primary-blue) +} + +.br-info[data-coalition="red"]::after { + color: var(--primary-red) +} + +.br-info[data-message]::after { + content: attr(data-message); +} + +.coordinates::after { + content: attr(data-value); + font-weight: bold; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--background-offwhite); +} \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css new file mode 100644 index 00000000..0ad67eb6 --- /dev/null +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -0,0 +1,226 @@ +body.feature-forceShowUnitControlPanel #unit-control-panel { + display: block !important; +} + +#unit-control-panel { + display: flex; + flex-direction: column; + row-gap: 10px; +} + +#unit-control-panel h3 { + margin-bottom: 8px; +} + +#unit-control-panel #selected-units-container { + align-items: left; + border-radius: var(--border-radius-md); + display: flex; + flex-direction: column; + max-height: 215px; + overflow-x: hidden; + overflow-y: auto; + row-gap: 4px; +} + +#unit-control-panel #selected-units-container button { + align-items: center; + border-radius: var(--border-radius-lg); + display: flex; + font-size: 11px; + height: 32px; + justify-content: space-between; + margin-right: 5px; + position: relative; + width: calc(100% - 5px); +} + +#unit-control-panel #selected-units-container button::after { + border-radius: 999px; + color: var(--secondary-semitransparent-white); + content: attr(data-label); + font-size: 10px; + padding: 4px 6px; + white-space: nowrap; + width: fit-content; +} + +#unit-control-panel #selected-units-container button:hover::after { + max-width: 100%; + text-overflow: unset; +} + +#unit-control-panel #selected-units-container button::before { + border-radius: var(--border-radius-sm); + content: attr(data-callsign); + display: block; + overflow: hidden; + padding: 4px; + padding-left: 0; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + width: fit-content; +} + +#unit-control-panel h4 { + margin-bottom: 8px; +} + +#advanced-settings-dialog { + width: 400px; +} + +#advanced-settings-dialog>.ol-dialog-content { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + margin-bottom: 10px; + margin-top: 10px; + row-gap: 10px; +} + +#advanced-settings-dialog>.ol-dialog-content>div { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + row-gap: 10px; +} + +#advanced-settings-dialog>.ol-dialog-content>div>.ol-group { + justify-content: space-between; +} + +#advanced-settings-dialog h4 { + white-space: nowrap; +} + +#advanced-settings-dialog hr { + margin-bottom: 10px; + margin-top: 15px; +} + +#general-settings-grid { + display: grid; + grid-template-columns: 1fr 1fr; + row-gap: 10px; +} + +#general-settings-grid>div { + width: 49%; +} + +#flight-data .ol-slider { + margin: 20px 0px; +} + +.ol-slider-container dd { + column-gap: 5px; +} + +#flight-data .ol-switch { + height: 20px; + width: 50px; +} + +#flight-data .ol-switch-fill { + background-color: var(--accent-light-blue); +} + +#flight-data .ol-switch-fill::after { + background-color: white; +} + +#altitude-type-switch[data-value="true"]>.ol-switch-fill::before { + content: "AGL"; +} + +#altitude-type-switch[data-value="false"]>.ol-switch-fill::before { + content: "ASL"; +} + +#speed-type-switch[data-value="true"]>.ol-switch-fill::before { + content: "GS"; +} + +#speed-type-switch[data-value="false"]>.ol-switch-fill::before { + content: "CAS"; +} + +#unit-control-panel .ol-slider-value { + color: var(--accent-light-blue); + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +#unit-control-panel .switch-control { + align-items: center; + display: grid; + grid-template-columns: 1.35fr 0.65fr; +} + +#unit-control-panel .switch-control>*:nth-child(2) { + justify-self: end; +} + +#unit-control-panel .switch-control>*:nth-child(3) { + color: var(--secondary-semitransparent-white); +} + +#unit-control-panel .switch-control h4 { + margin: 0px; +} + +#unit-control-panel .switch-control .ol-switch { + height: 25px; + width: 60px; +} + +#unit-control-panel .switch-control .ol-switch-fill { + background-color: var(--accent-light-blue); +} + +#unit-control-panel .switch-control .ol-switch-fill::after { + background-color: white; +} + +#unit-control-panel .switch-control .ol-switch[data-value="true"]>.ol-switch-fill::before { + content: "YES"; +} + +#unit-control-panel .switch-control .ol-switch[data-value="false"]>.ol-switch-fill::before { + content: "NO"; +} + +#advanced-settings-div { + column-gap: 5px; + display: flex; +} + +#advanced-settings-div>*:nth-child(2) { + margin-left: auto; +} + +#advanced-settings-div button { + height: 40px; +} + +/* Element visibility control */ +#unit-control-panel:not([data-show-categories-tooltip]) #categories-tooltip, +#unit-control-panel:not([data-show-speed-slider]) #speed-slider, +#unit-control-panel:not([data-show-altitude-slider]) #altitude-slider, +#unit-control-panel:not([data-show-roe]) #roe, +#unit-control-panel:not([data-show-threat]) #threat, +#unit-control-panel:not([data-show-emissions-countermeasures]) #emissions-countermeasures, +#unit-control-panel:not([data-show-on-off]) #ai-on-off, +#unit-control-panel:not([data-show-follow-roads]) #follow-roads, +#unit-control-panel:not([data-show-advanced-settings-button]) #advanced-settings-button, +#advanced-settings-dialog:not([data-show-settings]) #general-settings, +#advanced-settings-dialog:not([data-show-tasking]) #tasking, +#advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox, +#advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox, +#advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options, +#advanced-settings-dialog:not([data-show-radio]) #radio-options { + display: none; +} \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitinfo.css b/client/public/stylesheets/panels/unitinfo.css new file mode 100644 index 00000000..95703f8a --- /dev/null +++ b/client/public/stylesheets/panels/unitinfo.css @@ -0,0 +1,124 @@ +#unit-info-panel>* { + position: relative; + min-height: 100px; + bottom: 0px; +} + +#general { + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 4px; + position: relative; +} + +#unit-label { + font-weight: bold; +} + +#unit-control { + color: var(--secondary-lighter-grey); + font-weight: bold; +} + +#unit-name { + margin-bottom: 4px; + padding: 0px 0; +} + +#current-task { + border-radius: var(--border-radius-lg); + margin-top: auto; + padding: 6px 16px; +} + +#current-task::after { + content: attr(data-current-task); + display: block; +} + +#loadout { + display: flex; + overflow: visible; + width: 100%; + min-width: 125px; +} + +#loadout-container { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#loadout-silhouette { + filter: invert(100%); + height: 100px; + margin-right: 25px; + width: 100px; +} + +#loadout-items { + align-self: center; + display: flex; + flex-flow: column nowrap; + row-gap: 8px; +} + +#loadout-items>* { + align-items: center; + column-gap: 8px; + display: flex; + white-space: nowrap; +} + +#loadout-items>*::before { + align-items: center; + background-color: var(--secondary-light-grey); + border-radius: 999px; + content: attr(data-qty); + display: flex; + font-size: 11px; + font-weight: bold; + padding: 4px 6px; +} + +#loadout-items>*::after { + content: attr(data-item); + max-width: 125px; + overflow: hidden; + position: relative; + text-overflow: ellipsis; + width: 100%; +} + +#fuel-percentage { + align-items: center; + display: flex; + margin-top: auto; +} + +#fuel-percentage::before { + content: url("/resources/theme/images/icons/fuel.svg"); + display: inline-block; + filter: invert(100%); + height: 16px; + margin-right: 6px; + width: 16px; +} + +#fuel-percentage::after { + content: attr(data-percentage) "%"; +} + +#fuel-display { + background-color: var(--background-grey); + border-radius: var(--border-radius-md); + height: 6px; + margin-top: 4px; + overflow: hidden; +} + +#fuel-display #fuel-bar { + border-radius: var(--border-radius-md); + height: 100%; +} \ No newline at end of file diff --git a/client/public/stylesheets/selectionscroll.css b/client/public/stylesheets/selectionscroll.css deleted file mode 100644 index 70cd38d3..00000000 --- a/client/public/stylesheets/selectionscroll.css +++ /dev/null @@ -1,119 +0,0 @@ -.olympus-selection-scroll-container { - position: absolute; - font-size: 12px; - border-radius: 5px; - width: 220px; - height: fit-content; - z-index: 2000; - max-height: 400px; - padding: 8px; - display: flex; - flex-direction: column; - row-gap: 5px; - align-items: center; -} - -#olympus-selection-scroll-top-bar { - color: white; - font-size: 14px; - opacity: 1; - border-radius: 5px; - padding: 5px; - background-color: #333D; - width: 100%; - text-align: center; - display: flex; - align-items: center; - justify-content: space-between; - height: 40px; - padding-left: 15px; - padding-right: 15px; -} - -.olympus-selection-scroll { - overflow-x: hidden; - overflow-y: auto; - height: 100%; - width: 100%; -} - -.olympus-selection-scroll::-webkit-scrollbar { - width: 10px; -} - -.olympus-selection-scroll::-webkit-scrollbar-track { - background-color: transparent; - border-radius: 100px; -} - -.olympus-selection-scroll::-webkit-scrollbar-thumb { - background-color: white; - border-radius: 100px; - opacity: 0.8; - margin-top: 10px; -} - -.olympus-selection-scroll-element { - border-bottom: 1px solid #FFF5; - color: white; - cursor: pointer; - font-size: 13px; - opacity: 1; - padding-top: 10px; - padding-bottom: 10px; - padding-left: 15px; - background-color: var(--background-color-dark); - font-weight: 600; -} - -.olympus-selection-scroll:last-child { - border-radius: 5px; - border-bottom: 0px transparent !important; -} - -.olympus-selection-scroll-container label { - display: inline-block; - width: 40px; - height: 24px; -} - -.olympus-selection-scroll-container input { - display: inline-block; - width: 0; - height: 0; - margin: 0px; -} - -.olympus-selection-scroll-switch { - position: relative; - display: inline-block; - width: 40px; - height: 24px; - background-color: var(--active-coalition-color); - border-radius: 999px; - cursor: pointer; -} - -.olympus-selection-scroll-switch:before { - position: absolute; - content: ""; - height: 16px; - width: 16px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: 0.2s; - transition: 0.2s; - border-radius: 999px; -} - -input:checked+.olympus-selection-scroll-switch:before { - -webkit-transform: translateX(16px); - -ms-transform: translateX(16px); - transform: translateX(16px); -} - -.olympus-selection-scroll-title { - font-size: 11px; - font-weight: 600; -} \ No newline at end of file diff --git a/client/public/stylesheets/selectionwheel.css b/client/public/stylesheets/selectionwheel.css deleted file mode 100644 index d0d56f50..00000000 --- a/client/public/stylesheets/selectionwheel.css +++ /dev/null @@ -1,102 +0,0 @@ -.olympus-selection-wheel { - margin: 0; - position: fixed; - z-index: 1000; - width: 220px; - height: 220px; - overflow: visible; - display: flex; - align-items: center; - justify-content: center; -} - -.olympus-wheel { - width: 100%; - border-radius: 50%; - background-color: var(--background-color-dark); - -webkit-mask: radial-gradient(transparent 30%, #000 31%); - mask: radial-gradient(transparent 30%, #000 31%); - transition: background-color 0.2s; -} - -.olympus-wheel:before { - content: ""; - display: block; - padding-top: 100%; -} - -.selection-wheel-button { - position: fixed; - z-index: 1000; - width: 50px; - height: 50px; - opacity: 0; - /*transition: opacity var(--animation_duration), left var(--animation_duration), top var(--animation_duration);*/ - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; -} - -.selection-wheel-image { - width: 45px; - height: 45px; - /*filter: invert(100%);*/ - transition: width var(--animation_duration), height var(--animation_duration); - filter: invert(100%); -} - -.selection-wheel-button:hover { - -} - -.selection-wheel-button:hover .selection-wheel-image { - width: 50px; - height: 50px; - /*filter: invert(21%) sepia(23%) saturate(775%) hue-rotate(170deg) brightness(92%) contrast(90%);*/ -} - -.olympus-selection-wheel label { - width: 0; - height: 0; -} - -.olympus-selection-wheel input { - width: 0; - height: 0; -} - -.olympus-selection-wheel-switch { - position: absolute; - top: 50%; - left: 50%; - display: inline-block; - width: 60px; - height: 34px; - background-color: var(--active-coalition-color); - border-radius: 17px; - margin-left: -30px; - margin-top: -17px; - cursor: pointer; -} - -.olympus-selection-wheel-switch:before { - position: absolute; - content: ""; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: 0.2s; - transition: 0.2s; - border-radius: 50%; - -} - -input:checked+.olympus-selection-wheel-switch:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); -} \ No newline at end of file diff --git a/client/public/stylesheets/slider.css b/client/public/stylesheets/slider.css deleted file mode 100644 index cddd49ad..00000000 --- a/client/public/stylesheets/slider.css +++ /dev/null @@ -1,47 +0,0 @@ -.slider-container { - width: 100%; -} - -.slider { - width: 100%; - -webkit-appearance: none; - appearance: none; - height: 2px; - background: #d3d3d3; - outline: none; - opacity: 0.7; - -webkit-transition: .2s; - transition: opacity .2s; - margin-top: 10px; - margin-bottom: 10px; -} - -.slider:hover { - opacity: 1; -} - -.slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 20px; - height: 20px; - background: gray; - cursor: pointer; - border-radius: 999px; -} - -.active .slider::-webkit-slider-thumb { - background: #5ca7ff; -} - -.slider::-moz-range-thumb { - width: 20px; - height: 20px; - background: gray; - cursor: pointer; - border-radius: 999px; -} - -.active .slider::-moz-range-thumb { - background: #5ca7ff; -} \ No newline at end of file diff --git a/client/public/stylesheets/style.css b/client/public/stylesheets/style.css deleted file mode 100644 index b68fd52a..00000000 --- a/client/public/stylesheets/style.css +++ /dev/null @@ -1,187 +0,0 @@ -@import url("button.css"); -@import url("slider.css"); -@import url("dropdown.css"); - -@import url("selectionwheel.css"); -@import url("selectionscroll.css"); - -@import url("unitmarker.css"); -@import url("airbasemarker.css"); - -@import url("panels.css"); -@import url("connectionstatuspanel.css"); -@import url("unitcontrolpanel.css"); -@import url("visibilitycontrolpanel.css"); -@import url("unitinfopanel.css"); -@import url("mouseinfopanel.css"); - -@import url("layout.css"); - - -/* Variables definitions */ -:root { - --background-color-dark: #202831; - --background-color-light: #AAA; - --title-color: #d3e9ff; - --text-color: white; - --blue-coalition-color: #247be2; - --red-coalition-color: #f32121; - --neutral-coalition-color: #202831; - --active-coalition-color: var(--blue-coalition-color); - --highlight-color: #FFF5; -} - -* { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} - -html { - font-family: 'Open Sans', sans-serif; -} - -.leaflet-container.crosshair-cursor-enabled { - cursor:crosshair; -} - -.rectangular-container { - padding: 0.5em; - background-color: gray; - border-radius: 5px; - width: fit-content; - height: fit-content; - text-align: center; - color: white; - font-size: 12px; -} - -.rectangular-container-dark { - padding-left: 0.5em; - padding-right: 0.5em; - padding-top: 0.2em; - padding-bottom: 0.2em; - background-color: #151b20; - border-radius: 5px; - width: fit-content; - height: fit-content; - text-align: center; - color: white; - font-size: 12px; -} - -.rounded-container { - position: relative; - padding: 0.5em; - width: fit-content; - height: fit-content; - text-align: center; - color: white; - font-size: 12px; - border-radius: 9999px; - background-color: gray; -} - -.rounded-container.blue { - background-color: var(--blue-coalition-color); - border: 1px solid var(--blue-coalition-color); -} - -.rounded-container.red { - background-color: var(--red-coalition-color); - border: 1px solid var(--red-coalition-color); -} - -.rounded-container.neutral { - background-color: var(--neutral-coalition-color); -} - -.rounded-container-small { - padding: 0.2em; - width: fit-content; - height: fit-content; - text-align: center; - color: black; - font-weight: 600; - background-color: #FFFA; - font-size: 11px; - border-radius: 9999px; - padding-left: 5px; - padding-right: 5px; -} - -.rectangular-button { - position: relative; - padding: 0.5em; - width: fit-content; - height: fit-content; - text-align: center; - color: var(--highlight-color); - font-size: 12px; - border-radius: 5px; - background-color: transparent; - border: 1px solid var(--highlight-color); - cursor: pointer; - font-weight: 600; - display: flex; - align-items: center; - column-gap: 5px; -} - -.rectangular-button.blue { - border: 1px solid var(--blue-coalition-color); - color: var(--blue-coalition-color); -} - -.rectangular-button.red { - border: 1px solid var(--red-coalition-color); - color: var(--red-coalition-color); -} - -.rectangular-button.white { - border: 1px solid white; - color: white; -} - -.rectangular-button.white>img { - filter: invert(100%); -} - -.rectangular-button>img { - display: inline-block; - height: 18px; - width: 18px; -} - -.rectangular-button.red { - border: 1px solid var(--red-coalition-color); -} - -.vl { - border-left: 1px solid #555; - width: 1px !important; - display: inline-block; -} - -.hl { - border-top: 1px solid #555; - height: 1px !important; - display: inline-block; -} - -.measure-box { - position: absolute; - padding-left: 0.5em; - padding-right: 0.5em; - padding-top: 0.2em; - padding-bottom: 0.2em; - background-color: #151b20; - border-radius: 5px; - width: fit-content; - height: fit-content; - text-align: center; - color: white; - font-size: 12px; - z-index: 2000; - font-weight: 600; -} \ No newline at end of file diff --git a/client/public/stylesheets/uikit/uikit.css b/client/public/stylesheets/uikit/uikit.css new file mode 100644 index 00000000..d7a8881c --- /dev/null +++ b/client/public/stylesheets/uikit/uikit.css @@ -0,0 +1,101 @@ +body { + background-color:#eaeaea; +} + +#content-wrapper { + row-gap: 5px; + display: flex; + flex-direction: column; + flex-wrap: wrap; + height:100%; + width:100%; +} + +section { + column-gap: 20px; + display:flex; + flex-direction: row; + flex-wrap: wrap; +} + +.content { + background:white; + border-radius: 10px; + height:fit-content; + margin-bottom:4vh; + padding:20px; + width:fit-content; +} + +.content-header { + color:#666; + letter-spacing:1px; + margin-bottom: 1vh; +} + +.content-body { + column-gap: 20px; + display:flex; + flex-direction: row; + flex-wrap: wrap; +} + +.example { + align-items: center; + display:flex; + flex-direction: column; + min-width: 100px; +} + +.caption { + margin:2vh 0 1vh 0; +} + + +#paragraph { + max-width: 750px; +} + + +#tabs { + column-gap: 10px; + display: flex; + flex-direction: row; + margin-bottom:1em; +} + +#tabs > div { + background:#660066; + border-radius: 5px; + color:white; + cursor: pointer; + padding:6px 10px; +} + + +#overlaying-planes { + background-color:#99ccff; + padding:60px; + position:relative; +} + +#overlaying-planes .unit:nth-of-type(2) { + position: absolute; + left: 40px; + top: 10px; +} + +.icon-list { + text-align: center; +} + +.icon-list { + display: flex; + flex-direction: column; + row-gap: 10px; +} + +.icon-list span { + display:block; + font-size: 12px; +} \ No newline at end of file diff --git a/client/public/stylesheets/unitcontrolpanel.css b/client/public/stylesheets/unitcontrolpanel.css deleted file mode 100644 index f01d791a..00000000 --- a/client/public/stylesheets/unitcontrolpanel.css +++ /dev/null @@ -1,130 +0,0 @@ -#unit-control-buttons { - display: flex; - flex-direction: column; - row-gap: 5px; - padding: 10px; - border-radius: 999px; -} - -#unit-control-buttons>div { - filter: invert(100%); - opacity: 0.8; -} - -#unit-control-panel { - display: flex; - flex-direction: column; - flex-wrap: wrap; - justify-content: space-between; - align-content: flex-start; - row-gap: 10px; - padding-left: 30px; - padding-right: 30px; - padding-top: 20px; - padding-bottom: 20px; -} - -#selected-units-container { - display: flex; - flex-direction: column; - row-gap: 5px; - width: 100%; - height: 100%; -} - -#formation-buttons-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - row-gap: 5px; - column-gap: 5px; - width: 100%; - height: 100%; -} - -#roe-buttons-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - row-gap: 5px; - column-gap: 5px; - width: 100%; - height: 100%; -} - -#reaction-to-threat-buttons-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - row-gap: 5px; - column-gap: 5px; - width: 100%; - height: 100%; -} - -#selected-units-container .rounded-container { - width: calc(100% - 25px); - cursor: pointer; - margin-left: 25px; -} - -#selected-units-container .rounded-container.not-selected { - background-color: transparent; -} - -#selected-units-container .rounded-container .rounded-container-small { - display: inline-block; - position: absolute; - left: 5px; - top: 5px; -} - -#selected-units-container img { - height: calc(100% + 6px); - display: inline-block; - position: absolute; - left: -32px; - top: -3px; -} - -#selected-units-container img.blue { - filter: invert(81%) sepia(6%) saturate(1685%) hue-rotate(181deg) brightness(103%) contrast(92%); -} - -#selected-units-container img.red { - filter: invert(93%) sepia(97%) saturate(1174%) hue-rotate(291deg) brightness(105%) contrast(97%); -} - -#unit-control-panel #title-label { - color: white; - font-size: 14px; - width: 100%; - font-weight: 600; -} - -#unit-control-panel #section-label { - color: white; - font-size: 13px; - width: 100%; -} - -.flight-control-slider { - display: flex; - flex-wrap: wrap; - justify-content: space-between; -} - -.flight-control-title { - font-size: 13px; - color: white; -} - -.flight-control-value { - font-size: 14px; - font-weight: 600; - color: gray; -} - -.active .flight-control-value { - color: #5ca7ff; -} diff --git a/client/public/stylesheets/unitinfopanel.css b/client/public/stylesheets/unitinfopanel.css deleted file mode 100644 index ec6f24d8..00000000 --- a/client/public/stylesheets/unitinfopanel.css +++ /dev/null @@ -1,121 +0,0 @@ - -/* Panel properties */ -#unit-info-panel { - display: flex; - flex-direction: row; - padding-left: 30px; - padding-right: 30px; - padding-top: 20px; - padding-bottom: 20px; -} - -/* Common */ -#unit-info-panel>div { - height: 100%; - width: 100%; -} - -#unit-info-panel>.vl { - margin-left: 30px; - margin-right: 30px; -} - -/* Sections */ -#unit-info-panel #general { - display: flex; - flex-flow: row; - flex-wrap: wrap; - justify-content: flex-start; - column-gap: 5px; - row-gap: 5px; - align-content: flex-start; -} - -#unit-info-panel #flight-data { - display: flex; - flex-flow: row; - flex-wrap: wrap; - justify-content: space-between; - align-content: center; - align-items: center; - align-content: space-between; -} - -#unit-info-panel #loadout-data { - display: flex; - flex-flow: row; - flex-wrap: wrap; - align-content: flex-start; - justify-content: flex-start; - row-gap: 5px; -} - -/* General section */ -#general #unit-name { - color: white; - font-size: 18px; - width: 100%; - padding-bottom: 5px; - font-weight: 600; -} - -#general #task { - width: 100%; -} - -/* Flight data section */ -#flight-data #flight-data-label { - color: white; - font-size: 14px; - width: 100%; - font-weight: 600; -} - -#flight-data #latitude { - width: 50%; - color: white; - font-size: 13px; - text-align: center; - font-weight: 600; - color: #8bff63; -} - -#flight-data #longitude { - width: 50%; - color: white; - font-size: 13px; - text-align: center; - font-weight: 600; - color: #5ca7ff; -} - -.flight-data-label, .flight-data-value { - color: white; - font-size: 12px; - padding-left: 10px; -} - -.flight-data-icon { - width: 10%; - padding: 2px; - filter: invert(100%); -} - -.flight-data-label { - width: 58%; -} - -.flight-data-value { - width: 30%; - font-weight: 600; - text-align: right; -} - -/* Loadout section */ -#loadout-data #loadout-label { - color: white; - font-size: 14px; - width: 100%; - font-weight: 600; -} - diff --git a/client/public/stylesheets/unitmarker.css b/client/public/stylesheets/unitmarker.css deleted file mode 100644 index babbb02b..00000000 --- a/client/public/stylesheets/unitmarker.css +++ /dev/null @@ -1,108 +0,0 @@ -.unit-marker-container { - height: 60px; - width: 60px; - left: -30px; - top: -30px; - border: 0px black solid; - position: absolute; - padding: 0; - margin: 0; - border-collapse: collapse; -} - -.unit-marker-icon { - height: 60px; - width: 60px; - left: 0px; - top: 0px; - display: flex; - position: absolute; - justify-content: center; - align-items: center; -} - -.unit-marker-selected { - width: 100%; - height: 100%; - border-radius: 50%; -} - -#ring { - top: 0px; - position: absolute; - display: inline-block; - -webkit-mask: radial-gradient(transparent 60%, #000 61%); - mask: radial-gradient(transparent 60%, #000 61%); -} - -#background { - top: 0px; - position: absolute; - display: inline-block; - opacity: 0.2; -} - -.blue.unit-marker-selected { - background-color: var(--blue-coalition-color); -} - -.red.unit-marker-selected { - background-color: var(--red-coalition-color); -} - - -.unit-marker-hovered { - filter: brightness(130%); -} - -.unit-marker-dead { - filter: brightness(50%); -} - -.unit-marker-unitName { - top: -20px; - position: absolute; - text-align: center; - font: 800 16px Arial; - white-space: nowrap; - -webkit-text-fill-color: white; - -webkit-text-stroke: 1px; -} - -.unit-marker-name { - bottom: -20px; - position: absolute; - text-align: center; - font: 800 14px Arial; - white-space: nowrap; - -webkit-text-fill-color: white; - -webkit-text-stroke: 1px; -} - -.unit-marker-altitude { - width: 100%; - left: 0px; - top: 0px; - position: absolute; - text-align: right; - font: 800 12px Arial; - white-space: nowrap; - -webkit-text-fill-color: white; - -webkit-text-stroke: 1px; -} - -.unit-marker-speed { - width: 100%; - left: 0px; - top: 0px; - position: absolute; - text-align: left; - font: 800 12px Arial; - white-space: nowrap; - -webkit-text-fill-color: white; - -webkit-text-stroke: 1px; -} - -.unit-marker-container-table-dead .unit-marker-name { - opacity: 0; -} \ No newline at end of file diff --git a/client/public/stylesheets/visibilitycontrolpanel.css b/client/public/stylesheets/visibilitycontrolpanel.css deleted file mode 100644 index 92ad6bef..00000000 --- a/client/public/stylesheets/visibilitycontrolpanel.css +++ /dev/null @@ -1,4 +0,0 @@ -#visibility-control-panel .olympus-button { - filter: invert(100%); - opacity: 0.8; -} \ No newline at end of file diff --git a/client/public/themes/olympus/images/buttons/emissions/attack.svg b/client/public/themes/olympus/images/buttons/emissions/attack.svg new file mode 100644 index 00000000..0846e4e6 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/emissions/attack.svg @@ -0,0 +1,44 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/emissions/defend.svg b/client/public/themes/olympus/images/buttons/emissions/defend.svg new file mode 100644 index 00000000..5c884188 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/emissions/defend.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/emissions/free.svg b/client/public/themes/olympus/images/buttons/emissions/free.svg new file mode 100644 index 00000000..f07b2db6 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/emissions/free.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/emissions/silent.svg b/client/public/themes/olympus/images/buttons/emissions/silent.svg new file mode 100644 index 00000000..f735bc21 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/emissions/silent.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/roe/designated.svg b/client/public/themes/olympus/images/buttons/roe/designated.svg new file mode 100644 index 00000000..0846e4e6 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/roe/designated.svg @@ -0,0 +1,44 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/roe/free.svg b/client/public/themes/olympus/images/buttons/roe/free.svg new file mode 100644 index 00000000..f07b2db6 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/roe/free.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/roe/hold.svg b/client/public/themes/olympus/images/buttons/roe/hold.svg new file mode 100644 index 00000000..f735bc21 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/roe/hold.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/roe/return.svg b/client/public/themes/olympus/images/buttons/roe/return.svg new file mode 100644 index 00000000..5c884188 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/roe/return.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/spawn/aircraft.svg b/client/public/themes/olympus/images/buttons/spawn/aircraft.svg new file mode 100644 index 00000000..beb45a25 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/aircraft.svg @@ -0,0 +1,76 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/spawn/explosion.svg b/client/public/themes/olympus/images/buttons/spawn/explosion.svg new file mode 100644 index 00000000..192784b0 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/explosion.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/spawn/ground.svg b/client/public/themes/olympus/images/buttons/spawn/ground.svg new file mode 100644 index 00000000..f0d8bf1c --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/ground.svg @@ -0,0 +1,83 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/spawn/smoke.svg b/client/public/themes/olympus/images/buttons/spawn/smoke.svg new file mode 100644 index 00000000..468c0c95 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/smoke.svg @@ -0,0 +1,76 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/threat/evade.svg b/client/public/themes/olympus/images/buttons/threat/evade.svg new file mode 100644 index 00000000..54d7b49f --- /dev/null +++ b/client/public/themes/olympus/images/buttons/threat/evade.svg @@ -0,0 +1,62 @@ + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg b/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg new file mode 100644 index 00000000..fcc9376b --- /dev/null +++ b/client/public/themes/olympus/images/buttons/threat/manoeuvre.svg @@ -0,0 +1,50 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/threat/none.svg b/client/public/themes/olympus/images/buttons/threat/none.svg new file mode 100644 index 00000000..53414370 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/threat/none.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/threat/passive.svg b/client/public/themes/olympus/images/buttons/threat/passive.svg new file mode 100644 index 00000000..ac558fef --- /dev/null +++ b/client/public/themes/olympus/images/buttons/threat/passive.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/airbase.svg b/client/public/themes/olympus/images/buttons/visibility/airbase.svg new file mode 100644 index 00000000..28ad42a4 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/airbase.svg @@ -0,0 +1,76 @@ + + + + + + + + image/svg+xml + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/aircraft.svg b/client/public/themes/olympus/images/buttons/visibility/aircraft.svg new file mode 100644 index 00000000..09a9e322 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/aircraft.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/dcs.svg b/client/public/themes/olympus/images/buttons/visibility/dcs.svg new file mode 100644 index 00000000..bb7bd479 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/dcs.svg @@ -0,0 +1,32 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/groundunit-other.svg b/client/public/themes/olympus/images/buttons/visibility/groundunit-other.svg new file mode 100644 index 00000000..0a448b6f --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/groundunit-other.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg b/client/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg new file mode 100644 index 00000000..2a568fbb --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/human.svg b/client/public/themes/olympus/images/buttons/visibility/human.svg new file mode 100644 index 00000000..51278e9e --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/human.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/buttons/visibility/navyunit.svg b/client/public/themes/olympus/images/buttons/visibility/navyunit.svg new file mode 100644 index 00000000..d4ff6fec --- /dev/null +++ b/client/public/themes/olympus/images/buttons/visibility/navyunit.svg @@ -0,0 +1,41 @@ + + + + + + diff --git a/client/public/images/icons/altitude.svg b/client/public/themes/olympus/images/icons/altitude.svg similarity index 100% rename from client/public/images/icons/altitude.svg rename to client/public/themes/olympus/images/icons/altitude.svg diff --git a/client/public/themes/olympus/images/icons/arrow-pointer-solid.svg b/client/public/themes/olympus/images/icons/arrow-pointer-solid.svg new file mode 100644 index 00000000..4499db1b --- /dev/null +++ b/client/public/themes/olympus/images/icons/arrow-pointer-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/arrows-to-eye-solid.svg b/client/public/themes/olympus/images/icons/arrows-to-eye-solid.svg new file mode 100644 index 00000000..11815283 --- /dev/null +++ b/client/public/themes/olympus/images/icons/arrows-to-eye-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/bomb-solid.svg b/client/public/themes/olympus/images/icons/bomb-solid.svg new file mode 100644 index 00000000..79718751 --- /dev/null +++ b/client/public/themes/olympus/images/icons/bomb-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/check_square.svg b/client/public/themes/olympus/images/icons/check_square.svg new file mode 100644 index 00000000..85359d47 --- /dev/null +++ b/client/public/themes/olympus/images/icons/check_square.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/themes/olympus/images/icons/chevron-down.svg b/client/public/themes/olympus/images/icons/chevron-down.svg new file mode 100644 index 00000000..800f4fe2 --- /dev/null +++ b/client/public/themes/olympus/images/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/themes/olympus/images/icons/crosshairs-solid.svg b/client/public/themes/olympus/images/icons/crosshairs-solid.svg new file mode 100644 index 00000000..c2a60f84 --- /dev/null +++ b/client/public/themes/olympus/images/icons/crosshairs-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/diamond.svg b/client/public/themes/olympus/images/icons/diamond.svg new file mode 100644 index 00000000..eef3c6f2 --- /dev/null +++ b/client/public/themes/olympus/images/icons/diamond.svg @@ -0,0 +1,73 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/echelon-lh.svg b/client/public/themes/olympus/images/icons/echelon-lh.svg new file mode 100644 index 00000000..9359bfba --- /dev/null +++ b/client/public/themes/olympus/images/icons/echelon-lh.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/echelon-rh.svg b/client/public/themes/olympus/images/icons/echelon-rh.svg new file mode 100644 index 00000000..2da057de --- /dev/null +++ b/client/public/themes/olympus/images/icons/echelon-rh.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/echelon.svg b/client/public/themes/olympus/images/icons/echelon.svg new file mode 100644 index 00000000..21bb81bb --- /dev/null +++ b/client/public/themes/olympus/images/icons/echelon.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/explosion-solid.svg b/client/public/themes/olympus/images/icons/explosion-solid.svg new file mode 100644 index 00000000..b6383531 --- /dev/null +++ b/client/public/themes/olympus/images/icons/explosion-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/follow.svg b/client/public/themes/olympus/images/icons/follow.svg new file mode 100644 index 00000000..b3296b24 --- /dev/null +++ b/client/public/themes/olympus/images/icons/follow.svg @@ -0,0 +1,58 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/public/images/icons/formation-end.svg b/client/public/themes/olympus/images/icons/formation-end.svg similarity index 100% rename from client/public/images/icons/formation-end.svg rename to client/public/themes/olympus/images/icons/formation-end.svg diff --git a/client/public/images/icons/formation-middle.svg b/client/public/themes/olympus/images/icons/formation-middle.svg similarity index 100% rename from client/public/images/icons/formation-middle.svg rename to client/public/themes/olympus/images/icons/formation-middle.svg diff --git a/client/public/themes/olympus/images/icons/front.svg b/client/public/themes/olympus/images/icons/front.svg new file mode 100644 index 00000000..cfb0f29a --- /dev/null +++ b/client/public/themes/olympus/images/icons/front.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/images/icons/fuel.svg b/client/public/themes/olympus/images/icons/fuel.svg similarity index 100% rename from client/public/images/icons/fuel.svg rename to client/public/themes/olympus/images/icons/fuel.svg diff --git a/client/public/themes/olympus/images/icons/gears-solid.svg b/client/public/themes/olympus/images/icons/gears-solid.svg new file mode 100644 index 00000000..35a083dd --- /dev/null +++ b/client/public/themes/olympus/images/icons/gears-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/grip-lines-solid.svg b/client/public/themes/olympus/images/icons/grip-lines-solid.svg new file mode 100644 index 00000000..85af24c3 --- /dev/null +++ b/client/public/themes/olympus/images/icons/grip-lines-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/images/icons/heading.svg b/client/public/themes/olympus/images/icons/heading.svg similarity index 100% rename from client/public/images/icons/heading.svg rename to client/public/themes/olympus/images/icons/heading.svg diff --git a/client/public/themes/olympus/images/icons/line-abreast.svg b/client/public/themes/olympus/images/icons/line-abreast.svg new file mode 100644 index 00000000..c146a848 --- /dev/null +++ b/client/public/themes/olympus/images/icons/line-abreast.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/map_source.svg b/client/public/themes/olympus/images/icons/map_source.svg new file mode 100644 index 00000000..977057cc --- /dev/null +++ b/client/public/themes/olympus/images/icons/map_source.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/pin.svg b/client/public/themes/olympus/images/icons/pin.svg new file mode 100644 index 00000000..e42653e9 --- /dev/null +++ b/client/public/themes/olympus/images/icons/pin.svg @@ -0,0 +1,46 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/icons/plane.svg b/client/public/themes/olympus/images/icons/plane.svg new file mode 100644 index 00000000..b672646e --- /dev/null +++ b/client/public/themes/olympus/images/icons/plane.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/images/icons/ruler.svg b/client/public/themes/olympus/images/icons/ruler.svg similarity index 100% rename from client/public/images/icons/ruler.svg rename to client/public/themes/olympus/images/icons/ruler.svg diff --git a/client/public/images/icons/speed.svg b/client/public/themes/olympus/images/icons/speed.svg similarity index 100% rename from client/public/images/icons/speed.svg rename to client/public/themes/olympus/images/icons/speed.svg diff --git a/client/public/themes/olympus/images/icons/square-check-solid.svg b/client/public/themes/olympus/images/icons/square-check-solid.svg new file mode 100644 index 00000000..2bd2823a --- /dev/null +++ b/client/public/themes/olympus/images/icons/square-check-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/square-regular.svg b/client/public/themes/olympus/images/icons/square-regular.svg new file mode 100644 index 00000000..f66b39f7 --- /dev/null +++ b/client/public/themes/olympus/images/icons/square-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/sword.svg b/client/public/themes/olympus/images/icons/sword.svg new file mode 100644 index 00000000..2d925c03 --- /dev/null +++ b/client/public/themes/olympus/images/icons/sword.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/trail.svg b/client/public/themes/olympus/images/icons/trail.svg new file mode 100644 index 00000000..fb23fd08 --- /dev/null +++ b/client/public/themes/olympus/images/icons/trail.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/trash-can-regular.svg b/client/public/themes/olympus/images/icons/trash-can-regular.svg new file mode 100644 index 00000000..011e1a5e --- /dev/null +++ b/client/public/themes/olympus/images/icons/trash-can-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg new file mode 100644 index 00000000..bb69b55f --- /dev/null +++ b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/markers/airbase.svg b/client/public/themes/olympus/images/markers/airbase.svg new file mode 100644 index 00000000..d1fcf84c --- /dev/null +++ b/client/public/themes/olympus/images/markers/airbase.svg @@ -0,0 +1,83 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/markers/bullseye.svg b/client/public/themes/olympus/images/markers/bullseye.svg new file mode 100644 index 00000000..92d0b60d --- /dev/null +++ b/client/public/themes/olympus/images/markers/bullseye.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/client/public/images/marker-icon.png b/client/public/themes/olympus/images/markers/marker-icon.png similarity index 100% rename from client/public/images/marker-icon.png rename to client/public/themes/olympus/images/markers/marker-icon.png diff --git a/client/public/images/marker-shadow.png b/client/public/themes/olympus/images/markers/marker-shadow.png similarity index 100% rename from client/public/images/marker-shadow.png rename to client/public/themes/olympus/images/markers/marker-shadow.png diff --git a/client/public/themes/olympus/images/markers/move.svg b/client/public/themes/olympus/images/markers/move.svg new file mode 100644 index 00000000..4af0aa8e --- /dev/null +++ b/client/public/themes/olympus/images/markers/move.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/markers/target.svg b/client/public/themes/olympus/images/markers/target.svg new file mode 100644 index 00000000..7afbf612 --- /dev/null +++ b/client/public/themes/olympus/images/markers/target.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/markers/temporary-icon.png b/client/public/themes/olympus/images/markers/temporary-icon.png new file mode 100644 index 00000000..712221a0 Binary files /dev/null and b/client/public/themes/olympus/images/markers/temporary-icon.png differ diff --git a/client/public/themes/olympus/images/splash/1.png b/client/public/themes/olympus/images/splash/1.png new file mode 100644 index 00000000..1ae92cce Binary files /dev/null and b/client/public/themes/olympus/images/splash/1.png differ diff --git a/client/public/themes/olympus/images/states/attack.svg b/client/public/themes/olympus/images/states/attack.svg new file mode 100644 index 00000000..84cd084f --- /dev/null +++ b/client/public/themes/olympus/images/states/attack.svg @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/dcs.svg b/client/public/themes/olympus/images/states/dcs.svg new file mode 100644 index 00000000..1652fc61 --- /dev/null +++ b/client/public/themes/olympus/images/states/dcs.svg @@ -0,0 +1,71 @@ + + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/follow.svg b/client/public/themes/olympus/images/states/follow.svg new file mode 100644 index 00000000..92dadcac --- /dev/null +++ b/client/public/themes/olympus/images/states/follow.svg @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/human.svg b/client/public/themes/olympus/images/states/human.svg new file mode 100644 index 00000000..dd64fcc8 --- /dev/null +++ b/client/public/themes/olympus/images/states/human.svg @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/states/idle.svg b/client/public/themes/olympus/images/states/idle.svg new file mode 100644 index 00000000..757ee216 --- /dev/null +++ b/client/public/themes/olympus/images/states/idle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/public/themes/olympus/images/states/no-task.svg b/client/public/themes/olympus/images/states/no-task.svg new file mode 100644 index 00000000..2e1e906d --- /dev/null +++ b/client/public/themes/olympus/images/states/no-task.svg @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/refuel.svg b/client/public/themes/olympus/images/states/refuel.svg new file mode 100644 index 00000000..422193f1 --- /dev/null +++ b/client/public/themes/olympus/images/states/refuel.svg @@ -0,0 +1,61 @@ + + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/rtb.svg b/client/public/themes/olympus/images/states/rtb.svg new file mode 100644 index 00000000..f40e2627 --- /dev/null +++ b/client/public/themes/olympus/images/states/rtb.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/public/themes/olympus/images/units/aircraft.svg b/client/public/themes/olympus/images/units/aircraft.svg new file mode 100644 index 00000000..104f9bcf --- /dev/null +++ b/client/public/themes/olympus/images/units/aircraft.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/public/themes/olympus/images/units/bomb.svg b/client/public/themes/olympus/images/units/bomb.svg new file mode 100644 index 00000000..a38a168e --- /dev/null +++ b/client/public/themes/olympus/images/units/bomb.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/themes/olympus/images/units/death.svg b/client/public/themes/olympus/images/units/death.svg new file mode 100644 index 00000000..c68ab1d1 --- /dev/null +++ b/client/public/themes/olympus/images/units/death.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/themes/olympus/images/units/groundunit-other.svg b/client/public/themes/olympus/images/units/groundunit-other.svg new file mode 100644 index 00000000..6b6dcc7c --- /dev/null +++ b/client/public/themes/olympus/images/units/groundunit-other.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/public/themes/olympus/images/units/groundunit-sam.svg b/client/public/themes/olympus/images/units/groundunit-sam.svg new file mode 100644 index 00000000..ffc18a40 --- /dev/null +++ b/client/public/themes/olympus/images/units/groundunit-sam.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/public/themes/olympus/images/units/missile.svg b/client/public/themes/olympus/images/units/missile.svg new file mode 100644 index 00000000..d7711d7c --- /dev/null +++ b/client/public/themes/olympus/images/units/missile.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/units/navyunit.svg b/client/public/themes/olympus/images/units/navyunit.svg new file mode 100644 index 00000000..b3b2ed1b --- /dev/null +++ b/client/public/themes/olympus/images/units/navyunit.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/public/themes/olympus/images/units/static.svg b/client/public/themes/olympus/images/units/static.svg new file mode 100644 index 00000000..ead61396 --- /dev/null +++ b/client/public/themes/olympus/images/units/static.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css new file mode 100644 index 00000000..57f6bb3f --- /dev/null +++ b/client/public/themes/olympus/theme.css @@ -0,0 +1,82 @@ +:root { + /** Colours **/ + + /*** Coalition: neutral ***/ + --primary-neutral: #949ba7; + --secondary-neutral-outline: #111111; + --secondary-neutral-text: #111111; + --unit-background-neutral: #CFD9E8; + + /*** Coalition: blue ***/ + --primary-blue: #247be2; + --secondary-blue-outline: #082e44; + --secondary-blue-text: #017DC1; + --unit-background-blue: #3BB9FF; + + /*** Coalition: red ***/ + --primary-red: #ff5858; + --secondary-red-outline: #262222; + --secondary-red-text: #D42121; + --unit-background-red: #FF5858; + + /*** UI Colours **/ + --accent-green: #8bff63; + --accent-light-blue: #5ca7ff; + --accent-light-red: #F5B6B6; + + --background-grey: #3d4651; + --background-slate-blue: #363c43; + --background-offwhite: #f2f2f3; + --background-steel: #202831; + + --secondary-dark-steel: #181e25; + --secondary-gunmetal-grey: #2f2f2f; + --secondary-lighter-grey: #949ba7; + --secondary-light-grey: #797e83; + --secondary-semitransparent-white: #FFFFFFAA; + --secondary-transparent-white: #FFFFFF30; + --secondary-yellow: #ffd46893; + + --background-hover: #f2f2f333; + + --nav-text: #ECECEC; + + --ol-select-secondary: #545F6C; + + /*** General border radii **/ + --border-radius-xs: 2px; + --border-radius-sm: 5px; + --border-radius-md: 10px; + --border-radius-lg: 15px; + + /*** Fonts **/ + --font-weight-bolder: 600; + + /*** Unit marker settings ***/ + /*** All markers **/ + --unit-border-radius: var(--border-radius-xs); + --unit-font-size: 14px; + --unit-font-weight: bolder; + --unit-label-border-width: 2px; + --unit-spotlight-fill: var(--secondary-yellow); + --unit-spotlight-radius: 26px; + --unit-stroke-width: 3px; + --unit-height: 50px; + --unit-width: 50px; + + /*** Air units ***/ + --unit-aircraft-ammo-gap: calc(2px + var(--unit-stroke-width)); + --unit-aircraft-ammo-border-radius: 50%; + --unit-aircraft-ammo-border-width: 2px; + --unit-aircraft-ammo-radius: 2px; + --unit-aircraft-ammo-spacing: 2px; + --unit-aircraft-ammo-x: 0px; + --unit-aircraft-ammo-y: 30px; + --unit-aircraft-fuel-border-width: 2px; + --unit-aircraft-fuel-height: 6px; + --unit-aircraft-fuel-width: 36px; + --unit-aircraft-fuel-x: 0px; + --unit-aircraft-fuel-y: 22px; + --unit-aircraft-height: 28px; + --unit-aircraft-vvi-width: 4px; +} \ No newline at end of file diff --git a/client/routes/api/airbases.js b/client/routes/api/airbases.js new file mode 100644 index 00000000..a080c794 --- /dev/null +++ b/client/routes/api/airbases.js @@ -0,0 +1,93 @@ +var express = require('express'); +var app = express(); + +var fs = require('fs'); + +const bodyParser = require('body-parser'); +app.use(bodyParser.urlencoded({ extended: false})); +app.use(bodyParser.json()); + +const allowedTheatres = [ + "caucasus", + "marianas", + "nevada", + "persiangulf", + "syria" +]; + + +function getAirbasesData( theatreName ) { + + if ( !isValidTheatre( theatreName ) ) { + return false; + } + + return JSON.parse( fs.readFileSync( `src/airfields/${theatreName}.json` ) ).airfields + +} + + +function isValidTheatre( theatre ) { + + return ( allowedTheatres.indexOf( theatre ) > -1 ) + +} + + +function sendInvalidTheatre( res ) { + + res.status( 400 ).send( "Missing/invalid theatre name; must be one of:\n\t" + allowedTheatres.join( "\n\t" ) ); + +} + + +/**************************************************************************************************************/ +// Endpoints +/**************************************************************************************************************/ + + + +app.get( "/", ( req, res ) => { + + sendInvalidTheatre( res ); + +}); + + +app.get( "/:theatreName/:airbaseName", ( req, res ) => { + + const airbases = getAirbasesData( req.params.theatreName ); + + if ( !airbases ) { + sendInvalidTheatre( res ); + return; + } + + const airbaseName = req.params.airbaseName; + + if ( !airbases.hasOwnProperty( airbaseName ) ) { + res.status( 404 ).send( `Unknown airbase name "${airbaseName}". Available options are:\n\t` + Object.keys( airbases ).join( "\n\t" ) ); + } else { + res.status( 200 ).json( airbases[ airbaseName ] ); + } + + +}); + + +app.get( "/:theatreName", ( req, res ) => { + + const airbases = getAirbasesData( req.params.theatreName ); + + if ( !airbases ) { + sendInvalidTheatre( res ); + return; + } + + res.status( 200 ).json( airbases ); + +}); + + + +module.exports = app; \ No newline at end of file diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js new file mode 100644 index 00000000..2429cdce --- /dev/null +++ b/client/routes/api/atc.js @@ -0,0 +1,295 @@ +var express = require('express'); +var app = express(); + +const bodyParser = require('body-parser'); +app.use(bodyParser.urlencoded({ extended: false})); +app.use(bodyParser.json()); + + +/* + + Flight: + "name" + "take-off time" + "priority" + "status" + +//*/ + +function uuidv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + + + +function Flight( name, boardId, unitId ) { + this.assignedAltitude = 0; + this.assignedSpeed = 0; + this.id = uuidv4(); + this.boardId = boardId; + this.name = name; + this.status = "unknown"; + this.takeoffTime = -1; + this.unitId = parseInt( unitId ); +} + +Flight.prototype.getData = function() { + return { + "assignedAltitude" : this.assignedAltitude, + "assignedSpeed" : this.assignedSpeed, + "id" : this.id, + "boardId" : this.boardId, + "name" : this.name, + "status" : this.status, + "takeoffTime" : this.takeoffTime, + "unitId" : this.unitId + }; +} + + +Flight.prototype.setAssignedAltitude = function( assignedAltitude ) { + + if ( isNaN( assignedAltitude ) ) { + return "Altitude must be a number" + } + + this.assignedAltitude = parseInt( assignedAltitude ); + + return true; + +} + + +Flight.prototype.setAssignedSpeed = function( assignedSpeed ) { + + if ( isNaN( assignedSpeed ) ) { + return "Speed must be a number" + } + + this.assignedSpeed = parseInt( assignedSpeed ); + + return true; + +} + + +Flight.prototype.setOrder = function( order ) { + + this.order = order; + + return true; + +} + + +Flight.prototype.setStatus = function( status ) { + + if ( [ "unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated" ].indexOf( status ) < 0 ) { + return "Invalid status"; + } + + this.status = status; + + return true; + +} + + +Flight.prototype.setTakeoffTime = function( takeoffTime ) { + + if ( takeoffTime === "" || takeoffTime === -1 ) { + this.takeoffTime = -1; + } + + if ( isNaN( takeoffTime ) ) { + return "Invalid takeoff time" + } + + this.takeoffTime = parseInt( takeoffTime ); + + return true; + +} + + + +function ATCDataHandler( data ) { + this.data = data; +} + +ATCDataHandler.prototype.addFlight = function( flight ) { + + if ( flight instanceof Flight === false ) { + throw new Error( "Given flight is not an instance of Flight" ); + } + + this.data.flights[ flight.id ] = flight; + +} + + +ATCDataHandler.prototype.deleteFlight = function( flightId ) { + delete this.data.flights[ flightId ]; +} + + +ATCDataHandler.prototype.getFlight = function( flightId ) { + return this.data.flights[ flightId ] || false; +} + + +ATCDataHandler.prototype.getFlights = function() { + return this.data.flights; +} + + +const dataHandler = new ATCDataHandler( { + "flights": {} +} ); + + + +/**************************************************************************************************************/ +// Endpoints +/**************************************************************************************************************/ + + +app.get( "/flight", ( req, res ) => { + + let flights = Object.values( dataHandler.getFlights() ); + + if ( flights && req.query.boardId ) { + + flights = flights.reduce( ( acc, flight ) => { + if ( flight.boardId === req.query.boardId ) { + acc[ flight.id ] = flight; + } + return acc; + }, {} ); + + } + + res.json( flights ); + +}); + + +app.patch( "/flight/:flightId", ( req, res ) => { + + const flightId = req.params.flightId; + const flight = dataHandler.getFlight( flightId ); + + if ( !flight ) { + res.status( 400 ).send( `Unrecognised flight ID (given: "${req.params.flightId}")` ); + } + + if ( req.body.hasOwnProperty( "assignedAltitude" ) ) { + + const altitudeChangeSuccess = flight.setAssignedAltitude( req.body.assignedAltitude ); + + if ( altitudeChangeSuccess !== true ) { + res.status( 400 ).send( altitudeChangeSuccess ); + } + + } + + if ( req.body.hasOwnProperty( "assignedSpeed" ) ) { + + const speedChangeSuccess = flight.setAssignedSpeed( req.body.assignedSpeed ); + + if ( speedChangeSuccess !== true ) { + res.status( 400 ).send( speedChangeSuccess ); + } + + } + + if ( req.body.status ) { + + const statusChangeSuccess = flight.setStatus( req.body.status ); + + if ( statusChangeSuccess !== true ) { + res.status( 400 ).send( statusChangeSuccess ); + } + + } + + if ( req.body.hasOwnProperty( "takeoffTime" ) ) { + + const takeoffChangeSuccess = flight.setTakeoffTime( req.body.takeoffTime ); + + if ( takeoffChangeSuccess !== true ) { + res.status( 400 ).send( takeoffChangeSuccess ); + } + + } + + res.json( flight.getData() ); + +}); + + +app.post( "/flight/order", ( req, res ) => { + + if ( !req.body.boardId ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + + if ( !req.body.order || !Array.isArray( req.body.order ) ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + + req.body.order.forEach( ( flightId, i ) => { + + dataHandler.getFlight( flightId ).setOrder( i ); + + }); + + res.send( "" ); + +}); + + +app.post( "/flight", ( req, res ) => { + + if ( !req.body.boardId ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + + if ( !req.body.name ) { + res.status( 400 ).send( "Invalid/missing flight name" ); + } + + if ( !req.body.unitId || isNaN( req.body.unitId ) ) { + res.status( 400 ).send( "Invalid/missing unitId" ); + } + + const flight = new Flight( req.body.name, req.body.boardId, req.body.unitId ); + + dataHandler.addFlight( flight ); + + res.status( 201 ); + + res.json( flight.getData() ); + +}); + + +app.delete( "/flight/:flightId", ( req, res ) => { + + const flight = dataHandler.getFlight( req.params.flightId ); + + if ( !flight ) { + res.status( 400 ).send( `Unrecognised flight ID (given: "${req.params.flightId}")` ); + } + + dataHandler.deleteFlight( req.params.flightId ); + + res.status( 204 ).send( "" ); + +}); + + +module.exports = app; \ No newline at end of file diff --git a/client/routes/resources.js b/client/routes/resources.js new file mode 100644 index 00000000..2be3af2f --- /dev/null +++ b/client/routes/resources.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); + +var theme = "olympus"; + +router.get('/theme/*', function (req, res, next) { + res.redirect(req.url.replace("theme", "themes/" + theme)); +}); + +module.exports = router; diff --git a/client/routes/uikit.js b/client/routes/uikit.js new file mode 100644 index 00000000..d9d4cb46 --- /dev/null +++ b/client/routes/uikit.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function (req, res, next) { + res.render('uikit', { title: 'Express' }); +}); + +module.exports = router; diff --git a/client/src/@types/dom.d.ts b/client/src/@types/dom.d.ts new file mode 100644 index 00000000..3feba2b0 --- /dev/null +++ b/client/src/@types/dom.d.ts @@ -0,0 +1,37 @@ +interface CustomEventMap { + "unitSelection": CustomEvent, + "unitDeselection": CustomEvent, + "unitsSelection": CustomEvent, + "unitsDeselection": CustomEvent, + "clearSelection": CustomEvent<>, + "unitCreation": CustomEvent, + "unitDeletion": CustomEvent, + "unitDeath": CustomEvent, + "unitUpdated": CustomEvent, + "unitMoveCommand": CustomEvent, + "unitAttackCommand": CustomEvent, + "unitLandCommand": CustomEvent, + "unitSetAltitudeCommand": CustomEvent, + "unitSetSpeedCommand": CustomEvent, + "unitSetOption": CustomEvent, + "groupCreation": CustomEvent, + "groupDeletion": CustomEvent, + "mapStateChanged": CustomEvent, + "mapContextMenu": CustomEvent<> +} + +declare global { + interface Document { + addEventListener(type: K, + listener: (this: Document, ev: CustomEventMap[K]) => void): void; + dispatchEvent(ev: CustomEventMap[K]): void; + } +} + +export interface ContextMenuOption { + tooltip: string; + src: string; + callback: CallableFunction; +} + +export { }; \ No newline at end of file diff --git a/client/src/@types/server.d.ts b/client/src/@types/server.d.ts new file mode 100644 index 00000000..e1a9c3e9 --- /dev/null +++ b/client/src/@types/server.d.ts @@ -0,0 +1,16 @@ +interface UnitsData { + units: {[key: string]: UnitData}, + sessionHash: string +} + +interface AirbasesData { + airbases: {[key: string]: any}, +} + +interface BullseyesData { + bullseyes: {[key: string]: {latitude: number, longitude: number, coalition: string}}, +} + +interface LogData { + logs: {[key: string]: string}, +} \ No newline at end of file diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts new file mode 100644 index 00000000..0a7847b9 --- /dev/null +++ b/client/src/@types/unit.d.ts @@ -0,0 +1,100 @@ +interface UpdateData { + [key: string]: any +} + +interface BaseData { + controlled: boolean; + name: string; + unitName: string; + groupName: string; + alive: boolean; + category: string; +} + +interface FlightData { + latitude: number; + longitude: number; + altitude: number; + heading: number; + speed: number; +} + +interface MissionData { + fuel: number; + flags: any; + ammo: any; + contacts: any; + hasTask: boolean; + coalition: string; +} + +interface FormationData { + leaderID: number; +} + +interface TaskData { + currentState: string; + currentTask: string; + activePath: any; + desiredSpeed: number; + desiredSpeedType: string; + desiredAltitude: number; + desiredAltitudeType: string; + targetLocation: any; + isTanker: boolean; + isAWACS: boolean; + onOff: boolean; + followRoads: boolean; + targetID: number; +} + +interface OptionsData { + ROE: string; + reactionToThreat: string; + emissionsCountermeasures: string; + TACAN: TACAN; + radio: Radio; + generalSettings: GeneralSettings; +} + +interface TACAN { + isOn: boolean; + channel: number; + XY: string; + callsign: string; +} + +interface Radio { + frequency: number; + callsign: number; + callsignNumber: number; +} + +interface GeneralSettings { + prohibitJettison: boolean; + prohibitAA: boolean; + prohibitAG: boolean; + prohibitAfterburner: boolean; + prohibitAirWpn: boolean; +} + +interface UnitIconOptions { + showState: boolean, + showVvi: boolean, + showHotgroup: boolean, + showUnitIcon: boolean, + showShortLabel: boolean, + showFuel: boolean, + showAmmo: boolean, + showSummary: boolean, + rotateToHeading: boolean +} + +interface UnitData { + baseData: BaseData; + flightData: FlightData; + missionData: MissionData; + formationData: FormationData; + taskData: TaskData; + optionsData: OptionsData; +} \ No newline at end of file diff --git a/client/src/@types/unitdatabase.d.ts b/client/src/@types/unitdatabase.d.ts new file mode 100644 index 00000000..4348e35e --- /dev/null +++ b/client/src/@types/unitdatabase.d.ts @@ -0,0 +1,22 @@ +interface LoadoutItemBlueprint { + name: string; + quantity: number; + effectiveAgainst?: string; +} + +interface LoadoutBlueprint { + fuel: number; + items: LoadoutItemBlueprint[]; + roles: string[]; + code: string; + name: string; +} + +interface UnitBlueprint { + name: string; + era?: string[]; + label: string; + shortLabel: string; + loadouts: LoadoutBlueprint[]; + filename: string; +} diff --git a/client/src/aic/aic.ts b/client/src/aic/aic.ts new file mode 100644 index 00000000..2aaf8a1c --- /dev/null +++ b/client/src/aic/aic.ts @@ -0,0 +1,172 @@ +import { ToggleableFeature } from "../features/toggleablefeature"; +import { AICFormation_Azimuth } from "./aicformation/azimuth"; +import { AICFormation_Range } from "./aicformation/range"; +import { AICFormation_Single } from "./aicformation/single"; +import { AICFormationDescriptorSection } from "./aicformationdescriptorsection"; + + +export class AIC extends ToggleableFeature { + + #formations = [ + + new AICFormation_Single(), + new AICFormation_Range(), + new AICFormation_Azimuth() + + ]; + + + constructor() { + + super( false ); + + this.onStatusUpdate(); + + // This feels kind of dirty + let $aicFormationList = document.getElementById( "aic-formation-list" ); + + if ( $aicFormationList ) { + + this.getFormations().forEach( formation => { + + // Image + let $imageDiv = document.createElement( "div" ); + $imageDiv.classList.add( "aic-formation-image" ); + + let $img = document.createElement( "img" ); + $img.src = "images/formations/" + formation.icon; + + $imageDiv.appendChild( $img ); + + // Name + let $nameDiv = document.createElement( "div" ); + $nameDiv.classList.add( "aic-formation-name" ); + $nameDiv.innerText = formation.label; + + // Wrapper + let $wrapperDiv = document.createElement( "div" ); + $wrapperDiv.dataset.formationName = formation.name; + $wrapperDiv.appendChild( $imageDiv ) + $wrapperDiv.appendChild( $nameDiv ); + $wrapperDiv.addEventListener( "click", ( ev ) => { + + const controlTypeInput = document.querySelector( "input[type='radio'][name='control-type']:checked" ); + + let controlTypeValue:any = ( controlTypeInput instanceof HTMLInputElement && [ "broadcast", "tactical" ].indexOf( controlTypeInput.value ) > -1 ) ? controlTypeInput.value : "broadcast"; + + // TODO: make this not an "any" + const output:any = formation.getDescriptor({ + "aicCallsign" : "Magic", + "bullseyeName" : "Bullseye", + "control" : controlTypeValue, + "numGroups" : formation.numGroups + }); + + this.updateTeleprompt( output ); + + }); + + // Add to DOM + $aicFormationList?.appendChild( $wrapperDiv ); + + }); + + } + + } + + + getFormations() { + return this.#formations; + } + + + onStatusUpdate() { + + // Update the DOM + document.body.classList.toggle( "aic-enabled", this.getStatus() ); + + } + + + toggleHelp() { + document.getElementById( "aic-help" )?.classList.toggle( "hide" ); + } + +//* + updateTeleprompt( descriptor:T[] ) { + + let $teleprompt = document.getElementById( "aic-teleprompt" ); + + if ( $teleprompt instanceof HTMLElement ) { + + // Clean slate + while ( $teleprompt.childNodes.length > 0 ) { + $teleprompt.childNodes[0].remove(); + } + + function newDiv() { + return document.createElement( "div" ); + } + + // Wrapper + let $descriptor = newDiv(); + $descriptor.id = "aic-descriptor"; + + for ( const section of descriptor ) { + + if ( section.omitSection ) { + continue; + } + + let $section = newDiv(); + $section.classList.add( "aic-descriptor-section" ); + + let $sectionLabel = newDiv(); + $sectionLabel.classList.add( "aic-descriptor-section-label" ); + $sectionLabel.innerText = section.label; + $section.appendChild( $sectionLabel ); + + + for ( const phrase of section.getPhrases() ) { + + let $phrase = newDiv(); + $phrase.classList.add( "aic-descriptor-phrase" ); + + for ( const component of phrase.getComponents() ) { + + let $component = newDiv(); + $component.classList.add( "aic-descriptor-component" ); + + let $componentLabel = newDiv(); + $componentLabel.classList.add( "aic-descriptor-component-label" ); + $componentLabel.innerText = component.label; + + let $componentValue = newDiv(); + $componentValue.classList.add( "aic-descriptor-component-value" ); + $componentValue.innerText = component.value; + + $component.appendChild( $componentLabel ); + $component.appendChild( $componentValue ); + + $phrase.appendChild( $component ); + + } + + $section.appendChild( $phrase ); + + } + + $descriptor.appendChild( $section ); + + } + + $teleprompt.appendChild( $descriptor ); + + } + + + } +//*/ + +} \ No newline at end of file diff --git a/client/src/aic/aicformation.ts b/client/src/aic/aicformation.ts new file mode 100644 index 00000000..588f4326 --- /dev/null +++ b/client/src/aic/aicformation.ts @@ -0,0 +1,54 @@ +import { AICFormationContextDataInterface, AICFormationDescriptor } from "./aicformationdescriptor"; +import { AICFormationDescriptorPhrase } from "./aicformationdescriptorphrase"; +import { AICFormationDescriptorSection } from "./aicformationdescriptorsection"; + +export interface AICFormationInterface { + "icon" : string, + "label" : string, + "name" : string, + "numGroups" : number, + "summary" : string, + "unitBreakdown" : string[] +} + + + +export abstract class AICFormation { + + "icon" = ""; + "label" = ""; + "name" = ""; + "numGroups" = 1; + "summary" = ""; + "unitBreakdown":string[] = [] + + + constructor() { + + this.unitBreakdown = []; + } + + + addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) { + return phrase; + } + + + getDescriptor( contextData: AICFormationContextDataInterface ) { + + return new AICFormationDescriptor().generate( this, contextData ); + + } + + + hasUnitBreakdown() { + return this.unitBreakdown.length > 0; + } + + + showFormationNameInDescriptor() { + return true; + } + + +} \ No newline at end of file diff --git a/client/src/aic/aicformation/azimuth.ts b/client/src/aic/aicformation/azimuth.ts new file mode 100644 index 00000000..7db676ae --- /dev/null +++ b/client/src/aic/aicformation/azimuth.ts @@ -0,0 +1,38 @@ +import { AICFormation, AICFormationInterface } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +export class AICFormation_Azimuth extends AICFormation implements AICFormationInterface { + + "icon" = "azimuth.png"; + "label" = "Azimuth"; + "name" = "azimuth"; + "numGroups" = 2; + "summary" = "Two contacts, side-by-side in a line perpedicular to the perspective."; + "unitBreakdown" = [ " group", " group" ]; + + constructor() { + + super(); + + } + + addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) { + + switch ( section.name ) { + + case "formation": + + phrase.addComponent( new AICFormationDescriptorComponent( "" ) ); + phrase.addComponent( new AICFormationDescriptorComponent( "track " ) ); + + } + + return phrase; + + + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformation/range.ts b/client/src/aic/aicformation/range.ts new file mode 100644 index 00000000..08c00cd5 --- /dev/null +++ b/client/src/aic/aicformation/range.ts @@ -0,0 +1,38 @@ +import { AICFormation, AICFormationInterface } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +export class AICFormation_Range extends AICFormation implements AICFormationInterface { + + "icon" = "range.png"; + "label" = "Range"; + "name" = "range"; + "numGroups" = 2; + "summary" = "Two contacts, one behind the other"; + "unitBreakdown" = [ "Lead group", "Trail group" ]; + + constructor() { + + super(); + + } + + addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) { + + switch ( section.name ) { + + case "formation": + + phrase.addComponent( new AICFormationDescriptorComponent( "" ) ); + phrase.addComponent( new AICFormationDescriptorComponent( "track " ) ); + + + } + + return phrase; + + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformation/single.ts b/client/src/aic/aicformation/single.ts new file mode 100644 index 00000000..1e2aa851 --- /dev/null +++ b/client/src/aic/aicformation/single.ts @@ -0,0 +1,24 @@ +import { AICFormation, AICFormationInterface } from "../aicformation"; +import { AICFormationContextDataInterface, AICFormationDescriptor } from "../aicformationdescriptor"; + +export class AICFormation_Single extends AICFormation implements AICFormationInterface { + + "icon" = "single.png"; + "label" = "Single"; + "name" = "single"; + "numGroups" = 1; + "summary" = "One contact on its own"; + "unitBreakdown": string[] = []; + + constructor() { + + super(); + + } + + + showFormationNameInDescriptor() { + return false; + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptor.ts b/client/src/aic/aicformationdescriptor.ts new file mode 100644 index 00000000..c7a5a563 --- /dev/null +++ b/client/src/aic/aicformationdescriptor.ts @@ -0,0 +1,55 @@ +import { AICFormation } from "./aicformation"; +import { AICFormationDescriptorSection } from "./aicformationdescriptorsection"; +import { AICFormationDescriptorSection_Formation } from "./aicformationdescriptorsection/formation"; +import { AICFormationDescriptorSection_Unit } from "./aicformationdescriptorsection/unit"; +import { AICFormationDescriptorSection_NumGroups } from "./aicformationdescriptorsection/numgroups"; +import { AICFormationDescriptorSection_Who } from "./aicformationdescriptorsection/who"; + + +export interface AICFormationContextDataInterface { + "aicCallsign" : string, + "bullseyeName" : string, + "control" : "broadcast" | "tactical", + "numGroups" : number +} + + +export class AICFormationDescriptor { + + #sections:AICFormationDescriptorSection[] = [ + new AICFormationDescriptorSection_Who(), + new AICFormationDescriptorSection_NumGroups(), + new AICFormationDescriptorSection_Formation(), + new AICFormationDescriptorSection_Unit() + ] + + constructor() { + } + + + addSection( section:AICFormationDescriptorSection ) { + this.#sections.push( section ); + } + + + getSections() { + return this.#sections; + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + let output:object[] = []; + + for ( const section of this.#sections ) { + output.push( + section.generate( formation, contextData ) + ); + } + + return output; + + } + + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorcomponent.ts b/client/src/aic/aicformationdescriptorcomponent.ts new file mode 100644 index 00000000..da194576 --- /dev/null +++ b/client/src/aic/aicformationdescriptorcomponent.ts @@ -0,0 +1,18 @@ +interface ComponentInterface { + "label" : string; + "value" : string; +} + +export class AICFormationDescriptorComponent implements ComponentInterface { + + label = "(not set)"; + value = "(not set)"; + + constructor( value:any, label?:string ) { + + this.label = label || "(not set)"; + this.value = value; + + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorcomponent/distance.ts b/client/src/aic/aicformationdescriptorcomponent/distance.ts new file mode 100644 index 00000000..2a3766f3 --- /dev/null +++ b/client/src/aic/aicformationdescriptorcomponent/distance.ts @@ -0,0 +1,9 @@ +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; + +export abstract class AICFormactionDescriptorComponent_Distance extends AICFormationDescriptorComponent { + + constructor( value:string, label?:string ) { + super( value, label ); + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorcomponent/distance/range.ts b/client/src/aic/aicformationdescriptorcomponent/distance/range.ts new file mode 100644 index 00000000..ab8a58d3 --- /dev/null +++ b/client/src/aic/aicformationdescriptorcomponent/distance/range.ts @@ -0,0 +1,9 @@ +import { AICFormactionDescriptorComponent_Distance } from "../distance"; + +export class AICFormationDescriptorComponent_Distance_Range extends AICFormactionDescriptorComponent_Distance { + + constructor( value:string, label?:string ) { + super( value, label ); + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorphrase.ts b/client/src/aic/aicformationdescriptorphrase.ts new file mode 100644 index 00000000..b2a519a0 --- /dev/null +++ b/client/src/aic/aicformationdescriptorphrase.ts @@ -0,0 +1,40 @@ +import { AICFormation } from "./aicformation"; +import { AICFormationContextDataInterface } from "./aicformationdescriptor"; +import { AICFormationDescriptorComponent } from "./aicformationdescriptorcomponent"; + +export interface AICFormationDescriptorPhraseInterface { + "generate" : CallableFunction, + "label" : string, + "name" : string +} + +export class AICFormationDescriptorPhrase { + + #components : AICFormationDescriptorComponent[] = []; + label = ""; + name = ""; + + constructor() { + } + + + addComponent( component:AICFormationDescriptorComponent ) { + this.#components.push( component ); + return this; + } + + + + getComponents() { + return this.#components; + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + return this; + + } + + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorsection.ts b/client/src/aic/aicformationdescriptorsection.ts new file mode 100644 index 00000000..bcd2f3c3 --- /dev/null +++ b/client/src/aic/aicformationdescriptorsection.ts @@ -0,0 +1,40 @@ +import { AICFormation } from "./aicformation"; +import { AICFormationContextDataInterface } from "./aicformationdescriptor"; +import { AICFormationDescriptorPhrase } from "./aicformationdescriptorphrase"; + +export interface AICFormationDescriptorSectionInterface { + "generate" : CallableFunction, + "label" : string, + "name" : string, + "omitSection" : boolean +} + +export abstract class AICFormationDescriptorSection { + + #phrases : AICFormationDescriptorPhrase[] = []; + label = ""; + name = ""; + omitSection = false; + + constructor() { + } + + + addPhrase( phrase:AICFormationDescriptorPhrase ) { + this.#phrases.push( phrase ); + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + return this; + + } + + + getPhrases() { + return this.#phrases; + } + + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorsection/formation.ts b/client/src/aic/aicformationdescriptorsection/formation.ts new file mode 100644 index 00000000..c77cb4be --- /dev/null +++ b/client/src/aic/aicformationdescriptorsection/formation.ts @@ -0,0 +1,39 @@ +import { AICFormation } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +export class AICFormationDescriptorSection_Formation extends AICFormationDescriptorSection { + + label = "Formation"; + name = "formation"; + + constructor() { + + super(); + + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + if ( !formation.showFormationNameInDescriptor() ) { + this.omitSection = true; + return this; + } + + let phrase = new AICFormationDescriptorPhrase(); + + phrase.addComponent( new AICFormationDescriptorComponent( formation.label, "Formation" ) ); + + phrase = formation.addToDescriptorPhrase( this, phrase, contextData ); + + this.addPhrase( phrase ); + + + return this; + + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorsection/numgroups.ts b/client/src/aic/aicformationdescriptorsection/numgroups.ts new file mode 100644 index 00000000..81b92a86 --- /dev/null +++ b/client/src/aic/aicformationdescriptorsection/numgroups.ts @@ -0,0 +1,35 @@ +import { AICFormation } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +export class AICFormationDescriptorSection_NumGroups extends AICFormationDescriptorSection { + + label = "Groups"; + name = "numgroups"; + + constructor() { + + super(); + + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + let value = "Single group"; + + if ( contextData.numGroups > 1 ) { + value = contextData.numGroups + " groups"; + } + + let phrase = new AICFormationDescriptorPhrase(); + phrase.addComponent( new AICFormationDescriptorComponent( value, "Number of groups" ) ); + this.addPhrase( phrase ); + + return this; + + } + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorsection/unit.ts b/client/src/aic/aicformationdescriptorsection/unit.ts new file mode 100644 index 00000000..749fe625 --- /dev/null +++ b/client/src/aic/aicformationdescriptorsection/unit.ts @@ -0,0 +1,83 @@ +import { AICFormation } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +interface addUnitInformationInterface { + omitTrack?: boolean +} + +export class AICFormationDescriptorSection_Unit extends AICFormationDescriptorSection { + + label = "Unit"; + name = "unit"; + + constructor() { + + super(); + + } + + + addUnitInformation( formation:AICFormation, contextData: AICFormationContextDataInterface, phrase: AICFormationDescriptorPhrase, options?:addUnitInformationInterface ) { + + options = options || {}; + + const originPoint = ( contextData.control === "broadcast" ) ? contextData.bullseyeName : "BRAA"; + + phrase.addComponent( new AICFormationDescriptorComponent( originPoint, "Bearing origin point" ) ); + phrase.addComponent( new AICFormationDescriptorComponent( "", "Bearing" ) ); + phrase.addComponent( new AICFormationDescriptorComponent( "", "Range" ) ); + phrase.addComponent( new AICFormationDescriptorComponent( "", "Altitude" ) ); + + if ( contextData.control === "broadcast" ) { + if ( !options.hasOwnProperty( "omitTrack" ) || options.omitTrack !== true ) { + phrase.addComponent( new AICFormationDescriptorComponent( "track ", "Tracking" ) ); + } + } else { + phrase.addComponent( new AICFormationDescriptorComponent( "[hot|flanking [left|right]|beam |cold]", "Azimuth" ) ); + } + + return phrase; + + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + if ( formation.hasUnitBreakdown() ) { + + for ( const [ i, unitRef ] of formation.unitBreakdown.entries() ) { + + let phrase = new AICFormationDescriptorPhrase(); + + phrase.addComponent( new AICFormationDescriptorComponent( unitRef, "Unit reference" ) ); + + if ( i === 0 ) { + this.addUnitInformation( formation, contextData, phrase, { "omitTrack": true } ); + } else { + phrase.addComponent( new AICFormationDescriptorComponent( "" ) ); + } + + phrase.addComponent( new AICFormationDescriptorComponent( "hostile" ) ); + + this.addPhrase( phrase ); + + + } + + } else { + + this.addPhrase( + this.addUnitInformation( formation, contextData, new AICFormationDescriptorPhrase() ) + ); + + } + + return this; + + } + + +} \ No newline at end of file diff --git a/client/src/aic/aicformationdescriptorsection/who.ts b/client/src/aic/aicformationdescriptorsection/who.ts new file mode 100644 index 00000000..244b0558 --- /dev/null +++ b/client/src/aic/aicformationdescriptorsection/who.ts @@ -0,0 +1,35 @@ +import { AICFormation } from "../aicformation"; +import { AICFormationContextDataInterface } from "../aicformationdescriptor"; +import { AICFormationDescriptorSection } from "../aicformationdescriptorsection"; +import { AICFormationDescriptorComponent } from "../aicformationdescriptorcomponent"; +import { AICFormationDescriptorPhrase } from "../aicformationdescriptorphrase"; + +export class AICFormationDescriptorSection_Who extends AICFormationDescriptorSection { + + label = "Who"; + name = "who"; + + constructor() { + + super(); + + } + + + generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) { + + let phrase = new AICFormationDescriptorPhrase(); + + if ( contextData.control === "tactical" ) { + phrase.addComponent( new AICFormationDescriptorComponent( "", "Their callsign" ) ); + } + + phrase.addComponent( new AICFormationDescriptorComponent( contextData.aicCallsign, "Your callsign" ) ); + + this.addPhrase( phrase ); + + return this; + + } + +} \ No newline at end of file diff --git a/client/src/airfields/caucasus.json b/client/src/airfields/caucasus.json new file mode 100644 index 00000000..7c060874 --- /dev/null +++ b/client/src/airfields/caucasus.json @@ -0,0 +1,361 @@ +{ + "airfields": { + "Anapa-Vityazevo": { + "ICAO": "URKA", + "Elevation": "141", + "TACAN": "", + "Runways:": { + "04": { + "Mag Hdg": "034", + "Length": "9000", + "ILS": "" + }, + "22": { + "Mag Hdg": "214", + "Length": "9000", + "ILS": "" + } + } + }, + "Batumi": { + "ICAO": "UGSB", + "Elevation": "33", + "TACAN": "16X", + "Runways": { + "13": { + "Mag Hdg": "119", + "Length": "7500", + "ILS": "" + }, + "31": { + "Mag Hdg": "299", + "Length": "7500", + "ILS": "" + } + } + }, + "Beslan": { + "ICAO": "URMO", + "Elevation": "1722", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "086", + "Length": "9600", + "ILS": "110.50" + }, + "28": { + "Mag Hdg": "266", + "Length": "9600", + "ILS": "" + } + } + }, + "Gelendzhik": { + "ICAO": "URKG", + "Elevation": "72", + "TACAN": "", + "Runways": { + "01": { + "Mag Hdg": "032", + "Length": "5400", + "ILS": "" + }, + "19": { + "Mag Hdg": "212", + "Length": "5400", + "ILS": "" + } + } + }, + "Gudauta": { + "ICAO": "UG23", + "Elevation": "69", + "TACAN": "", + "Runways": { + "15": { + "Mag Hdg": "144", + "Length": "7700", + "ILS": "" + }, + "33": { + "Mag Hdg": "324", + "Length": "7700", + "ILS": "" + } + } + }, + "Kobuleti": { + "ICAO": "UG5X", + "Elevation": "69", + "TACAN": "67X", + "Runways": { + "07": { + "Mag Hdg": "063", + "Length": "7400", + "ILS": "111.50" + }, + "25": { + "Mag Hdg": "243", + "Length": "7400", + "ILS": "" + } + } + }, + "Krasnodar-Center": { + "ICAO": "URKL", + "Elevation": "98", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "079", + "Length": "7700", + "ILS": "" + }, + "27": { + "Mag Hdg": "259", + "Length": "7700", + "ILS": "" + } + } + }, + "Krasnodar-Pashkovsky": { + "ICAO": "URKK", + "Elevation": "112", + "TACAN": "", + "Runways": { + "05": { + "Mag Hdg": "039", + "Length": "9600", + "ILS": "" + }, + "23": { + "Mag Hdg": "219", + "Length": "9600", + "ILS": "" + } + } + }, + "Krymsk": { + "ICAO": "URKW", + "Elevation": "66", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "032", + "Length": "8000", + "ILS": "" + }, + "22": { + "Mag Hdg": "212", + "Length": "8000", + "ILS": "" + } + } + }, + "Kutaisi": { + "ICAO": "UGKO", + "Elevation": "148", + "TACAN": "44X", + "Runways": { + "07": { + "Mag Hdg": "067'", + "Length": "7700", + "ILS": "109.75" + }, + "25": { + "Mag Hdg": "247", + "Length": "7700", + "ILS": "" + } + } + }, + "Maykop-Khanskaya": { + "ICAO": "URKH", + "Elevation": "591", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "031", + "Length": "10100", + "ILS": "" + }, + "22": { + "Mag Hdg": "211", + "Length": "10100", + "ILS": "" + } + } + }, + "Mineralnye Vody": { + "ICAO": "URMM", + "Elevation": "1050", + "TACAN": "", + "Runways": { + "12": { + "Mag Hdg": "108", + "Length": "12700", + "ILS": "111.70" + }, + "30": { + "Mag Hdg": "288", + "Length": "12700", + "ILS": "109.30" + } + } + }, + "Mozdok": { + "ICAO": "XRMF", + "Elevation": "507", + "TACAN": "", + "Runways": { + "08": { + "Mag Hdg": "075", + "Length": "9400", + "ILS": "" + }, + "26": { + "Mag Hdg": "255", + "Length": "9400", + "ILS": "" + } + } + }, + "Nalchick": { + "ICAO": "URMN", + "Elevation": "1411", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "048'", + "Length": "7000", + "ILS": "" + }, + "24": { + "Mag Hdg": "228", + "Length": "7000", + "ILS": "110.50" + } + } + }, + "Novorossiysk": { + "ICAO": "URKN", + "Elevation": "131", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "034", + "Length": "5400", + "ILS": "" + }, + "22": { + "Mag Hdg": "214", + "Length": "5400", + "ILS": "" + } + } + }, + "Senaki-Kolkhi": { + "ICAO": "UGKS", + "Elevation": "43", + "TACAN": "31X", + "Runways": { + "09": { + "Mag Hdg": "088'", + "Length": "7400", + "ILS": "108.90" + }, + "27": { + "Mag Hdg": "268", + "Length": "7400", + "ILS": "" + } + } + }, + "Sochi-Adler": { + "ICAO": "URSS", + "Elevation": "98", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "055", + "Length": "9700", + "ILS": "111.10" + }, + "27": { + "Mag Hdg": "235", + "Length": "9700", + "ILS": "" + } + } + }, + "Tbilisi-Lochini": { + "ICAO": "UGTB", + "Elevation": "1574", + "TACAN": "25X", + "Runways": { + "13": { + "Mag Hdg": "121", + "Length": "9300", + "ILS": "110.30" + }, + "31": { + "Mag Hdg": "301", + "Length": "9300", + "ILS": "108.90" + } + } + }, + "Soganlug": { + "ICAO": "UG24", + "Elevation": "1500", + "TACAN": "25X", + "Runways": { + "14": { + "Mag Hdg": "125", + "Length": "6500", + "ILS": "" + }, + "32": { + "Mag Hdg": "305", + "Length": "6500", + "ILS": "" + } + } + }, + "Sukhumi-Babushara": { + "ICAO": "UGSS", + "Elevation": "43", + "TACAN": "", + "Runways": { + "12": { + "Mag Hdg": "109", + "Length": "11400", + "ILS": "" + }, + "30": { + "Mag Hdg": "289", + "Length": "11400", + "ILS": "" + } + } + }, + "Vaziani": { + "ICAO": "UG27", + "Elevation": "1524", + "TACAN": "22X", + "Runways": { + "13": { + "Mag Hdg": "129", + "Length": "7700", + "ILS": "108.75" + }, + "31": { + "Mag Hdg": "309", + "Length": "7700", + "ILS": "108.75" + } + } + } + } +} diff --git a/client/src/airfields/marianas.json b/client/src/airfields/marianas.json new file mode 100644 index 00000000..3451c203 --- /dev/null +++ b/client/src/airfields/marianas.json @@ -0,0 +1,170 @@ +{ + "airfields": { + "Andersen_AFB": { + "ICAO": "PGUA", + "Elevation": "606", + "TACAN": "54X", + "Runways:": { + "06L": { + "Mag Hdg": "066", + "Length": "10300", + "ILS": "109.30" + }, + "24R": { + "Mag Hdg": "246", + "Length": "10300", + "ILS": "109.35" + }, + "06R": { + "Mag Hdg": "066", + "Length": "10900", + "ILS": "110.10" + }, + "24l": { + "Mag Hdg": "246", + "Length": "10900", + "ILS": "110.15" + } + } + }, + "Antonio_B._Won_Pat_Int_Airport": { + "ICAO": "PGUM", + "Elevation": "255", + "TACAN": "105X", + "Runways:": { + "06L": { + "Mag Hdg": "066", + "Length": "10600", + "ILS": "110.30" + }, + "24R": { + "Mag Hdg": "245", + "Length": "10600", + "ILS": "" + }, + "06R": { + "Mag Hdg": "066", + "Length": "8600", + "ILS": "110.90" + }, + "24L": { + "Mag Hdg": "245", + "Length": "8600", + "ILS": "" + } + } + }, + "North_West_Field": { + "ICAO": "", + "Elevation": "522", + "TACAN": "", + "Runways:": { + "06": { + "Mag Hdg": "063", + "Length": "4500", + "ILS": "" + }, + "24": { + "Mag Hdg": "243", + "Length": "4500", + "ILS": "" + } + } + }, + "Olf_Orote_Field": { + "ICAO": "", + "Elevation": "94", + "TACAN": "", + "Runways:": { + "07": { + "Mag Hdg": "067", + "Length": "3500", + "ILS": "" + }, + "25": { + "Mag Hdg": "247", + "Length": "3500", + "ILS": "" + } + } + }, + "Pagan_Airstrip": { + "ICAO": "", + "Elevation": "49", + "TACAN": "", + "Runways:": { + "11": { + "Mag Hdg": "112", + "Length": "1800", + "ILS": "" + }, + "29": { + "Mag Hdg": "292", + "Length": "1800", + "ILS": "" + } + } + }, + "Rota_Int": { + "ICAO": "PGRO", + "Elevation": "569", + "TACAN": "", + "Runways:": { + "09": { + "Mag Hdg": "092", + "Length": "6600", + "ILS": "" + }, + "27": { + "Mag Hdg": "272", + "Length": "6600", + "ILS": "" + } + } + }, + "Saipan_Int": { + "ICAO": "PGSN", + "Elevation": "213", + "TACAN": "", + "Runways:": { + "06": { + "Mag Hdg": "068", + "Length": "6200", + "ILS": "" + }, + "24": { + "Mag Hdg": "248", + "Length": "6200", + "ILS": "" + }, + "07": { + "Mag Hdg": "068", + "Length": "10600", + "ILS": "109.90" + }, + "25": { + "Mag Hdg": "248", + "Length": "10600", + "ILS": "" + } + } + }, + "Tinian_Int": { + "ICAO": "PGWT", + "Elevation": "284", + "TACAN": "", + "Runways:": { + "08": { + "Mag Hdg": "079", + "Length": "8200", + "ILS": "" + }, + "26": { + "Mag Hdg": "259", + "Length": "8200", + "ILS": "" + } + } + } + } +} \ No newline at end of file diff --git a/client/src/airfields/nevada.json b/client/src/airfields/nevada.json new file mode 100644 index 00000000..e94d1d4d --- /dev/null +++ b/client/src/airfields/nevada.json @@ -0,0 +1,413 @@ +{ + "airfields": { + "BeattyAirport": { + "ICAO": "KBTY", + "Elevation": "3173", + "TACAN": "94X", + "Runways:": { + "16": { + "Mag Hdg": "168", + "Length": "5500", + "ILS": "" + }, + "34": { + "Mag Hdg": "348", + "Length": "5500", + "ILS": "" + } + } + }, + "BoulderCityAirport": { + "ICAO": "KBVU", + "Elevation": "2205", + "TACAN": "114X", + "Runways:": { + "09": { + "Mag Hdg": "087", + "Length": "4400", + "ILS": "" + }, + "27": { + "Mag Hdg": "267", + "Length": "4400", + "ILS": "" + }, + "15": { + "Mag Hdg": "153", + "Length": "3700", + "ILS": "" + }, + "33": { + "Mag Hdg": "333", + "Length": "3700", + "ILS": "" + } + } + }, + "Creech": { + "ICAO": "KINS", + "Elevation": "3126", + "TACAN": "87X", + "Runways:": { + "08": { + "Mag Hdg": "080", + "Length": "8700", + "ILS": "108.70" + }, + "26": { + "Mag Hdg": "260", + "Length": "8700", + "ILS": "" + }, + "13": { + "Mag Hdg": "134", + "Length": "4700", + "ILS": "" + }, + "31": { + "Mag Hdg": "314", + "Length": "4700", + "ILS": "" + } + } + }, + "EchoBayAirport": { + "ICAO": "0L9", + "Elevation": "1549", + "TACAN": "", + "Runways:": { + "06": { + "Mag Hdg": "066", + "Length": "3300", + "ILS": "" + }, + "24": { + "Mag Hdg": "246", + "Length": "3300", + "ILS": "" + } + } + }, + "groom": { + "ICAO": "KXTA", + "Elevation": "4495", + "TACAN": "18X", + "Runways:": { + "14L": { + "Mag Hdg": "145", + "Length": "11700", + "ILS": "" + }, + "32R": { + "Mag Hdg": "325", + "Length": "11700", + "ILS": "109.30" + }, + "14R (CLOSED)": { + "Mag Hdg": "145", + "Length": "17800", + "ILS": "" + }, + "32L (CLOSED)": { + "Mag Hdg": "325", + "Length": "17800", + "ILS": "" + } + } + }, + "HendersonExecutiveAirport": { + "ICAO": "KHND", + "Elevation": "2493", + "TACAN": "", + "Runways:": { + "17L": { + "Mag Hdg": "168", + "Length": "4600", + "ILS": "" + }, + "35R": { + "Mag Hdg": "348", + "Length": "4600", + "ILS": "" + }, + "17R": { + "Mag Hdg": "168", + "Length": "6100", + "ILS": "" + }, + "35L": { + "Mag Hdg": "348", + "Length": "6100", + "ILS": "" + } + } + }, + "JeanAirport": { + "ICAO": "", + "Elevation": "2825", + "TACAN": "", + "Runways:": { + "02L": { + "Mag Hdg": "020", + "Length": "4500", + "ILS": "" + }, + "20R": { + "Mag Hdg": "200", + "Length": "4500", + "ILS": "" + }, + "02R": { + "Mag Hdg": "020", + "Length": "3600", + "ILS": "" + }, + "20L": { + "Mag Hdg": "200", + "Length": "3600", + "ILS": "" + } + } + }, + "LasVegas": { + "ICAO": "KLAS", + "Elevation": "2178", + "TACAN": "116X", + "Runways:": { + "01L": { + "Mag Hdg": "013", + "Length": "8000", + "ILS": "" + }, + "19R": { + "Mag Hdg": "193", + "Length": "8000", + "ILS": "" + }, + "01R": { + "Mag Hdg": "013", + "Length": "8000", + "ILS": "" + }, + "19L": { + "Mag Hdg": "193", + "Length": "8000", + "ILS": "" + }, + "07L": { + "Mag Hdg": "078", + "Length": "10600", + "ILS": "" + }, + "25R": { + "Mag Hdg": "258", + "Length": "10600", + "ILS": "110.30" + }, + "07R": { + "Mag Hdg": "078", + "Length": "10100", + "ILS": "" + }, + "25L": { + "Mag Hdg": "258", + "Length": "10100", + "ILS": "" + } + } + }, + "LaughlinAirport": { + "ICAO": "KIFP", + "Elevation": "673", + "TACAN": "", + "Runways:": { + "16": { + "Mag Hdg": "164", + "Length": "7100", + "ILS": "" + }, + "34": { + "Mag Hdg": "344", + "Length": "7100", + "ILS": "" + } + } + }, + "LincolnCountyAirport": { + "ICAO": "", + "Elevation": "4816", + "TACAN": "", + "Runways:": { + "17": { + "Mag Hdg": "170", + "Length": "4500", + "ILS": "" + }, + "35": { + "Mag Hdg": "350", + "Length": "4500", + "ILS": "" + } + } + }, + "MesquiteAirport": { + "ICAO": "67L", + "Elevation": "1859", + "TACAN": "", + "Runways:": { + "01": { + "Mag Hdg": "017", + "Length": "5000", + "ILS": "" + }, + "19": { + "Mag Hdg": "197", + "Length": "5000", + "ILS": "" + } + } + }, + "MinahAirport_3Q0": { + "ICAO": "", + "Elevation": "4560", + "TACAN": "", + "Runways:": { + "13": { + "Mag Hdg": "140", + "Length": "4100", + "ILS": "" + }, + "31": { + "Mag Hdg": "320", + "Length": "4100", + "ILS": "" + } + } + }, + "nellis": { + "ICAO": "KLSV", + "Elevation": "1849", + "TACAN": "12X", + "Runways:": { + "03L": { + "Mag Hdg": "029", + "Length": "9800", + "ILS": "" + }, + "21R": { + "Mag Hdg": "209", + "Length": "9800", + "ILS": "" + }, + "03R": { + "Mag Hdg": "029", + "Length": "9800", + "ILS": "" + }, + "21L": { + "Mag Hdg": "209", + "Length": "9800", + "ILS": "109.10" + } + } + }, + "NorthLasVegasAirport": { + "ICAO": "KVGT", + "Elevation": "2228", + "TACAN": "", + "Runways:": { + "07": { + "Mag Hdg": "076", + "Length": "4900", + "ILS": "" + }, + "25": { + "Mag Hdg": "256", + "Length": "4900", + "ILS": "" + }, + "12L": { + "Mag Hdg": "122", + "Length": "3800", + "ILS": "110.70" + }, + "30R": { + "Mag Hdg": "302", + "Length": "3800", + "ILS": "109.10" + }, + "12R": { + "Mag Hdg": "122", + "Length": "4600", + "ILS": "" + }, + "30L": { + "Mag Hdg": "302", + "Length": "4600", + "ILS": "" + } + } + }, + "PahuteMesaAirstrip": { + "ICAO": "", + "Elevation": "5059", + "TACAN": "", + "Runways:": { + "18": { + "Mag Hdg": "182", + "Length": "5500", + "ILS": "" + }, + "36": { + "Mag Hdg": "002", + "Length": "5500", + "ILS": "" + } + } + }, + "TonopahAirport": { + "ICAO": "KTPH", + "Elevation": "5390", + "TACAN": "119X", + "Runways:": { + "11": { + "Mag Hdg": "113", + "Length": "5600", + "ILS": "" + }, + "29": { + "Mag Hdg": "293", + "Length": "5600", + "ILS": "" + }, + "15": { + "Mag Hdg": "153", + "Length": "6800", + "ILS": "" + }, + "33": { + "Mag Hdg": "333", + "Length": "6800", + "ILS": "" + } + } + }, + "TonopathAFB": { + "ICAO": "KTNX", + "Elevation": "5535", + "TACAN": "77X", + "Runways:": { + "14": { + "Mag Hdg": "145", + "Length": "11700", + "ILS": "108.30" + }, + "32": { + "Mag Hdg": "325", + "Length": "11700", + "ILS": "111.70" + } + } + } + } +} \ No newline at end of file diff --git a/client/src/airfields/persiangulf.json b/client/src/airfields/persiangulf.json new file mode 100644 index 00000000..81c3be69 --- /dev/null +++ b/client/src/airfields/persiangulf.json @@ -0,0 +1,567 @@ +{ + "airfields": { + "Abu_Dhabi_International_Airport": { + "ICAO": "OMAA", + "Elevation": "92", + "TACAN": "", + "Runways:": { + "13L": { + "Mag Hdg": "127", + "Length": "13100", + "ILS": "" + }, + "13R": { + "Mag Hdg": "127", + "Length": "13200", + "ILS": "" + }, + "31L": { + "Mag Hdg": "307", + "Length": "13100", + "ILS": "" + }, + "31R": { + "Mag Hdg": "307", + "Length": "13200", + "ILS": "" + } + } + }, + "Ai_Ain_International_Airport": { + "ICAO": "OMAL", + "Elevation": "814", + "TACAN": "", + "Runways:": { + "01": { + "Mag Hdg": "006", + "Length": "12800", + "ILS": "" + }, + "19": { + "Mag Hdg": "186", + "Length": "12800", + "ILS": "" + } + } + }, + "abu_musa_airport": { + "ICAO": "OIBA", + "Elevation": "16", + "TACAN": "", + "Runways:": { + "08": { + "Mag Hdg": "082", + "Length": "7800", + "ILS": "" + }, + "26": { + "Mag Hdg": "262", + "Length": "7800", + "ILS": "" + } + } + }, + "Dhafra_AFB": { + "ICAO": "OMAM", + "Elevation": "52", + "TACAN": "96X", + "Runways:": { + "13L": { + "Mag Hdg": "126", + "Length": "11700", + "ILS": "111.10" + }, + "31R": { + "Mag Hdg": "306", + "Length": "11700", + "ILS": "109.10" + }, + "13R": { + "Mag Hdg": "16", + "Length": "11700", + "ILS": "108.70" + }, + "31L": { + "Mag Hdg": "306", + "Length": "11700", + "ILS": "108.70" + } + } + }, + "Al_Maktoum_International_Airport": { + "ICAO": "OMDW", + "Elevation": "125", + "TACAN": "", + "Runways:": { + "12": { + "Mag Hdg": "120", + "Length": "14400", + "ILS": "111.75" + }, + "30": { + "Mag Hdg": "300", + "Length": "14400", + "ILS": "109.75" + } + } + }, + "Minhad_AFB": { + "ICAO": "OMDM", + "Elevation": "190", + "TACAN": "99X", + "Runways:": { + "09": { + "Mag Hdg": "088", + "Length": "12600", + "ILS": "110.70" + }, + "27": { + "Mag Hdg": "268", + "Length": "12600", + "ILS": "110.75" + } + } + }, + "Al_Bateen_Airport": { + "ICAO": "OMAD", + "Elevation": "12", + "TACAN": "", + "Runways:": { + "13": { + "Mag Hdg": "127", + "Length": "7000", + "ILS": "" + }, + "31": { + "Mag Hdg": "307", + "Length": "7000", + "ILS": "" + } + } + }, + "Bandar_Abbas_airfield": { + "ICAO": "OIKB", + "Elevation": "29", + "TACAN": "78X", + "Runways:": { + "03L": { + "Mag Hdg": "25", + "Length": "11000", + "ILS": "" + }, + "21R": { + "Mag Hdg": "205", + "Length": "10000", + "ILS": "" + }, + "03R": { + "Mag Hdg": "25", + "Length": "11700", + "ILS": "" + }, + "21L": { + "Mag Hdg": "205", + "Length": "11700", + "ILS": "109.90" + } + } + }, + "Bandar_Lengeh_Airport": { + "ICAO": "OIBL", + "Elevation": "82", + "TACAN": "", + "Runways:": { + "08": { + "Mag Hdg": "079", + "Length": "7900", + "ILS": "" + }, + "26": { + "Mag Hdg": "259", + "Length": "7900", + "ILS": "" + } + } + }, + "Bandar_e_Jask_airfield": { + "ICAO": "OIZJ", + "Elevation": "26", + "TACAN": "110X", + "Runways:": { + "06": { + "Mag Hdg": "059", + "Length": "7300", + "ILS": "" + }, + "24": { + "Mag Hdg": "239", + "Length": "7300", + "ILS": "" + } + } + }, + "Dubai_International_Airport": { + "ICAO": "OMDB", + "Elevation": "16", + "TACAN": "", + "Runways:": { + "12L": { + "Mag Hdg": "120", + "Length": "11400", + "ILS": "110.10" + }, + "30R": { + "Mag Hdg": "300", + "Length": "11400", + "ILS": "110.90" + }, + "12R": { + "Mag Hdg": "120", + "Length": "11400", + "ILS": "109.50" + }, + "30L": { + "Mag Hdg": "300", + "Length": "11400", + "ILS": "111.30" + } + } + }, + "Fujarirah_AFB": { + "ICAO": "OMFJ", + "Elevation": "121", + "TACAN": "", + "Runways:": { + "11": { + "Mag Hdg": "111", + "Length": "9700", + "ILS": "" + }, + "29": { + "Mag Hdg": "291", + "Length": "9700", + "ILS": "111.50" + } + } + }, + "Havadarya_AFB": { + "ICAO": "OIKP", + "Elevation": "52", + "TACAN": "47X", + "Runways:": { + "08": { + "Mag Hdg": "077", + "Length": "7200", + "ILS": "108.90" + }, + "26": { + "Mag Hdg": "257", + "Length": "7200", + "ILS": "" + } + } + }, + "Jiroft_airfield": { + "ICAO": "OIKJ", + "Elevation": "2664", + "TACAN": "", + "Runways:": { + "13": { + "Mag Hdg": "125", + "Length": "9600", + "ILS": "" + }, + "31": { + "Mag Hdg": "305", + "Length": "9600", + "ILS": "" + } + } + }, + "Kerman_AFB": { + "ICAO": "OIKK", + "Elevation": "5745", + "TACAN": "97X", + "Runways:": { + "16": { + "Mag Hdg": "155", + "Length": "12400", + "ILS": "" + }, + "34": { + "Mag Hdg": "335", + "Length": "12400", + "ILS": "" + } + } + }, + "Khasab_AFB": { + "ICAO": "OOKB", + "Elevation": "102", + "TACAN": "", + "Runways:": { + "01": { + "Mag Hdg": "012", + "Length": "8000", + "ILS": "" + }, + "19": { + "Mag Hdg": "192", + "Length": "8000", + "ILS": "110.30" + } + } + }, + "Kish_International_Airport": { + "ICAO": "OIBK", + "Elevation": "115", + "TACAN": "112X", + "Runways:": { + "10": { + "Mag Hdg": "094", + "Length": "11700", + "ILS": "" + }, + "28": { + "Mag Hdg": "274", + "Length": "11700", + "ILS": "" + }, + "09R": { + "Mag Hdg": "094", + "Length": "11700", + "ILS": "" + }, + "27L": { + "Mag Hdg": "274", + "Length": "11700", + "ILS": "" + } + } + }, + "Lar_airbase": { + "ICAO": "OISL", + "Elevation": "2635", + "TACAN": "", + "Runways:": { + "09": { + "Mag Hdg": "088", + "Length": "10100", + "ILS": "" + }, + "27": { + "Mag Hdg": "268", + "Length": "10100", + "ILS": "" + } + } + }, + "Lavan_Island_Airport": { + "ICAO": "OIBV", + "Elevation": "75", + "TACAN": "", + "Runways:": { + "11": { + "Mag Hdg": "110", + "Length": "8600", + "ILS": "" + }, + "29": { + "Mag Hdg": "290", + "Length": "8600", + "ILS": "" + } + } + }, + "Liwa_Airbase": { + "ICAO": "OMLW", + "Elevation": "400", + "TACAN": "121X", + "Runways:": { + "13": { + "Mag Hdg": "130", + "Length": "11600", + "ILS": "" + }, + "31": { + "Mag Hdg": "310", + "Length": "11600", + "ILS": "" + } + } + }, + "Qeshm_Airport": { + "ICAO": "OIKQ", + "Elevation": "26", + "TACAN": "", + "Runways:": { + "05": { + "Mag Hdg": "047", + "Length": "13600", + "ILS": "" + }, + "23": { + "Mag Hdg": "227", + "Length": "13600", + "ILS": "" + } + } + }, + "Ras_Ai_Khaimah_International_Airport": { + "ICAO": "OMRK", + "Elevation": "330", + "TACAN": "", + "Runways:": { + "17": { + "Mag Hdg": "163", + "Length": "12000", + "ILS": "" + }, + "35": { + "Mag Hdg": "343", + "Length": "12000", + "ILS": "" + } + } + }, + "Sas_Ai_Nakheel_Airport": { + "ICAO": "OMNK", + "Elevation": "10", + "TACAN": "", + "Runways:": { + "16": { + "Mag Hdg": "160", + "Length": "6000", + "ILS": "" + }, + "34": { + "Mag Hdg": "340", + "Length": "6000", + "ILS": "" + } + } + }, + "Sharjah_International_Airport": { + "ICAO": "OMSJ", + "Elevation": "26", + "TACAN": "", + "Runways:": { + "12L": { + "Mag Hdg": "121", + "Length": "10500", + "ILS": "108.55" + }, + "30R": { + "Mag Hdg": "301", + "Length": "10500", + "ILS": "111.95" + }, + "12R": { + "Mag Hdg": "121", + "Length": "10500", + "ILS": "" + }, + "30L": { + "Mag Hdg": "301", + "Length": "10500", + "ILS": "" + } + } + }, + "Shiraz_AFB": { + "ICAO": "OISS", + "Elevation": "4879", + "TACAN": "94X", + "Runways:": { + "11L": { + "Mag Hdg": "113", + "Length": "14000", + "ILS": "" + }, + "29R": { + "Mag Hdg": "293", + "Length": "14000", + "ILS": "" + }, + "11R": { + "Mag Hdg": "113", + "Length": "13800", + "ILS": "" + }, + "29L": { + "Mag Hdg": "293", + "Length": "13800", + "ILS": "108.50" + } + } + }, + "Sir_Abu_Nuayr": { + "ICAO": "OMSN", + "Elevation": "26", + "TACAN": "", + "Runways:": { + "10": { + "Mag Hdg": "097", + "Length": "2300", + "ILS": "" + }, + "28": { + "Mag Hdg": "277", + "Length": "2300", + "ILS": "" + } + } + }, + "Sirri_Island_AFB": { + "ICAO": "OIBS", + "Elevation": "20", + "TACAN": "", + "Runways:": { + "12": { + "Mag Hdg": "125", + "Length": "7900", + "ILS": "" + }, + "30": { + "Mag Hdg": "305", + "Length": "7900", + "ILS": "" + } + } + }, + "Tunb_Islab_AFB": { + "ICAO": "OIGI", + "Elevation": "43", + "TACAN": "", + "Runways:": { + "03": { + "Mag Hdg": "025", + "Length": "6200", + "ILS": "" + }, + "21": { + "Mag Hdg": "205", + "Length": "6200", + "ILS": "" + } + } + }, + "Tonb_e_Kochak_Airport": { + "ICAO": "OITK", + "Elevation": "16", + "TACAN": "89X", + "Runways:": { + "08": { + "Mag Hdg": "079", + "Length": "2500", + "ILS": "" + }, + "26": { + "Mag Hdg": "259", + "Length": "2500", + "ILS": "" + } + } + } + } +} diff --git a/client/src/airfields/syria.json b/client/src/airfields/syria.json new file mode 100644 index 00000000..b48949b7 --- /dev/null +++ b/client/src/airfields/syria.json @@ -0,0 +1,995 @@ +{ + "airfields": { + "Abu_al-Duhur": { + "ICAO": "OS57", + "Elevation": "820", + "TACAN": "", + "Runways:": { + "09": { + "Mag Hdg": "088", + "Length": "9200", + "ILS": "" + }, + "27": { + "Mag Hdg": "268", + "Length": "9200", + "ILS": "" + } + } + }, + "Adana": { + "ICAO": "LTAF", + "Elevation": "56", + "TACAN": "", + "Runways": { + "05": { + "Mag Hdg": "050", + "Length": "8800", + "ILS": "108.70" + }, + "23": { + "Mag Hdg": "230", + "Length": "8800", + "ILS": "" + } + } + }, + "Akrotiri": { + "ICAO": "LCRA", + "Elevation": "69", + "TACAN": "107X", + "Runways": { + "10": { + "Mag Hdg": "106", + "Length": "8800", + "ILS": "" + }, + "28": { + "Mag Hdg": "286", + "Length": "8800", + "ILS": "109.70" + } + } + }, + "Al_Qusayr": { + "ICAO": "OS70", + "Elevation": "1729", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "096", + "Length": "9500", + "ILS": "" + }, + "28": { + "Mag Hdg": "276", + "Length": "9500", + "ILS": "" + } + } + }, + "Dumayr": { + "ICAO": "OS61", + "Elevation": "2067", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "060", + "Length": "9500", + "ILS": "" + }, + "24": { + "Mag Hdg": "240", + "Length": "9500", + "ILS": "" + } + } + }, + "Aleppo": { + "ICAO": "OSAP", + "Elevation": "1254", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "092", + "Length": "9200", + "ILS": "" + }, + "27": { + "Mag Hdg": "272", + "Length": "9200", + "ILS": "" + } + } + }, + "An_Nasiriyah": { + "ICAO": "OSAP", + "Elevation": "1254", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "092", + "Length": "9200", + "ILS": "" + }, + "27": { + "Mag Hdg": "272", + "Length": "9200", + "ILS": "" + } + } + }, + "At_Tanf": { + "ICAO": "", + "Elevation": "2329", + "TACAN": "", + "Runways": "" + }, + "Bassel_Al-Assad": { + "ICAO": "OSLK", + "Elevation": "92", + "TACAN": "", + "Runways": { + "17L": { + "Mag Hdg": "173", + "Length": "7900", + "ILS": "" + }, + "35R": { + "Mag Hdg": "353", + "Length": "7900", + "ILS": "" + }, + "17R": { + "Mag Hdg": "173", + "Length": "8900", + "ILS": "109.10" + }, + "35L": { + "Mag Hdg": "353", + "Length": "8900", + "ILS": "" + } + } + }, + "Beirut": { + "ICAO": "OLBA", + "Elevation": "39", + "TACAN": "", + "Runways": { + "03": { + "Mag Hdg": "030", + "Length": "7000", + "ILS": "110.70" + }, + "21": { + "Mag Hdg": "210", + "Length": "7000", + "ILS": "" + }, + "16": { + "Mag Hdg": "164", + "Length": "10300", + "ILS": "110.10" + }, + "34": { + "Mag Hdg": "344", + "Length": "10300", + "ILS": "" + }, + "17": { + "Mag Hdg": "174", + "Length": "7600", + "ILS": "109.50" + }, + "35": { + "Mag Hdg": "354", + "Length": "7600", + "ILS": "" + } + } + }, + "Damascus": { + "ICAO": "OSDI", + "Elevation": "2008", + "TACAN": "", + "Runways": { + "05L": { + "Mag Hdg": "045", + "Length": "11600", + "ILS": "" + }, + "23R": { + "Mag Hdg": "225", + "Length": "11600", + "ILS": "109.90" + }, + "05R": { + "Mag Hdg": "045", + "Length": "11600", + "ILS": "111.10" + }, + "23L": { + "Mag Hdg": "225", + "Length": "11600", + "ILS": "" + } + } + }, + "Deir_ez_Zor": { + "ICAO": "OSDZ", + "Elevation": "713", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "104", + "Length": "9500", + "ILS": "" + }, + "28": { + "Mag Hdg": "284", + "Length": "9500", + "ILS": "" + } + } + }, + "Ercan": { + "ICAO": "LCEN", + "Elevation": "312", + "TACAN": "", + "Runways": { + "11": { + "Mag Hdg": "110", + "Length": "8700", + "ILS": "" + }, + "29": { + "Mag Hdg": "290", + "Length": "8700", + "ILS": "108.30" + } + } + }, + "Eyn_Shemer": { + "ICAO": "LLES", + "Elevation": "110", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "095", + "Length": "4000", + "ILS": "" + }, + "27": { + "Mag Hdg": "2750", + "Length": "4000", + "ILS": "" + } + } + }, + "Gaziantep": { + "ICAO": "LTAJ", + "Elevation": "2290", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "100", + "Length": "9100", + "ILS": "" + }, + "28": { + "Mag Hdg": "280", + "Length": "9100", + "ILS": "109.10" + } + } + }, + "Gazipasa": { + "ICAO": "LTFG", + "Elevation": "130", + "TACAN": "", + "Runways": { + "08": { + "Mag Hdg": "080", + "Length": "7500", + "ILS": "108.50" + }, + "26": { + "Mag Hdg": "260", + "Length": "7500", + "ILS": "" + } + } + }, + "Gecitkale": { + "ICAO": "LCGK", + "Elevation": "148", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "088", + "Length": "9100", + "ILS": "108.50" + }, + "27": { + "Mag Hdg": "268", + "Length": "9100", + "ILS": "" + } + } + }, + "H3": { + "ICAO": "", + "Elevation": "2583", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "059", + "Length": "9800", + "ILS": "" + }, + "24": { + "Mag Hdg": "239", + "Length": "9800", + "ILS": "" + }, + "11": { + "Mag Hdg": "107", + "Length": "9500", + "ILS": "" + }, + "29": { + "Mag Hdg": "287", + "Length": "9500", + "ILS": "" + } + } + }, + "H3_Northwest": { + "ICAO": "", + "Elevation": "2582", + "TACAN": "", + "Runways": { + "12": { + "Mag Hdg": "117", + "Length": "8000", + "ILS": "" + }, + "30": { + "Mag Hdg": "297", + "Length": "8000", + "ILS": "" + } + } + }, + "H3_Southwest": { + "ICAO": "", + "Elevation": "2671", + "TACAN": "", + "Runways": { + "12": { + "Mag Hdg": "116", + "Length": "8000", + "ILS": "" + }, + "30": { + "Mag Hdg": "296", + "Length": "8000", + "ILS": "" + } + } + }, + "H4": { + "ICAO": "OJHR", + "Elevation": "2257", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "098", + "Length": "8000", + "ILS": "" + }, + "28": { + "Mag Hdg": "278", + "Length": "8000", + "ILS": "" + } + } + }, + "Haifa": { + "ICAO": "LLHA", + "Elevation": "20", + "TACAN": "", + "Runways": { + "16": { + "Mag Hdg": "157", + "Length": "3300", + "ILS": "" + }, + "34": { + "Mag Hdg": "337", + "Length": "3300", + "ILS": "" + } + } + }, + "Hama": { + "ICAO": "OS58", + "Elevation": "984", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "095", + "Length": "8600", + "ILS": "" + }, + "27": { + "Mag Hdg": "275", + "Length": "8600", + "ILS": "" + } + } + }, + "Hatay": { + "ICAO": "LTDA", + "Elevation": "253", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "039", + "Length": "9600", + "ILS": "108.90" + }, + "22": { + "Mag Hdg": "219", + "Length": "9600", + "ILS": "" + } + } + }, + "Incirlik": { + "ICAO": "LTAG", + "Elevation": "230", + "TACAN": "21X", + "Runways": { + "05": { + "Mag Hdg": "049", + "Length": "9600", + "ILS": "109.30" + }, + "23": { + "Mag Hdg": "229", + "Length": "9500", + "ILS": "111.70" + } + } + }, + "Jirah": { + "ICAO": "OS62", + "Elevation": "1173", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "095", + "Length": "9600", + "ILS": "" + }, + "28": { + "Mag Hdg": "275", + "Length": "9600", + "ILS": "" + } + } + }, + "Khalkhalah": { + "ICAO": "OS69", + "Elevation": "2418", + "TACAN": "", + "Runways": { + "07": { + "Mag Hdg": "071", + "Length": "9500", + "ILS": "" + }, + "25": { + "Mag Hdg": "251", + "Length": "9500", + "ILS": "" + }, + "15": { + "Mag Hdg": "146", + "Length": "8300", + "ILS": "" + }, + "33": { + "Mag Hdg": "326", + "Length": "8300", + "ILS": "" + } + } + }, + "Kharab_Ishk": { + "ICAO": "", + "Elevation": "1416", + "TACAN": "", + "Runways": "" + }, + "King_Hussein": { + "ICAO": "OJMF", + "Elevation": "2205", + "TACAN": "", + "Runways": { + "13": { + "Mag Hdg": "127", + "Length": "9500", + "ILS": "" + }, + "31": { + "Mag Hdg": "307", + "Length": "9500", + "ILS": "" + } + } + }, + "Kingsfield": { + "ICAO": "LCRE", + "Elevation": "276", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "058", + "Length": "3300", + "ILS": "" + }, + "24": { + "Mag Hdg": "238", + "Length": "3300", + "ILS": "" + } + } + }, + "Kiryat_Shmona": { + "ICAO": "LLKS", + "Elevation": "360", + "TACAN": "", + "Runways": { + "03": { + "Mag Hdg": "033", + "Length": "3500", + "ILS": "" + }, + "21": { + "Mag Hdg": "213", + "Length": "3500", + "ILS": "" + } + } + }, + "Kuweires": { + "ICAO": "OS66", + "Elevation": "1201", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "095", + "Length": "7700", + "ILS": "" + }, + "28": { + "Mag Hdg": "275", + "Length": "7700", + "ILS": "" + } + } + }, + "Lakatamia": { + "ICAO": "", + "Elevation": "758", + "TACAN": "", + "Runways": "" + }, + "Larnaca": { + "ICAO": "LCRE", + "Elevation": "16", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "043", + "Length": "8800", + "ILS": "" + }, + "22": { + "Mag Hdg": "223", + "Length": "8800", + "ILS": "110.30" + } + } + }, + "Marj_Al_Sultan": { + "ICAO": "", + "Elevation": "2008", + "TACAN": "", + "Runways": "" + }, + "Marj_Ruhayyil": { + "ICAO": "OS63", + "Elevation": "2161", + "TACAN": "", + "Runways": { + "06L": { + "Mag Hdg": "059", + "Length": "9400", + "ILS": "" + }, + "24R": { + "Mag Hdg": "239", + "Length": "9400", + "ILS": "" + }, + "06R": { + "Mag Hdg": "059", + "Length": "8400", + "ILS": "" + }, + "24L": { + "Mag Hdg": "239", + "Length": "8400", + "ILS": "" + } + } + }, + "Megiddo": { + "ICAO": "LLMG", + "Elevation": "180", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "088", + "Length": "6200", + "ILS": "" + }, + "27": { + "Mag Hdg": "268", + "Length": "6200", + "ILS": "" + } + } + }, + "Mezzeh": { + "ICAO": "OS67", + "Elevation": "2387", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "056", + "Length": "8800", + "ILS": "" + }, + "24": { + "Mag Hdg": "236", + "Length": "8800", + "ILS": "" + } + } + }, + "Minakh": { + "ICAO": "OS71", + "Elevation": "1614", + "TACAN": "", + "Runways": { + "10": { + "Mag Hdg": "096", + "Length": "4500", + "ILS": "" + }, + "28": { + "Mag Hdg": "276", + "Length": "4500", + "ILS": "" + } + } + }, + "Naqoura": { + "ICAO": "", + "Elevation": "381", + "TACAN": "", + "Runways": "" + }, + "Palmyra": { + "ICAO": "OSPR", + "Elevation": "1325", + "TACAN": "", + "Runways": { + "08": { + "Mag Hdg": "079", + "Length": "9200", + "ILS": "" + }, + "26": { + "Mag Hdg": "259", + "Length": "9200", + "ILS": "" + } + } + }, + "Paphos": { + "ICAO": "LCPH", + "Elevation": "40", + "TACAN": "79X", + "Runways": { + "11": { + "Mag Hdg": "109", + "Length": "8600", + "ILS": "" + }, + "29": { + "Mag Hdg": "289", + "Length": "8600", + "ILS": "108.90" + } + } + }, + "Qabr_al_Sitt": { + "ICAO": "", + "Elevation": "2135", + "TACAN": "", + "Runways": "" + }, + "Ramat_David": { + "ICAO": "LLRD", + "Elevation": "146", + "TACAN": "84X", + "Runways": { + "09": { + "Mag Hdg": "084", + "Length": "7600", + "ILS": "" + }, + "27": { + "Mag Hdg": "264", + "Length": "7600", + "ILS": "" + }, + "11": { + "Mag Hdg": "105", + "Length": "7700", + "ILS": "" + }, + "29": { + "Mag Hdg": "285", + "Length": "7700", + "ILS": "" + }, + "15": { + "Mag Hdg": "141", + "Length": "7700", + "ILS": "" + }, + "33": { + "Mag Hdg": "321", + "Length": "7700", + "ILS": "111.10" + } + } + }, + "Rayak": { + "ICAO": "OLRA", + "Elevation": "3035", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "042", + "Length": "9400", + "ILS": "" + }, + "22": { + "Mag Hdg": "222", + "Length": "9400", + "ILS": "" + } + } + }, + "Rene_Mouawad": { + "ICAO": "OLKA", + "Elevation": "23", + "TACAN": "", + "Runways": { + "06": { + "Mag Hdg": "058", + "Length": "9000", + "ILS": "" + }, + "24": { + "Mag Hdg": "238", + "Length": "9000", + "ILS": "" + } + } + }, + "Rosh_Pina": { + "ICAO": "LLIB", + "Elevation": "914", + "TACAN": "", + "Runways": { + "05": { + "Mag Hdg": "049", + "Length": "3200", + "ILS": "" + }, + "23": { + "Mag Hdg": "229", + "Length": "3200", + "ILS": "" + }, + "15": { + "Mag Hdg": "147", + "Length": "2900", + "ILS": "" + }, + "33": { + "Mag Hdg": "327", + "Length": "2900", + "ILS": "" + } + } + }, + "Ruwayshid": { + "ICAO": "", + "Elevation": "2980", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "091", + "Length": "7000", + "ILS": "" + }, + "27": { + "Mag Hdg": "271", + "Length": "7000", + "ILS": "" + } + } + }, + "Sanliurfa": { + "ICAO": "LTCS", + "Elevation": "2703", + "TACAN": "", + "Runways": { + "04": { + "Mag Hdg": "036", + "Length": "12900", + "ILS": "" + }, + "22": { + "Mag Hdg": "216", + "Length": "12900", + "ILS": "" + } + } + }, + "Sayqal": { + "ICAO": "OS68", + "Elevation": "2274", + "TACAN": "", + "Runways": { + "05": { + "Mag Hdg": "055", + "Length": "7600", + "ILS": "" + }, + "23": { + "Mag Hdg": "235", + "Length": "7600", + "ILS": "" + }, + "08": { + "Mag Hdg": "085", + "Length": "9500", + "ILS": "" + }, + "26": { + "Mag Hdg": "265", + "Length": "9500", + "ILS": "" + } + } + }, + "Shayrat": { + "ICAO": "OS65", + "Elevation": "2638", + "TACAN": "", + "Runways": { + "11": { + "Mag Hdg": "107", + "Length": "9300", + "ILS": "" + }, + "29": { + "Mag Hdg": "287", + "Length": "9300", + "ILS": "" + } + } + }, + "Tabqa": { + "ICAO": "OS59", + "Elevation": "1099", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "088", + "Length": "9300", + "ILS": "" + }, + "27": { + "Mag Hdg": "268", + "Length": "9300", + "ILS": "" + } + } + }, + "Taftanaz": { + "ICAO": "", + "Elevation": "", + "TACAN": "", + "Runways": "" + }, + "Tal_Siman": { + "ICAO": "", + "Elevation": "", + "TACAN": "", + "Runways": "" + }, + "thalah": { + "ICAO": "OS60", + "Elevation": "2414", + "TACAN": "", + "Runways": { + "05": { + "Mag Hdg": "053", + "Length": "9500", + "ILS": "" + }, + "23": { + "Mag Hdg": "233", + "Length": "9500", + "ILS": "" + } + } + }, + "Tiyas": { + "ICAO": "OS72", + "Elevation": "1798", + "TACAN": "", + "Runways": { + "09": { + "Mag Hdg": "085", + "Length": "9500", + "ILS": "" + }, + "27": { + "Mag Hdg": "265", + "Length": "9500", + "ILS": "" + } + } + }, + "Wujah_Al_Hajar": { + "ICAO": "Z190", + "Elevation": "641", + "TACAN": "", + "Runways": { + "02": { + "Mag Hdg": "024", + "Length": "4800", + "ILS": "" + }, + "20": { + "Mag Hdg": "204", + "Length": "4800", + "ILS": "" + } + } + } + } +} diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts new file mode 100644 index 00000000..1597ca7e --- /dev/null +++ b/client/src/atc/atc.ts @@ -0,0 +1,188 @@ +import { getMissionData } from ".."; +import { getConnected } from "../server/server"; +import { ATCBoard } from "./atcboard"; +import { ATCBoardGround } from "./board/ground"; +import { ATCBoardTower } from "./board/tower"; + +export interface FlightInterface { + assignedSpeed: any; + assignedAltitude : any; + id : string; + boardId : string; + name : string; + order : number; + status : "unknown"; + takeoffTime : number; + unitId : number; +} + + +class ATCDataHandler { + + #atc:ATC; + #flights:{[key:string]: FlightInterface} = {}; + + #updateInterval:number|undefined = undefined; + #updateIntervalDelay:number = 2500; // Wait between unit update requests + + + constructor( atc:ATC ) { + + this.#atc = atc; + + } + + + getFlights( boardId:string ) { + + return Object.values( this.#flights ).reduce( ( acc:{[key:string]: FlightInterface}, flight ) => { + + if ( flight.boardId === boardId ) { + acc[ flight.id ] = flight; + } + + return acc; + }, {} ); + } + + + startUpdates() { + + this.#updateInterval = window.setInterval( () => { + + if ( !getConnected() ) { + return; + } + + const aBoardIsVisible = this.#atc.getBoards().some( board => board.boardIsVisible() ); + + if ( aBoardIsVisible ) { + + fetch( '/api/atc/flight', { + method: 'GET', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + } + }) + .then( response => response.json() ) + .then( data => { + this.setFlights( data ); + }); + + } + + }, this.#updateIntervalDelay ); + + } + + + setFlights( flights:{[key:string]: any} ) { + + this.#flights = flights; + + } + + + stopUpdates() { + + clearInterval( this.#updateInterval ); + + } + +} + + + + +export class ATC { + + #boards:ATCBoard[] = []; + #dataHandler:ATCDataHandler; + + #initDate:Date = new Date(); + + constructor() { + + this.#dataHandler = new ATCDataHandler( this ); + + this.lookForBoards(); + + } + + + addBoard( board:T ) { + + board.startUpdates(); + + this.#boards.push( board ); + + } + + + getBoards() { + return this.#boards; + } + + + getDataHandler() { + return this.#dataHandler; + } + + + getMissionElapsedSeconds() : number { + return new Date().getTime() - this.#initDate.getTime(); + } + + + getMissionStartDateTime() : Date { + return new Date( 1990, 3, 1, 18, 0, 0 ); + } + + + getMissionDateTime() : Date { + return new Date( getMissionData().getNowDate() ); + } + + + lookForBoards() { + + document.querySelectorAll( ".ol-strip-board" ).forEach( board => { + + if ( board instanceof HTMLElement ) { + + switch ( board.dataset.boardType ) { + + case "ground": + this.addBoard( new ATCBoardGround( this, board ) ); + return; + + case "tower": + this.addBoard( new ATCBoardTower( this, board ) ); + return; + + default: + console.warn( "Unknown board type for ATC board, got: " + board.dataset.boardType ); + + } + + } + + }); + + } + + startUpdates() { + + this.#dataHandler.startUpdates(); + + } + + + stopUpdates() { + + this.#dataHandler.stopUpdates(); + + } + +} \ No newline at end of file diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts new file mode 100644 index 00000000..2edcbc9e --- /dev/null +++ b/client/src/atc/atcboard.ts @@ -0,0 +1,527 @@ +import { Dropdown } from "../controls/dropdown"; +import { zeroAppend } from "../other/utils"; +import { ATC } from "./atc"; +import { Unit } from "../units/unit"; +import { getMissionData, getUnitsManager } from ".."; +import Sortable from "sortablejs"; +import { FlightInterface } from "./atc"; +import { getConnected } from "../server/server"; + +export interface StripBoardStripInterface { + "id": string, + "element": HTMLElement, + "dropdowns": {[key:string]: Dropdown}, + "isDeleted"?: boolean, + "unitId": number +} + +export abstract class ATCBoard { + + #atc:ATC; + #boardId:string = ""; + #templates: {[key:string]: string} = {}; + + + // Elements + #boardElement:HTMLElement; + #clockElement:HTMLElement; + #stripBoardElement:HTMLElement; + + // Content + #isAddFlightByClickEnabled:boolean = false; + #strips:{[key:string]: StripBoardStripInterface} = {}; + #unitIdsBeingMonitored:number[] = []; + + // Update timing + #updateInterval:number|undefined = undefined; + #updateIntervalDelay:number = 1000; + + + constructor( atc:ATC, boardElement:HTMLElement, options?:{[key:string]: any} ) { + + options = options || {}; + + this.#atc = atc; + this.#boardElement = boardElement; + this.#stripBoardElement = this.getBoardElement().querySelector( ".ol-strip-board-strips" ); + this.#clockElement = this.getBoardElement().querySelector( ".ol-strip-board-clock" ); + + + new MutationObserver( () => { + if ( this.boardIsVisible() ) { + this.startUpdates(); + } else { + this.stopUpdates(); + } + }).observe( this.getBoardElement(), { + "attributes": true, + "childList": false, + "subtree": false + }); + + + new Sortable( this.getStripBoardElement(), { + "handle": ".handle", + "onUpdate": ev => { + + const order = [].slice.call( this.getStripBoardElement().children ).map( ( strip:HTMLElement ) => { + return strip.dataset.flightId + }); + + fetch( '/api/atc/flight/order', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "order" : order + }) + }); + + } + }); + + + window.setInterval( () => { + + if ( !getConnected() ) { + return; + } + this.updateClock(); + }, 1000 ); + + + if ( this.#boardElement.classList.contains( "ol-draggable" ) ) { + + let options:any = {}; + + let handle = this.#boardElement.querySelector( ".handle" ); + + if ( handle instanceof HTMLElement ) { + options.handle = handle; + } + + } + + + this.#setupAddFlight(); + + // this.#_setupDemoData(); + + } + + + addFlight( unit:Unit ) { + + const baseData = unit.getBaseData(); + + const unitCanBeAdded = () => { + + if ( baseData.category !== "Aircraft" ) { + return false; + } + + if ( baseData.controlled === true ) { + // return false; + } + + if ( this.#unitIdsBeingMonitored.includes( unit.ID ) ) { + return false; + } + + return true; + } + + if ( !unitCanBeAdded() ) { + return; + } + + this.#unitIdsBeingMonitored.push( unit.ID ); + + return fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : baseData.unitName, + "unitId" : unit.ID + }) + }); + + } + + + addStrip( strip:StripBoardStripInterface ) { + + this.#strips[ strip.id ] = strip; + + strip.element.querySelectorAll( "button.deleteFlight" ).forEach( btn => { + btn.addEventListener( "click", ev => { + ev.preventDefault(); + this.deleteFlight( strip.id ); + }); + }); + + } + + + boardIsVisible() { + return ( !this.getBoardElement().classList.contains( "hide" ) ); + } + + + calculateTimeToGo( fromTimestamp:number, toTimestamp:number ) { + + let timestamp = ( toTimestamp - fromTimestamp ) / 1000; + + const hasElapsed = ( timestamp < 0 ) ? true : false; + + if ( hasElapsed ) { + timestamp = -( timestamp ); + } + + const hours = ( timestamp < 3600 ) ? "00" : zeroAppend( Math.floor( timestamp / 3600 ), 2 ); + const rMinutes = timestamp % 3600; + + const minutes = ( timestamp < 60 ) ? "00" : zeroAppend( Math.floor( rMinutes / 60 ), 2 ); + const seconds = zeroAppend( Math.floor( rMinutes % 60 ), 2 ); + + return { + "elapsedMarker": ( hasElapsed ) ? "+" : "-", + "hasElapsed": hasElapsed, + "hours": hours, + "minutes": minutes, + "seconds": seconds, + "time": `${hours}:${minutes}:${seconds}`, + "totalSeconds": timestamp + }; + + } + + + deleteStrip( flightId:string ) { + + if ( this.#strips.hasOwnProperty( flightId ) ) { + + this.#strips[ flightId ].element.remove(); + this.#strips[ flightId ].isDeleted = true; + + window.setTimeout( () => { + delete this.#strips[ flightId ]; + }, 10000 ); + + } + + } + + + deleteFlight( flightId:string ) { + + this.deleteStrip( flightId ); + + fetch( '/api/atc/flight/' + flightId, { + method: 'DELETE', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId": this.getBoardId() + }) + }); + + } + + + getATC() { + return this.#atc; + } + + + getBoardElement() { + return this.#boardElement; + } + + + getBoardId(): string { + return this.getBoardElement().id; + } + + + getStripBoardElement() { + return this.#stripBoardElement; + } + + + getStrips() { + return this.#strips; + } + + + getStrip( id:string ) { + return this.#strips[ id ] || false; + } + + + getTemplate( key:string ) { + + return this.#templates[ key ] || false; + + } + + + getUnitIdsBeingMonitored() { + + return this.#unitIdsBeingMonitored; + + } + + + setTemplates( templates:{[key:string]: string} ) { + this.#templates = templates; + } + + + #setupAddFlight() { + + const toggleIsAddFlightByClickEnabled = () => { + this.#isAddFlightByClickEnabled = ( !this.#isAddFlightByClickEnabled ); + this.getBoardElement().classList.toggle( "add-flight-by-click", this.#isAddFlightByClickEnabled ); + } + + + document.addEventListener( "unitSelection", ( ev:CustomEventInit ) => { + + if ( this.#isAddFlightByClickEnabled !== true ) { + return; + } + + this.addFlight( ev.detail ); + + toggleIsAddFlightByClickEnabled(); + + }); + + + const form = this.getBoardElement().querySelector( "form.ol-strip-board-add-flight" ); + const suggestions = form.querySelector( ".ol-auto-suggest" ); + const unitName = form.querySelector( "input[name='unitName']" ); + + const toggleSuggestions = ( bool:boolean ) => { + suggestions.toggleAttribute( "data-has-suggestions", bool ); + } + + let searchTimeout:number|null; + + unitName.addEventListener( "keyup", ev => { + + if ( searchTimeout ) { + clearTimeout( searchTimeout ); + } + + const resetSuggestions = () => { + suggestions.innerHTML = ""; + toggleSuggestions( false ); + } + + resetSuggestions(); + + searchTimeout = window.setTimeout( () => { + + const searchString = unitName.value.toLowerCase(); + + if ( searchString === "" ) { + return; + } + + const units = getUnitsManager().getSelectableAircraft(); + const unitIdsBeingMonitored = this.getUnitIdsBeingMonitored(); + + const results = Object.keys( units ).reduce( ( acc:Unit[], unitId:any ) => { + + const unit = units[ unitId ]; + const baseData = unit.getBaseData(); + + if ( !unitIdsBeingMonitored.includes( parseInt( unitId ) ) && baseData.unitName.toLowerCase().indexOf( searchString ) > -1 ) { + acc.push( unit ); + } + + return acc; + + }, [] ); + + toggleSuggestions( results.length > 0 ); + + results.forEach( unit => { + + const baseData = unit.getBaseData(); + + const a = document.createElement( "a" ); + a.innerText = baseData.unitName; + + a.addEventListener( "click", ev => { + this.addFlight( unit ); + resetSuggestions(); + unitName.value = ""; + }); + + suggestions.appendChild( a ); + + }); + + + + }, 1000 ); + + + }); + + form.querySelectorAll( ".add-flight-by-click" ).forEach( el => { + el.addEventListener( "click", ev => { + ev.preventDefault(); + toggleIsAddFlightByClickEnabled(); + }); + }); + + } + + + sortFlights( flights:FlightInterface[] ) { + + flights.sort( ( a, b ) => { + + const aVal = a.order; + const bVal = b.order; + + return ( aVal > bVal ) ? 1 : -1; + + }); + + return flights; + + } + + + startUpdates() { + + if ( !this.boardIsVisible() ) { + return; + } + + this.#updateInterval = window.setInterval( () => { + + if ( !getConnected() ) { + return; + } + + this.update(); + + }, this.#updateIntervalDelay ); + + } + + + stopUpdates() { + + clearInterval( this.#updateInterval ); + + } + + + timestampToLocaleTime( timestamp:number ) { + + return ( timestamp === -1 ) ? "-" : new Date( timestamp ).toLocaleTimeString(); + + } + + + timeToGo( timestamp:number ) { + + const timeData = this.calculateTimeToGo( this.getATC().getMissionDateTime().getTime(), timestamp ); + + return ( timestamp === -1 ) ? "-" : timeData.elapsedMarker + timeData.time; + + } + + protected update() { + console.warn( "No custom update method defined." ); + } + + + updateClock() { + + const missionTime = this.#atc.getMissionDateTime().getTime(); + const timeDiff = new Date().getTime() - getMissionData().getUpdateTime(); + + const nowDate = new Date( missionTime + timeDiff ); + + this.#clockElement.innerText = nowDate.toLocaleTimeString(); + + } + + + updateFlight( flightId:string, reqBody:object ) { + + return fetch( '/api/atc/flight/' + flightId, { + method: 'PATCH', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify( reqBody ) + }); + + } + + + #_setupDemoData() { + + fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : this.getBoardId() + " 1", + "unitId" : 1 + }) + }); + + + // fetch( '/api/atc/flight/', { + // method: 'POST', + // headers: { + // 'Accept': '*/*', + // 'Content-Type': 'application/json' + // }, + // "body": JSON.stringify({ + // "boardId" : this.getBoardId(), + // "name" : this.getBoardId() + " 2", + // "unitId" : 2 + // }) + // }); + + // fetch( '/api/atc/flight/', { + // method: 'POST', + // headers: { + // 'Accept': '*/*', + // 'Content-Type': 'application/json' + // }, + // "body": JSON.stringify({ + // "boardId" : this.getBoardId(), + // "name" : this.getBoardId() + " 3", + // "unitId" : 9 + // }) + // }); + + } + + +} \ No newline at end of file diff --git a/client/src/atc/board/ground.ts b/client/src/atc/board/ground.ts new file mode 100644 index 00000000..2baf4862 --- /dev/null +++ b/client/src/atc/board/ground.ts @@ -0,0 +1,200 @@ +import { Dropdown } from "../../controls/dropdown"; +import { ATC } from "../atc"; +import { ATCBoard } from "../atcboard"; + + +export class ATCBoardGround extends ATCBoard { + + constructor( atc:ATC, element:HTMLElement ) { + + super( atc, element ); + + } + + + update() { + + const flights = this.sortFlights( Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ) ); + const stripBoard = this.getStripBoardElement(); + + const missionTime = this.getATC().getMissionDateTime().getTime(); + + for( const strip of stripBoard.children ) { + strip.toggleAttribute( "data-updating", true ); + } + + + flights.forEach( flight => { + + let strip = this.getStrip( flight.id ); + + if ( !strip ) { + + const template = `
+
+
${flight.name}
+ +
+
${flight.status}
+
+
+ +
+ +
${this.timeToGo( flight.takeoffTime )}
+ + +
`; + + stripBoard.insertAdjacentHTML( "beforeend", template ); + + + strip = { + "id": flight.id, + "element": stripBoard.lastElementChild, + "dropdowns": {}, + "unitId": -1 + }; + + strip.element.querySelectorAll( ".ol-select" ).forEach( select => { + + switch( select.getAttribute( "data-point" ) ) { + + case "status": + + strip.dropdowns.status = new Dropdown( select.id, ( value:string, ev:MouseEvent ) => { + + fetch( '/api/atc/flight/' + flight.id, { + method: 'PATCH', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "status": value + }) + }); + + }, [ + "unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated" + ]); + + break; + + } + + }); + + strip.element.querySelectorAll( `input[type="text"]` ).forEach( input => { + + if ( input instanceof HTMLInputElement ) { + + input.addEventListener( "blur", ( ev ) => { + + const target = ev.target; + + if ( target instanceof HTMLInputElement ) { + + if ( /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test( target.value ) ) { + target.value += ":00"; + } + + const value = target.value; + + if ( value === target.dataset.previousValue ) { + return; + + } else if ( value === "" ) { + + this.#updateTakeoffTime( flight.id, -1 ); + + } else if ( /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test( value ) ) { + + let [ hours, minutes, seconds ] = value.split( ":" ).map( str => parseInt( str ) ); + + const missionStart = this.getATC().getMissionStartDateTime(); + + this.#updateTakeoffTime( flight.id, new Date( + missionStart.getFullYear(), + missionStart.getMonth(), + missionStart.getDate(), + hours, + minutes, + seconds + ).getTime() ); + + } else { + + target.value === target.dataset.previousValue + + } + + } + + }); + + } + + }); + + this.addStrip( strip ); + + } else { + + if ( flight.status !== strip.element.getAttribute( "data-flight-status" ) ) { + strip.element.setAttribute( "data-flight-status", flight.status ); + strip.dropdowns.status.selectText( flight.status ); + } + + strip.element.querySelectorAll( `input[name="takeoffTime"]:not(:focus)` ).forEach( el => { + if ( el instanceof HTMLInputElement ) { + el.value = this.timestampToLocaleTime( flight.takeoffTime ); + el.dataset.previousValue = el.value; + } + }); + + strip.element.querySelectorAll( `[data-point="timeToGo"]` ).forEach( el => { + + if ( flight.takeoffTime > 0 && this.calculateTimeToGo( missionTime, flight.takeoffTime ).totalSeconds <= 120 ) { + strip.element.setAttribute( "data-time-warning", "level-1" ); + } + + if ( el instanceof HTMLElement ) { + el.innerText = this.timeToGo( flight.takeoffTime ); + } + + }); + + + } + + strip.element.toggleAttribute( "data-updating", false ); + + }); + + + stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { + this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" ); + }); + + } + + + + + #updateTakeoffTime = function( flightId:string, time:number ) { + + fetch( '/api/atc/flight/' + flightId, { + method: 'PATCH', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "takeoffTime": time + }) + }); + + } + +} \ No newline at end of file diff --git a/client/src/atc/board/tower.ts b/client/src/atc/board/tower.ts new file mode 100644 index 00000000..c1698966 --- /dev/null +++ b/client/src/atc/board/tower.ts @@ -0,0 +1,194 @@ +import { getUnitsManager } from "../.."; +import { Dropdown } from "../../controls/dropdown"; +import { mToFt, msToKnots } from "../../other/utils"; +import { ATC } from "../atc"; +import { ATCBoard } from "../atcboard"; + + +export class ATCBoardTower extends ATCBoard { + + constructor( atc:ATC, element:HTMLElement ) { + + super( atc, element ); + + } + + + update() { + + const flights = this.sortFlights( Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ) ); + const missionTime = this.getATC().getMissionDateTime().getTime(); + const selectableUnits = getUnitsManager().getSelectableAircraft(); + const stripBoard = this.getStripBoardElement(); + + for( const strip of stripBoard.children ) { + strip.toggleAttribute( "data-updating", true ); + } + + + flights.forEach( flight => { + + let strip = this.getStrip( flight.id ); + + if ( strip.isDeleted === true ) { + return; + } + + const flightData:FlightData = { + latitude: -1, + longitude: -1, + altitude: -1, + heading: -1, + speed: -1, + ...( selectableUnits.hasOwnProperty( flight.unitId ) ? selectableUnits[flight.unitId].getFlightData() : {} ) + }; + + if ( !strip ) { + + const template = `
+
+ + +
000
+
-
+ +
+
-
+ + +
`; + + stripBoard.insertAdjacentHTML( "beforeend", template ); + + + strip = { + "id": flight.id, + "element": stripBoard.lastElementChild, + "dropdowns": {}, + "unitId": flight.unitId + }; + + + strip.element.querySelectorAll( `input[type="text"]` ).forEach( input => { + + if ( input instanceof HTMLInputElement ) { + + switch ( input.name ) { + + case "assignedAltitude": + + input.addEventListener( "change", ( ev ) => { + + let val = parseInt( input.value.replace( /[^\d]/g, "" ) ); + + if ( isNaN( val ) || val < 0 || val > 40 ) { + val = 0; + } + + this.updateFlight( flight.id, { + "assignedAltitude": val + }); + + }); + + break; + + case "assignedSpeed": + + input.addEventListener( "change", ( ev ) => { + + let val = parseInt( input.value.replace( /[^\d]/g, "" ) ); + + if ( isNaN( val ) || val < 0 || val > 750 ) { + val = 0; + } + + this.updateFlight( flight.id, { + "assignedSpeed": val + }); + + }); + + break; + + } + + } + + }); + + strip.element.querySelectorAll( ".select-unit" ).forEach( el => { + + el.addEventListener( "click", ev => { + ev.preventDefault(); + getUnitsManager().selectUnit( flight.unitId ); + }); + + }); + + this.addStrip( strip ); + + } else { + + // + // Altitude + // + + let assignedAltitude = strip.element.querySelector( `input[name="assignedAltitude"]`); + + if ( !assignedAltitude.matches( ":focus" ) && assignedAltitude.value !== flight.assignedAltitude ) { + assignedAltitude.value = flight.assignedAltitude; + } + + flightData.altitude = Math.floor( mToFt(flightData.altitude) ); + + strip.element.querySelectorAll( `[data-point="altitude"]` ).forEach( el => { + if ( el instanceof HTMLElement ) { + el.innerText = "" + flightData.altitude; + } + }); + + const altitudeDelta = ( flight.assignedAltitude === 0 ) ? 0 : ( flight.assignedAltitude * 1000 ) - flightData.altitude; + + strip.element.toggleAttribute( "data-altitude-assigned", ( flight.assignedAltitude > 0 ) ); + strip.element.toggleAttribute( "data-warning-altitude", ( altitudeDelta >= 300 || altitudeDelta <= -300 ) ); + + + // + // Speed + // + + let assignedSpeed = strip.element.querySelector( `input[name="assignedSpeed"]`); + + if ( !assignedSpeed.matches( ":focus" ) && assignedSpeed.value !== flight.assignedSpeed ) { + assignedSpeed.value = flight.assignedSpeed; + } + + flightData.speed = Math.floor( msToKnots(flightData.speed) ); + + strip.element.querySelectorAll( `[data-point="speed"]` ).forEach( el => { + if ( el instanceof HTMLElement ) { + el.innerText = "" + flightData.speed; + } + }); + + const speedDelta = ( flight.assignedSpeed === 0 ) ? 0 : flight.assignedSpeed - flightData.speed; + + strip.element.toggleAttribute( "data-speed-assigned", ( flight.assignedSpeed > 0 ) ); + strip.element.toggleAttribute( "data-warning-speed", ( speedDelta >= 25 || speedDelta <= -25 ) ); + + + + } + + strip.element.toggleAttribute( "data-updating", false ); + + }); + + stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { + this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" ); + }); + + } + +} \ No newline at end of file diff --git a/client/src/atc/unitdatatable.ts b/client/src/atc/unitdatatable.ts new file mode 100644 index 00000000..1ef21f2c --- /dev/null +++ b/client/src/atc/unitdatatable.ts @@ -0,0 +1,57 @@ +import { getUnitsManager } from ".."; +import { Panel } from "../panels/panel"; +import { Unit } from "../units/unit"; + +export class UnitDataTable extends Panel { + constructor(id: string) { + super(id); + this.hide(); + } + + update() { + var units = getUnitsManager().getUnits(); + + const unitsArray = Object.values(units).sort((a: Unit, b: Unit) => { + const aVal = a.getBaseData().unitName?.toLowerCase(); + const bVal = b.getBaseData().unitName?.toLowerCase(); + + if (aVal > bVal) { + return 1; + } else if (bVal > aVal) { + return -1; + } else { + return 0; + } + }); + + + function addRow(parentEl: HTMLElement, columns: string[]) { + const rowDiv = document.createElement("div"); + + for (const item of columns) { + + const div = document.createElement("div"); + div.innerText = item; + rowDiv.appendChild(div); + + } + parentEl.appendChild(rowDiv); + } + + const el = this.getElement().querySelector("#unit-list"); + + if (el) { + + el.innerHTML = ""; + + addRow(el, ["Callsign", "Name", "Category", "AI/Human"]) + + for (const unit of unitsArray) { + + const dataset = [unit.getBaseData().unitName, unit.getBaseData().name, unit.getBaseData().category, (unit.getBaseData().controlled) ? "AI" : "Human"]; + + addRow(el, dataset); + } + } + } +} \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts new file mode 100644 index 00000000..0f91ed25 --- /dev/null +++ b/client/src/constants/constants.ts @@ -0,0 +1,102 @@ +import { LatLng, LatLngBounds, TileLayer, tileLayer } from "leaflet"; + +export const ROEs: string[] = ["Hold", "Return", "Designated", "Free"]; +export const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"]; +export const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"]; + +export const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"]; +export const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"]; +export const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"]; + +export const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; +export const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 }; +export const speedIncrements: { [key: string]: number } = { Aircraft: 25, Helicopter: 10, NavyUnit: 5, GroundUnit: 5 }; +export const minAltitudeValues: { [key: string]: number } = { Aircraft: 0, Helicopter: 0 }; +export const maxAltitudeValues: { [key: string]: number } = { Aircraft: 50000, Helicopter: 10000 }; +export const altitudeIncrements: { [key: string]: number } = { Aircraft: 500, Helicopter: 100 }; + +export const minimapBoundaries = [ + [ // NTTR + new LatLng(39.7982463, -119.985425), + new LatLng(34.4037128, -119.7806729), + new LatLng(34.3483316, -112.4529351), + new LatLng(39.7372411, -112.1130805), + new LatLng(39.7982463, -119.985425) + ], + [ // Syria + new LatLng(37.3630556, 29.2686111), + new LatLng(31.8472222, 29.8975), + new LatLng(32.1358333, 42.1502778), + new LatLng(37.7177778, 42.3716667), + new LatLng(37.3630556, 29.2686111) + ], + [ // Caucasus + new LatLng(39.6170191, 27.634935), + new LatLng(38.8735863, 47.1423108), + new LatLng(47.3907982, 49.3101946), + new LatLng(48.3955879, 26.7753625), + new LatLng(39.6170191, 27.634935) + ], + [ // Persian Gulf + new LatLng(32.9355285, 46.5623682), + new LatLng(21.729393, 47.572675), + new LatLng(21.8501348, 63.9734737), + new LatLng(33.131584, 64.7313594), + new LatLng(32.9355285, 46.5623682) + ], + [ // Marianas + new LatLng(22.09, 135.0572222), + new LatLng(10.5777778, 135.7477778), + new LatLng(10.7725, 149.3918333), + new LatLng(22.5127778, 149.5427778), + new LatLng(22.09, 135.0572222) + ] +]; + +export const mapBounds = { + "Syria": { bounds: new LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]), zoom: 5 }, + "MarianaIslands": { bounds: new LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]), zoom: 5 }, + "Nevada": { bounds: new LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]), zoom: 5 }, + "PersianGulf": { bounds: new LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]), zoom: 5 }, + "Caucasus": { bounds: new LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]), zoom: 4 }, + // TODO "Falklands" +} + +export const layers = { + "ArcGIS Satellite": { + urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + maxZoom: 20, + minZoom: 1, + attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + }, + "USGS Topo": { + urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 20, + attribution: 'Tiles courtesy of the U.S. Geological Survey' + }, + "OpenStreetMap Mapnik": { + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 19, + attribution: '© OpenStreetMap contributors' + }, + "OPENVKarte": { + urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' + }, + "Esri.DeLorme": { + urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 11, + attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', + }, + "CyclOSM": { + urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 20, + attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' + } +} \ No newline at end of file diff --git a/client/src/controls/airbasecontextmenu.ts b/client/src/controls/airbasecontextmenu.ts new file mode 100644 index 00000000..5ee53166 --- /dev/null +++ b/client/src/controls/airbasecontextmenu.ts @@ -0,0 +1,70 @@ +import { getMap, getUnitsManager, setActiveCoalition } from ".."; +import { Airbase } from "../missionhandler/airbase"; +import { ContextMenu } from "./contextmenu"; + +export class AirbaseContextMenu extends ContextMenu { + #airbase: Airbase | null = null; + + constructor(id: string) { + super(id); + document.addEventListener("contextMenuSpawnAirbase", (e: any) => { + this.showSpawnMenu(); + }) + + document.addEventListener("contextMenuLandAirbase", (e: any) => { + if (this.#airbase) + getUnitsManager().selectedUnitsLandAt(this.#airbase.getLatLng()); + this.hide(); + }) + } + + setAirbase(airbase: Airbase) { + this.#airbase = airbase; + this.setName(airbase.getName()); + this.setProperties(airbase.getProperties()); + this.setParkings(airbase.getParkings()); + this.setCoalition(airbase.getCoalition()); + this.enableLandButton(getUnitsManager().getSelectedUnitsTypes().length == 1 && getUnitsManager().getSelectedUnitsTypes()[0] === "Aircraft" && (getUnitsManager().getSelectedUnitsCoalition() === airbase.getCoalition() || airbase.getCoalition() === "neutral")) + } + + setName(airbaseName: string) { + var nameDiv = this.getContainer()?.querySelector("#airbase-name"); + if (nameDiv != null) + nameDiv.innerText = airbaseName; + } + + setProperties(airbaseProperties: string[]) { + this.getContainer()?.querySelector("#airbase-properties")?.replaceChildren(...airbaseProperties.map((property: string) => { + var div = document.createElement("div"); + div.innerText = property; + return div; + }),); + } + + setParkings(airbaseParkings: string[]) { + this.getContainer()?.querySelector("#airbase-parking")?.replaceChildren(...airbaseParkings.map((parking: string) => { + var div = document.createElement("div"); + div.innerText = parking; + return div; + })); + } + + setCoalition(coalition: string) { + (this.getContainer()?.querySelector("#spawn-airbase-aircraft-button")).dataset.activeCoalition = coalition; + } + + enableLandButton(enableLandButton: boolean) { + this.getContainer()?.querySelector("#land-here-button")?.classList.toggle("hide", !enableLandButton); + } + + showSpawnMenu() { + if (this.#airbase != null) { + setActiveCoalition(this.#airbase.getCoalition()); + getMap().showMapContextMenu({ originalEvent: { x: this.getX(), y: this.getY(), latlng: this.getLatLng() } }); + getMap().getMapContextMenu().hideUpperBar(); + getMap().getMapContextMenu().showSubMenu("aircraft"); + getMap().getMapContextMenu().setAirbaseName(this.#airbase.getName()); + getMap().getMapContextMenu().setLatLng(this.#airbase.getLatLng()); + } + } +} \ No newline at end of file diff --git a/client/src/controls/button.ts b/client/src/controls/button.ts deleted file mode 100644 index bb602838..00000000 --- a/client/src/controls/button.ts +++ /dev/null @@ -1,38 +0,0 @@ -export class Button { - #container: HTMLElement | null; - #srcs: string[]; - #callback: CallableFunction; - #img: any; - #state: number = 0; - - constructor(ID: string, srcs: string[], callback: CallableFunction) { - this.#container = document.getElementById(ID); - this.#srcs = srcs; - this.#callback = callback; - if (this.#container != null) { - this.#img = document.createElement("img"); - this.#img.src = this.#srcs[this.#state]; - this.#container.appendChild(this.#img); - this.#container.addEventListener("click", () => this.#onClick()); - } - } - - setState(state: number) { - if (state < this.#srcs.length) { - this.#state = state; - this.#img.src = this.#srcs[this.#state]; - } - } - - getState() { - return this.#state; - } - - #onClick() { - if (this.#img != null) { - this.setState(this.#state < this.#srcs.length - 1 ? this.#state + 1 : 0); - if (this.#callback) - this.#callback(this.#state); - } - } -} \ No newline at end of file diff --git a/client/src/controls/contextmenu.ts b/client/src/controls/contextmenu.ts new file mode 100644 index 00000000..58cf371f --- /dev/null +++ b/client/src/controls/contextmenu.ts @@ -0,0 +1,55 @@ +import { LatLng } from "leaflet"; + +export class ContextMenu { + #container: HTMLElement | null; + #latlng: LatLng = new LatLng(0, 0); + #x: number = 0; + #y: number = 0; + + constructor(id: string) { + this.#container = document.getElementById(id); + this.hide(); + } + + show(x: number, y: number, latlng: LatLng) { + this.#latlng = latlng; + this.#container?.classList.toggle("hide", false); + this.#x = x; + this.#y = y; + this.clip(); + } + + hide() { + this.#container?.classList.toggle("hide", true); + } + + getContainer() { + return this.#container; + } + + getLatLng() { + return this.#latlng; + } + + getX() { + return this.#x; + } + + getY() { + return this.#y; + } + + clip() { + if (this.#container != null) { + if (this.#x + this.#container.offsetWidth < window.innerWidth) + this.#container.style.left = this.#x + "px"; + else + this.#container.style.left = window.innerWidth - this.#container.offsetWidth - 10 + "px"; + + if (this.#y + this.#container.offsetHeight < window.innerHeight) + this.#container.style.top = this.#y + "px"; + else + this.#container.style.top = window.innerHeight - this.#container.offsetHeight - 10 + "px"; + } + } +} \ No newline at end of file diff --git a/client/src/controls/control.ts b/client/src/controls/control.ts new file mode 100644 index 00000000..4786a862 --- /dev/null +++ b/client/src/controls/control.ts @@ -0,0 +1,34 @@ +export class Control { + #container: HTMLElement | null; + expectedValue: any = null; + + constructor(ID: string) { + this.#container = document.getElementById(ID); + } + + show() { + if (this.#container != null) + this.#container.classList.remove("hide"); + } + + hide() { + if (this.#container != null) + this.#container.classList.add("hide"); + } + + getContainer() { + return this.#container; + } + + setExpectedValue(expectedValue: any) { + this.expectedValue = expectedValue; + } + + resetExpectedValue() { + this.expectedValue = null; + } + + checkExpectedValue(value: any) { + return this.expectedValue === null || value === this.expectedValue; + } +} \ No newline at end of file diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 68b73a2a..2877d80d 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -1,62 +1,120 @@ export class Dropdown { - #container: HTMLElement | null; - #options: string[]; - #open?: boolean; - #content?: HTMLElement; - #callback?: CallableFunction; + #element: HTMLElement; + #options: HTMLElement; + #value: HTMLElement; + #callback: CallableFunction; + #defaultValue: string; + #optionsList: string[] = []; + #index: number = 0; - constructor(ID: string, options: string[], callback: CallableFunction) { - this.#container = document.getElementById(ID); - this.#options = options; + constructor(ID: string, callback: CallableFunction, options: string[] | null = null) { + this.#element = document.getElementById(ID); + this.#options = this.#element.querySelector(".ol-select-options"); + this.#value = this.#element.querySelector(".ol-select-value"); + this.#defaultValue = this.#value.innerText; this.#callback = callback; - this.close() - this.#container?.addEventListener("click", () => { - this.#open ? this.close() : this.open(); - }) - if (this.#container != null && this.#options.length > 0) - this.#container.innerHTML = this.#options[0]; + + if (options != null) { + this.setOptions(options); + } + + this.#value.addEventListener("click", (ev) => { + this.#toggle(); + }); + + document.addEventListener("click", (ev) => { + if (!(this.#value.contains(ev.target as Node) || this.#options.contains(ev.target as Node) || this.#element.contains(ev.target as Node))) { + this.#close(); + } + }); + + this.#options.classList.add("ol-scrollable"); } - open() { - if (this.#container != null) { - this.#open = true; - this.#container.classList.add("olympus-dropdown-open"); - this.#container.classList.remove("olympus-dropdown-closed"); - this.#content = document.createElement("div"); - this.#content.classList.add("olympus-dropdown-content"); - this.#content.style.width = (this.#container.offsetWidth - this.#container.offsetHeight) + "px"; + setOptions(optionsList: string[], sortAlphabetically: boolean = true) { + this.#optionsList = optionsList.sort(); + this.#options.replaceChildren(...optionsList.map((option: string, idx: number) => { + var div = document.createElement("div"); + var button = document.createElement("button"); + button.textContent = option; + div.appendChild(button); - this.#content.style.left = this.#container.offsetLeft + "px"; - this.#content.style.top = this.#container.offsetTop + this.#container.offsetHeight + "px"; - console.log(this.#container); - document.body.appendChild(this.#content); + if (option === this.#defaultValue) + this.#index = idx; - var height = 2; - for (let optionID in this.#options) { - var node = document.createElement("div"); - node.classList.add("olympus-dropdown-element"); - node.appendChild(document.createTextNode(this.#options[optionID])); - this.#content.appendChild(node); - height += node.offsetHeight + 2; - node.addEventListener('click', () => { - this.close(); - if (this.#container != null) - this.#container.innerHTML = this.#options[optionID]; - if (this.#callback != null) - this.#callback(this.#options[optionID]) - }) - } - this.#content.style.height = height + "px"; + button.addEventListener("click", (e: MouseEvent) => { + e.stopPropagation(); + this.selectValue(idx); + }); + return div; + })); + } + + selectText(text: string) { + const index = [].slice.call(this.#options.children).findIndex((opt: Element) => opt.querySelector("button")?.innerText === text); + if (index > -1) { + this.selectValue(index); } } - close() { - if (this.#container != null) { - this.#open = false; - this.#container?.classList.remove("olympus-dropdown-open"); - this.#container?.classList.add("olympus-dropdown-closed"); - if (this.#content != null) - document.body.removeChild(this.#content); + selectValue(idx: number) { + if (idx < this.#optionsList.length) { + var option = this.#optionsList[idx]; + var el = document.createElement("div"); + el.classList.add("ol-ellipsed"); + el.innerText = option; + this.#value.replaceChildren(); + this.#value.appendChild(el); + this.#index = idx; + this.#close(); + this.#callback(option); + return true; + } + else + return false; + } + + reset() { + this.#options.replaceChildren(); + this.#value.innerText = this.#defaultValue; + } + + getValue() { + return this.#value.innerText; + } + + setValue(value: string) { + var index = this.#optionsList.findIndex((option) => { return option === value }); + if (index > -1) + this.selectValue(index); + } + + getIndex() { + return this.#index; + } + + #clip() { + const options = this.#options; + const bounds = options.getBoundingClientRect(); + this.#element.dataset.position = (bounds.bottom > window.innerHeight) ? "top" : ""; + } + + #close() { + this.#element.classList.remove("is-open"); + this.#element.dataset.position = ""; + } + + #open() { + this.#element.classList.add("is-open"); + this.#options.classList.toggle("scrollbar-visible", this.#options.scrollHeight > this.#options.clientHeight); + this.#clip(); + } + + #toggle() { + if (this.#element.classList.contains("is-open")) { + this.#close(); + } else { + this.#open(); } } } \ No newline at end of file diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts new file mode 100644 index 00000000..51027f01 --- /dev/null +++ b/client/src/controls/mapcontextmenu.ts @@ -0,0 +1,227 @@ +import { LatLng } from "leaflet"; +import { getActiveCoalition, getMap, setActiveCoalition } from ".."; +import { spawnAircraft, spawnExplosion, spawnGroundUnit, spawnSmoke } from "../server/server"; +import { aircraftDatabase } from "../units/aircraftdatabase"; +import { groundUnitsDatabase } from "../units/groundunitsdatabase"; +import { ContextMenu } from "./contextmenu"; +import { Dropdown } from "./dropdown"; +import { Switch } from "./switch"; +import { Slider } from "./slider"; +import { ftToM } from "../other/utils"; + +export interface SpawnOptions { + role: string; + type: string; + latlng: LatLng; + coalition: string; + loadout: string | null; + airbaseName: string | null; + altitude: number | null; +} + +export class MapContextMenu extends ContextMenu { + #coalitionSwitch: Switch; + #aircraftRoleDropdown: Dropdown; + #aircraftTypeDropdown: Dropdown; + #aircraftLoadoutDropdown: Dropdown; + #aircrafSpawnAltitudeSlider: Slider; + #groundUnitRoleDropdown: Dropdown; + #groundUnitTypeDropdown: Dropdown; + #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: ftToM(20000) }; + + constructor(id: string) { + super(id); + + this.#coalitionSwitch = new Switch("coalition-switch", this.#onSwitchClick); + this.#coalitionSwitch.setValue(false); + this.#coalitionSwitch.getContainer()?.addEventListener("contextmenu", (e) => this.#onSwitchRightClick(e)); + this.#aircraftRoleDropdown = new Dropdown("aircraft-role-options", (role: string) => this.#setAircraftRole(role)); + this.#aircraftTypeDropdown = new Dropdown("aircraft-type-options", (type: string) => this.#setAircraftType(type)); + this.#aircraftLoadoutDropdown = new Dropdown("loadout-options", (loadout: string) => this.#setAircraftLoadout(loadout)); + this.#aircrafSpawnAltitudeSlider = new Slider("aircraft-spawn-altitude-slider", 0, 50000, "ft", (value: number) => {this.#spawnOptions.altitude = ftToM(value);}); + this.#aircrafSpawnAltitudeSlider.setIncrement(500); + this.#aircrafSpawnAltitudeSlider.setValue(20000); + this.#aircrafSpawnAltitudeSlider.setActive(true); + this.#groundUnitRoleDropdown = new Dropdown("ground-unit-role-options", (role: string) => this.#setGroundUnitRole(role)); + this.#groundUnitTypeDropdown = new Dropdown("ground-unit-type-options", (type: string) => this.#setGroundUnitType(type)); + + document.addEventListener("contextMenuShow", (e: any) => { + this.showSubMenu(e.detail.type); + }); + + document.addEventListener("contextMenuDeployAircraft", () => { + this.hide(); + this.#spawnOptions.coalition = getActiveCoalition(); + if (this.#spawnOptions) { + getMap().addTemporaryMarker(this.#spawnOptions.latlng); + spawnAircraft(this.#spawnOptions); + } + }); + + document.addEventListener("contextMenuDeployGroundUnit", () => { + this.hide(); + this.#spawnOptions.coalition = getActiveCoalition(); + if (this.#spawnOptions) { + getMap().addTemporaryMarker(this.#spawnOptions.latlng); + spawnGroundUnit(this.#spawnOptions); + } + }); + + document.addEventListener("contextMenuDeploySmoke", (e: any) => { + this.hide(); + spawnSmoke(e.detail.color, this.getLatLng()); + }); + + document.addEventListener("contextMenuExplosion", (e: any) => { + this.hide(); + spawnExplosion(e.detail.strength, this.getLatLng()); + }); + + + this.hide(); + } + + show(x: number, y: number, latlng: LatLng) { + this.#spawnOptions.airbaseName = null; + super.show(x, y, latlng); + this.#spawnOptions.latlng = latlng; + this.showUpperBar(); + } + + showSubMenu(type: string) { + this.getContainer()?.querySelector("#aircraft-spawn-menu")?.classList.toggle("hide", type !== "aircraft"); + this.getContainer()?.querySelector("#aircraft-spawn-button")?.classList.toggle("is-open", type === "aircraft"); + this.getContainer()?.querySelector("#ground-unit-spawn-menu")?.classList.toggle("hide", type !== "ground-unit"); + this.getContainer()?.querySelector("#ground-unit-spawn-button")?.classList.toggle("is-open", type === "ground-unit"); + this.getContainer()?.querySelector("#smoke-spawn-menu")?.classList.toggle("hide", type !== "smoke"); + this.getContainer()?.querySelector("#smoke-spawn-button")?.classList.toggle("is-open", type === "smoke"); + this.getContainer()?.querySelector("#explosion-menu")?.classList.toggle("hide", type !== "explosion"); + this.getContainer()?.querySelector("#explosion-spawn-button")?.classList.toggle("is-open", type === "explosion"); + + this.#resetAircraftRole(); + this.#resetAircraftType(); + this.#resetGroundUnitRole(); + this.#resetGroundUnitType(); + this.clip(); + } + + showUpperBar() { + this.getContainer()?.querySelector("#upper-bar")?.classList.toggle("hide", false); + } + + hideUpperBar() { + this.getContainer()?.querySelector("#upper-bar")?.classList.toggle("hide", true); + } + + setAirbaseName(airbaseName: string) { + this.#spawnOptions.airbaseName = airbaseName; + } + + setLatLng(latlng: LatLng) { + this.#spawnOptions.latlng = latlng; + } + + #onSwitchClick(value: boolean) { + value? setActiveCoalition("red"): setActiveCoalition("blue"); + } + + #onSwitchRightClick(e: any) { + this.#coalitionSwitch.setValue(undefined); + setActiveCoalition("neutral"); + } + + /********* Aircraft spawn menu *********/ + #setAircraftRole(role: string) { + this.#spawnOptions.role = role; + this.#resetAircraftType(); + this.#aircraftTypeDropdown.setOptions(aircraftDatabase.getByRole(role).map((blueprint) => { return blueprint.label })); + this.#aircraftTypeDropdown.selectValue(0); + this.clip(); + } + + #resetAircraftRole() { + (this.getContainer()?.querySelector("#aircraft-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = true; + (this.getContainer()?.querySelector("#loadout-list")).replaceChildren(); + this.#aircraftRoleDropdown.reset(); + this.#aircraftTypeDropdown.reset(); + this.#aircraftRoleDropdown.setOptions(aircraftDatabase.getRoles()); + this.clip(); + } + + #setAircraftType(label: string) { + this.#resetAircraftType(); + var type = aircraftDatabase.getByLabel(label)?.name || null; + if (type != null) { + this.#spawnOptions.type = type; + this.#aircraftLoadoutDropdown.setOptions(aircraftDatabase.getLoadoutNamesByRole(type, this.#spawnOptions.role)); + this.#aircraftLoadoutDropdown.selectValue(0); + var image = (this.getContainer()?.querySelector("#unit-image")); + image.src = `images/units/${aircraftDatabase.getByLabel(label)?.filename}`; + image.classList.toggle("hide", false); + } + this.clip(); + } + + #resetAircraftType() { + (this.getContainer()?.querySelector("#aircraft-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = true; + (this.getContainer()?.querySelector("#loadout-list")).replaceChildren(); + this.#aircraftLoadoutDropdown.reset(); + (this.getContainer()?.querySelector("#unit-image")).classList.toggle("hide", true); + this.clip(); + } + + #setAircraftLoadout(loadoutName: string) { + var loadout = aircraftDatabase.getLoadoutByName(this.#spawnOptions.type, loadoutName); + if (loadout) { + this.#spawnOptions.loadout = loadout.code; + (this.getContainer()?.querySelector("#aircraft-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = false; + var items = loadout.items.map((item: any) => { return `${item.quantity}x ${item.name}`; }); + items.length == 0 ? items.push("Empty loadout") : ""; + (this.getContainer()?.querySelector("#loadout-list")).replaceChildren( + ...items.map((item: any) => { + var div = document.createElement('div'); + div.innerText = item; + return div; + }) + ) + } + this.clip(); + } + + /********* Ground unit spawn menu *********/ + #setGroundUnitRole(role: string) { + this.#spawnOptions.role = role; + this.#resetGroundUnitType(); + + const types = groundUnitsDatabase.getByRole(role).map((blueprint) => { return blueprint.label }); + this.#groundUnitTypeDropdown.setOptions(types); + this.#groundUnitTypeDropdown.selectValue(0); + this.clip(); + } + + #resetGroundUnitRole() { + (this.getContainer()?.querySelector("#ground-unit-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = true; + (this.getContainer()?.querySelector("#loadout-list")).replaceChildren(); + this.#groundUnitRoleDropdown.reset(); + this.#groundUnitTypeDropdown.reset(); + + const roles = groundUnitsDatabase.getRoles(); + this.#groundUnitRoleDropdown.setOptions(roles); + this.clip(); + } + + #setGroundUnitType(label: string) { + this.#resetGroundUnitType(); + var type = groundUnitsDatabase.getByLabel(label)?.name || null; + if (type != null) { + this.#spawnOptions.type = type; + (this.getContainer()?.querySelector("#ground-unit-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = false; + } + this.clip(); + } + + #resetGroundUnitType() { + (this.getContainer()?.querySelector("#ground-unit-spawn-menu")?.querySelector(".deploy-unit-button")).disabled = true; + this.clip(); + } +} \ No newline at end of file diff --git a/client/src/controls/selectionscroll.ts b/client/src/controls/selectionscroll.ts deleted file mode 100644 index 7a518371..00000000 --- a/client/src/controls/selectionscroll.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { LatLng } from "leaflet"; -import { getActiveCoalition, setActiveCoalition } from ".."; - -export class SelectionScroll { - #container: HTMLElement | null; - #display: string; - - constructor(id: string,) { - this.#container = document.getElementById(id); - this.#display = ''; - if (this.#container != null) { - this.#container.querySelector("#coalition-switch")?.addEventListener('change', (e) => this.#onSwitch(e)) - this.#display = this.#container.style.display; - this.hide(); - } - } - - show(x: number, y: number, title: string, options: any, callback: CallableFunction, showCoalition: boolean) { - /* Hide to remove buttons, if present */ - this.hide(); - - if (this.#container != null && options.length >= 1) { - var titleDiv = this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector(".olympus-selection-scroll-title"); - if (titleDiv) - titleDiv.innerHTML = title; - this.#container.style.display = this.#display; - this.#container.style.left = x - this.#container.offsetWidth / 2 + "px"; - this.#container.style.top = y - 20 + "px"; - var scroll = this.#container.querySelector(".olympus-selection-scroll"); - if (scroll != null) - { - for (let optionID in options) { - var node = document.createElement("div"); - node.classList.add("olympus-selection-scroll-element"); - if (typeof options[optionID] === 'string' || options[optionID] instanceof String){ - node.appendChild(document.createTextNode(options[optionID])); - node.addEventListener('click', () => callback(options[optionID])); - } - else { - node.appendChild(document.createTextNode(options[optionID].tooltip)); - node.addEventListener('click', () => options[optionID].callback()); - } - scroll.appendChild(node); - } - } - - /* Hide the coalition switch if required */ - var switchContainer = this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector("#coalition-switch-container"); - if (showCoalition == false) { - switchContainer.style.display = "none"; - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--neutral-coalition-color")); - } - else { - switchContainer.style.display = "block"; - if (getActiveCoalition() == "blue") - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color")); - else - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color")); - } - } - } - - hide() { - if (this.#container != null) { - this.#container.style.display = "none"; - var buttons = this.#container.querySelectorAll(".olympus-selection-scroll-element"); - var scroll = this.#container.querySelector(".olympus-selection-scroll"); - if (scroll != null) - { - for (let child of buttons) { - scroll.removeChild(child); - } - } - } - } - - #onSwitch(e: any) { - if (this.#container != null) { - if (e.currentTarget.checked) { - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color")); - setActiveCoalition("red"); - } - else { - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color")); - setActiveCoalition("blue"); - } - } - } -} \ No newline at end of file diff --git a/client/src/controls/selectionwheel.ts b/client/src/controls/selectionwheel.ts deleted file mode 100644 index 76c30539..00000000 --- a/client/src/controls/selectionwheel.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { getActiveCoalition, setActiveCoalition } from ".."; -import { deg2rad } from "../other/utils"; - -export class SelectionWheel { - #container: HTMLElement | null; - #display: string; - - constructor(id: string) { - this.#container = document.getElementById(id); - this.#display = ''; - if (this.#container != null) { - this.#container.querySelector("#coalition-switch")?.addEventListener('change', (e) => this.#onSwitch(e)) - this.#display = this.#container.style.display; - this.hide(); - } - } - - show(x: number, y: number, options: any, showCoalition: boolean) { - /* Hide to remove buttons, if present */ - this.hide(); - - if (this.#container != null) { - this.#container.style.display = this.#display; - this.#container.style.left = x - 110 + "px"; - this.#container.style.top = y - 110 + "px"; - - var angularSize = 360 / options.length; - var r = 80; - - /* Create the buttons */ - for (let id in options) { - var button = document.createElement("div"); - button.classList.add("selection-wheel-button"); - button.style.left = x - 25 + "px"; - button.style.top = y - 25 + "px"; - button.addEventListener('click', (e) => options[id].callback(e)); - this.#container.appendChild(button); - var angle = parseInt(id) * angularSize; - button.style.opacity = "1"; - button.style.left = x + r * Math.sin(deg2rad(angle)) - 25 + "px"; - button.style.top = y - r * Math.cos(deg2rad(angle)) - 25 + "px"; - - var image = document.createElement("img"); - image.classList.add("selection-wheel-image"); - image.src = `images/buttons/${options[id].src}` - image.title = options[id].tooltip; - if ('tint' in options[id]) { - button.style.setProperty('background-color', options[id].tint); - image.style.opacity = "0"; - } - button.appendChild(image); - } - - /* Hide the coalition switch if required */ - var switchContainer = this.#container.querySelector("#coalition-switch-container"); - if (showCoalition == false) { - switchContainer.style.display = "none"; - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--neutral-coalition-color")); - } - else { - switchContainer.style.display = "block"; - if (getActiveCoalition() == "blue") - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color")); - else - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color")); - } - } - } - - hide() { - if (this.#container != null) { - this.#container.style.display = "none"; - var buttons = this.#container.querySelectorAll(".selection-wheel-button"); - for (let child of buttons) { - this.#container.removeChild(child); - } - } - } - - #onSwitch(e: any) { - if (this.#container != null) { - if (e.currentTarget.checked) { - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color")); - setActiveCoalition("red"); - } - else { - document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color")); - setActiveCoalition("blue"); - } - } - } -} \ No newline at end of file diff --git a/client/src/controls/slider.ts b/client/src/controls/slider.ts index 38b2a49e..edfb18a1 100644 --- a/client/src/controls/slider.ts +++ b/client/src/controls/slider.ts @@ -1,83 +1,123 @@ -export class Slider { - #container: HTMLElement | null; - #callback: CallableFunction; - #slider: HTMLInputElement | null = null; - #value: HTMLElement | null = null; - #minValue: number; - #maxValue: number; - #minValueDiv: HTMLElement | null = null; - #maxValueDiv: HTMLElement | null = null; - #unit: string; - #display: string = ""; +import { zeroPad } from "../other/utils"; +import { Control } from "./control"; - constructor(ID: string, minValue: number, maxValue: number, unit: string, callback: CallableFunction) { - this.#container = document.getElementById(ID); - this.#callback = callback; - this.#minValue = minValue; - this.#maxValue = maxValue; - this.#unit = unit; - if (this.#container != null) { - this.#display = this.#container.style.display; - this.#slider = this.#container.querySelector("input"); - if (this.#slider != null) - { - this.#slider.addEventListener("input", (e: any) => this.#onInput()); - this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize()); - } - this.#value = this.#container.querySelector("#value"); +export class Slider extends Control { + #callback: CallableFunction | null = null; + #slider: HTMLInputElement | null = null; + #valueText: HTMLElement | null = null; + #minValue: number = 0; + #maxValue: number = 0; + #increment: number = 0; + #minMaxValueDiv: HTMLElement | null = null; + #unitOfMeasure: string; + #dragged: boolean = false; + #value: number = 0; + + constructor(ID: string, minValue: number, maxValue: number, unitOfMeasure: string, callback: CallableFunction) { + super(ID); + this.#callback = callback; + this.#unitOfMeasure = unitOfMeasure; + this.#slider = this.getContainer()?.querySelector("input") as HTMLInputElement; + + if (this.#slider != null) { + this.#slider.addEventListener("input", (e: any) => this.#update()); + this.#slider.addEventListener("mousedown", (e: any) => this.#onStart()); + this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize()); + } + + this.#valueText = this.getContainer()?.querySelector(".ol-slider-value") as HTMLElement; + this.#minMaxValueDiv = this.getContainer()?.querySelector(".ol-slider-min-max") as HTMLElement; + + this.setIncrement(1); + this.setMinMax(minValue, maxValue); + } + + setActive(newActive: boolean) { + if (!this.getDragged()) { + this.getContainer()?.classList.toggle("active", newActive); + if (!newActive && this.#valueText != null) + this.#valueText.innerText = "Mixed values"; } } - #onValue() - { - if (this.#value != null && this.#slider != null) - this.#value.innerHTML = this.#minValue + Math.round(parseFloat(this.#slider.value) / 100 * (this.#maxValue - this.#minValue)) + this.#unit + setMinMax(newMinValue: number, newMaxValue: number) { + if (this.#minValue != newMinValue || this.#maxValue != newMaxValue) { + this.#minValue = newMinValue; + this.#maxValue = newMaxValue; + this.#updateMaxValue(); + + if (this.#minMaxValueDiv != null) { + this.#minMaxValueDiv.setAttribute('data-min-value', `${this.#minValue}${this.#unitOfMeasure}`); + this.#minMaxValueDiv.setAttribute('data-max-value', `${this.#maxValue}${this.#unitOfMeasure}`); + } + } + } + + setIncrement(newIncrement: number) { + if (this.#increment != newIncrement) { + this.#increment = newIncrement; + this.#updateMaxValue(); + } + } + + setValue(newValue: number, ignoreExpectedValue: boolean = true) { + if (!this.getDragged() && (ignoreExpectedValue || this.checkExpectedValue(newValue))) { + this.#value = newValue; + if (this.#slider != null) + this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * parseFloat(this.#slider.max)); + this.#update(); + } + } + + getValue() { + return this.#value; + } + + setDragged(newDragged: boolean) { + this.#dragged = newDragged; + } + + getDragged() { + return this.#dragged; + } + + #updateMaxValue() { + var oldValue = this.getValue(); + if (this.#slider != null) + this.#slider.max = String((this.#maxValue - this.#minValue) / this.#increment); + this.setValue(oldValue); + } + + #update() { + if (this.#valueText != null && this.#slider != null) + { + /* Update the text value */ + var value = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); + var strValue = String(value); + if (value > 1000) + strValue = String(Math.floor(value / 1000)) + "," + zeroPad(value - Math.floor(value / 1000) * 1000, 3); + this.#valueText.innerText = `${strValue} ${this.#unitOfMeasure.toUpperCase()}`; + + /* Update the position of the slider */ + var percentValue = parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * 90 + 5; + this.#slider.style.background = `linear-gradient(to right, var(--accent-light-blue) 5%, var(--accent-light-blue) ${percentValue}%, var(--background-grey) ${percentValue}%, var(--background-grey) 100%)` + } this.setActive(true); } - #onInput() - { - this.#onValue(); + #onStart() { + this.setDragged(true); } - #onFinalize() - { - if (this.#slider != null) - this.#callback(this.#minValue + parseFloat(this.#slider.value) / 100 * (this.#maxValue - this.#minValue)); - } - - show() - { - if (this.#container != null) - this.#container.style.display = this.#display; - } - - hide() - { - if (this.#container != null) - this.#container.style.display = 'none'; - } - - setActive(newActive: boolean) - { - if (this.#container) - { - this.#container.classList.toggle("active", newActive); - if (!newActive && this.#value != null) - this.#value.innerHTML = "Mixed values" + #onFinalize() { + this.setDragged(false); + if (this.#slider != null) { + this.resetExpectedValue(); + this.setValue(this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } } } - - setMinMax(newMinValue: number, newMaxValue: number) - { - this.#minValue = newMinValue; - this.#maxValue = newMaxValue; - } - - setValue(newValue: number) - { - if (this.#slider != null) - this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * 100); - this.#onValue() - } } \ No newline at end of file diff --git a/client/src/controls/switch.ts b/client/src/controls/switch.ts new file mode 100644 index 00000000..60f9e3e5 --- /dev/null +++ b/client/src/controls/switch.ts @@ -0,0 +1,46 @@ +import { Control } from "./control"; + +export class Switch extends Control { + #value: boolean | undefined = false; + #callback: CallableFunction | null = null; + + constructor(ID: string, callback: CallableFunction, initialValue?: boolean) { + super(ID); + this.getContainer()?.addEventListener('click', (e) => this.#onToggle()); + this.setValue(initialValue !== undefined? initialValue: true); + + this.#callback = callback; + + /* Add the toggle itself to the document */ + const container = this.getContainer(); + if (container != undefined){ + const width = getComputedStyle(container).width; + const height = getComputedStyle(container).height; + var el = document.createElement("div"); + el.classList.add("ol-switch-fill"); + el.style.setProperty("--width", width? width: "0"); + el.style.setProperty("--height", height? height: "0"); + this.getContainer()?.appendChild(el); + } + } + + setValue(newValue: boolean | undefined, ignoreExpectedValue: boolean = true) { + if (ignoreExpectedValue || this.checkExpectedValue(newValue)) { + this.#value = newValue; + this.getContainer()?.setAttribute("data-value", String(newValue)); + } + } + + getValue() { + return this.#value; + } + + #onToggle() { + this.resetExpectedValue(); + this.setValue(!this.getValue()); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } + } +} \ No newline at end of file diff --git a/client/src/controls/unitcontextmenu.ts b/client/src/controls/unitcontextmenu.ts new file mode 100644 index 00000000..15ce7227 --- /dev/null +++ b/client/src/controls/unitcontextmenu.ts @@ -0,0 +1,56 @@ +import { deg2rad, ftToM } from "../other/utils"; +import { ContextMenu } from "./contextmenu"; + +export class UnitContextMenu extends ContextMenu { + #customFormationCallback: CallableFunction | null = null; + + constructor(id: string) { + super(id); + + document.addEventListener("applyCustomFormation", () => { + var dialog = document.getElementById("custom-formation-dialog"); + if (dialog) { + dialog.classList.add("hide"); + var clock = 1; + while (clock < 8) { + if ((dialog.querySelector(`#formation-${clock}`)).checked) + break + clock++; + } + var angleDeg = 360 - (clock - 1) * 45; + var angleRad = deg2rad(angleDeg); + var distance = ftToM(parseInt((dialog.querySelector(`#distance`)?.querySelector("input")).value)); + var upDown = ftToM(parseInt((dialog.querySelector(`#up-down`)?.querySelector("input")).value)); + + // X: front-rear, positive front + // Y: top-bottom, positive top + // Z: left-right, positive right + + var x = distance * Math.cos(angleRad); + var y = upDown; + var z = distance * Math.sin(angleRad); + + if (this.#customFormationCallback) + this.#customFormationCallback({ "x": x, "y": y, "z": z }) + } + }) + } + + setCustomFormationCallback(callback: CallableFunction) { + this.#customFormationCallback = callback; + } + + setOptions(options: { [key: string]: {text: string, tooltip: string }}, callback: CallableFunction) { + this.getContainer()?.replaceChildren(...Object.keys(options).map((key: string, idx: number) => { + const option = options[key]; + var button = document.createElement("button"); + var el = document.createElement("div"); + el.title = option.tooltip; + el.innerText = option.text; + el.id = key; + button.addEventListener("click", () => callback(key)); + button.appendChild(el); + return (button); + })); + } +} \ No newline at end of file diff --git a/client/src/dcs/dcs.ts b/client/src/dcs/dcs.ts deleted file mode 100644 index c9ae53f7..00000000 --- a/client/src/dcs/dcs.ts +++ /dev/null @@ -1,244 +0,0 @@ -import * as L from 'leaflet' -import { getUnitsManager, setConnected } from '..'; -import { ConvertDDToDMS } from '../other/utils'; - -/* Edit here to change server address */ -var RESTaddress = "http://localhost:30000/restdemo"; - -export function getDataFromDCS(callback: CallableFunction) { - /* Request the updated unit data from the server */ - var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("GET", RESTaddress, true); - - xmlHttp.onload = function (e) { - var data = JSON.parse(xmlHttp.responseText); - callback(data); - setConnected(true); - }; - - xmlHttp.onerror = function () { - console.error("An error occurred during the XMLHttpRequest"); - setConnected(false); - }; - xmlHttp.send(null); -} - -export function addDestination(ID: number, path: any) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { }; - - var command = { "ID": ID, "path": path } - var data = { "setPath": command } - - xhr.send(JSON.stringify(data)); -} - -export function spawnSmoke(color: string, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - console.log("Added " + color + " smoke at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - - var command = { "color": color, "location": latlng }; - var data = { "smoke": command } - - xhr.send(JSON.stringify(data)); -} - -export function spawnGroundUnit(type: string, latlng: L.LatLng, coalition: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - console.log("Added " + coalition + " " + type + " at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - - var command = { "type": type, "location": latlng, "coalition": coalition }; - var data = { "spawnGround": command } - - xhr.send(JSON.stringify(data)); -} - -export function spawnAircraft(type: string, latlng: L.LatLng, coalition: string, payloadName: string | null = null, airbaseName: string | null = null) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - console.log("Added " + coalition + " " + type + " at " + ConvertDDToDMS(latlng.lat, false) + " " + ConvertDDToDMS(latlng.lng, true)); - } - }; - - var command = { "type": type, "location": latlng, "coalition": coalition, "payloadName": payloadName != null? payloadName: "", "airbaseName": airbaseName != null? airbaseName: ""}; - var data = { "spawnAir": command } - - xhr.send(JSON.stringify(data)); -} - -export function attackUnit(ID: number, targetID: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName); - } - }; - - var command = { "ID": ID, "targetID": targetID }; - var data = { "attackUnit": command } - - xhr.send(JSON.stringify(data)); -} - -export function cloneUnit(ID: number, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned"); - } - }; - - var command = { "ID": ID, "location": latlng }; - var data = { "cloneUnit": command } - - xhr.send(JSON.stringify(data)); -} - -export function landAt(ID: number, latlng: L.LatLng) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned"); - } - }; - - var command = { "ID": ID, "location": latlng }; - var data = { "landAt": command } - - xhr.send(JSON.stringify(data)); -} - -export function changeSpeed(ID: number, speedChange: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - - var command = {"ID": ID, "change": speedChange} - var data = {"changeSpeed": command} - - xhr.send(JSON.stringify(data)); -} - -export function setSpeed(ID: number, speed: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - - var command = {"ID": ID, "speed": speed} - var data = {"setSpeed": command} - - xhr.send(JSON.stringify(data)); -} - -export function changeAltitude(ID: number, altitudeChange: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " altitude change request: " + altitudeChange); - } - }; - - var command = {"ID": ID, "change": altitudeChange} - var data = {"changeAltitude": command} - - xhr.send(JSON.stringify(data)); -} - -export function setAltitude(ID: number, altitude: number) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - - var command = {"ID": ID, "altitude": altitude} - var data = {"setAltitude": command} - - xhr.send(JSON.stringify(data)); -} - -export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " created formation with: " + wingmenIDs); - } - }; - - var command = {"ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader} - var data = {"setLeader": command} - - xhr.send(JSON.stringify(data)); -} - -export function setROE(ID: number, ROE: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - - var command = {"ID": ID, "ROE": ROE} - var data = {"setROE": command} - - xhr.send(JSON.stringify(data)); -} - -export function setReactionToThreat(ID: number, reactionToThreat: string) { - var xhr = new XMLHttpRequest(); - xhr.open("PUT", RESTaddress); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange); - } - }; - - var command = {"ID": ID, "reactionToThreat": reactionToThreat} - var data = {"setReactionToThreat": command} - - xhr.send(JSON.stringify(data)); -} \ No newline at end of file diff --git a/client/src/features/featureswitches.ts b/client/src/features/featureswitches.ts new file mode 100644 index 00000000..a7cf1c30 --- /dev/null +++ b/client/src/features/featureswitches.ts @@ -0,0 +1,153 @@ +export interface FeatureSwitchInterface { + "defaultEnabled": boolean, // default on/off state (if allowed by forceState) + "forceState": number, // -1 don't force; 0 force off; 1 force on + "label": string, + "name": string, + "onEnabled"?: CallableFunction, + "options"?: object, + "removeArtifactsIfDisabled"?: boolean +} + + +class FeatureSwitch { + + // From config param + defaultEnabled; + forceState = -1; + label; + name; + onEnabled; + removeArtifactsIfDisabled = true; + + // Self-set + userPreference; + + + constructor(config: FeatureSwitchInterface) { + + this.defaultEnabled = config.defaultEnabled; + this.forceState = config.forceState; + this.label = config.label; + this.name = config.name; + this.onEnabled = config.onEnabled; + + this.userPreference = this.getUserPreference(); + + } + + + getUserPreference() { + + let preferences = JSON.parse(localStorage.getItem("featureSwitches") || "{}"); + + return (preferences.hasOwnProperty(this.name)) ? preferences[this.name] : this.defaultEnabled; + + } + + + isEnabled() { + + if ( this.forceState === 0 ) { + return false; + } + + if ( this.forceState === 1 ) { + return true; + } + + return this.userPreference; + } + +} + +export class FeatureSwitches { + + #featureSwitches: FeatureSwitch[] = [ + + new FeatureSwitch({ + "defaultEnabled": false, + "forceState": -1, + "label": "AIC", + "name": "aic" + }), + + new FeatureSwitch({ + "defaultEnabled": false, + "forceState": -1, + "label": "AI Formations", + "name": "ai-formations", + "removeArtifactsIfDisabled": false + }), + + new FeatureSwitch({ + "defaultEnabled": false, + "forceState": 1, + "label": "ATC", + "name": "atc" + }), + + new FeatureSwitch({ + "defaultEnabled": false, + "forceState": -1, + "label": "Force show unit control panel", + "name": "forceShowUnitControlPanel" + }), + + new FeatureSwitch({ + "defaultEnabled": true, + "forceState": -1, + "label": "Show splash screen", + "name": "splashScreen" + }) + + ]; + + + constructor() { + + this.#testSwitches(); + + this.savePreferences(); + + } + + + getSwitch(switchName: string) { + + return this.#featureSwitches.find(featureSwitch => featureSwitch.name === switchName); + + } + + + #testSwitches() { + for (const featureSwitch of this.#featureSwitches) { + if (featureSwitch.isEnabled()) { + if (typeof featureSwitch.onEnabled === "function") { + featureSwitch.onEnabled(); + } + } else { + document.querySelectorAll("[data-feature-switch='" + featureSwitch.name + "']").forEach(el => { + if (featureSwitch.removeArtifactsIfDisabled === false) { + el.remove(); + } else { + el.classList.add("hide"); + } + }); + } + document.body.classList.toggle("feature-" + featureSwitch.name, featureSwitch.isEnabled()); + } + } + + savePreferences() { + + let preferences: any = {}; + + for (const featureSwitch of this.#featureSwitches) { + preferences[featureSwitch.name] = featureSwitch.isEnabled(); + } + + localStorage.setItem("featureSwitches", JSON.stringify(preferences)); + + } + +} \ No newline at end of file diff --git a/client/src/features/toggleablefeature.ts b/client/src/features/toggleablefeature.ts new file mode 100644 index 00000000..09f1723e --- /dev/null +++ b/client/src/features/toggleablefeature.ts @@ -0,0 +1,35 @@ +export abstract class ToggleableFeature { + + #status: boolean = false; + + + constructor(defaultStatus: boolean) { + + this.#status = defaultStatus; + + this.onStatusUpdate(); + + } + + + getStatus(): boolean { + return this.#status; + } + + + protected onStatusUpdate() { } + + + toggleStatus(force?: boolean): void { + + if (force) { + this.#status = force; + } else { + this.#status = !this.#status; + } + + this.onStatusUpdate(); + + } + +} \ No newline at end of file diff --git a/client/src/index.ts b/client/src/index.ts index 586cf390..e4760d01 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -1,99 +1,210 @@ import { Map } from "./map/map" -import { getDataFromDCS } from "./dcs/dcs" -import { SelectionWheel } from "./controls/selectionwheel"; import { UnitsManager } from "./units/unitsmanager"; import { UnitInfoPanel } from "./panels/unitinfopanel"; -import { SelectionScroll } from "./controls/selectionscroll"; -import { Dropdown } from "./controls/dropdown"; import { ConnectionStatusPanel } from "./panels/connectionstatuspanel"; -import { Button } from "./controls/button"; -import { MissionData } from "./missiondata/missiondata"; +import { MissionHandler } from "./missionhandler/missionhandler"; import { UnitControlPanel } from "./panels/unitcontrolpanel"; -import { MouseInfoPanel } from "./panels/mouseInfoPanel"; -import { Slider } from "./controls/slider"; +import { MouseInfoPanel } from "./panels/mouseinfopanel"; +import { AIC } from "./aic/aic"; +import { ATC } from "./atc/atc"; +import { FeatureSwitches } from "./features/featureswitches"; +import { LogPanel } from "./panels/logpanel"; +import { getConfig, getPaused, setAddress, setCredentials, setPaused, startUpdate, toggleDemoEnabled } from "./server/server"; +import { UnitDataTable } from "./atc/unitdatatable"; +import { keyEventWasInInput } from "./other/utils"; +import { Popup } from "./popups/popup"; +import { Dropdown } from "./controls/dropdown"; +import { HotgroupPanel } from "./panels/hotgrouppanel"; +import { SVGInjector } from "@tanem/svg-injector"; -/* TODO: should this be a class? */ var map: Map; -var selectionWheel: SelectionWheel; -var selectionScroll: SelectionScroll; var unitsManager: UnitsManager; -var missionData: MissionData; +var missionHandler: MissionHandler; + +var aic: AIC; +var atc: ATC; var unitInfoPanel: UnitInfoPanel; var connectionStatusPanel: ConnectionStatusPanel; var unitControlPanel: UnitControlPanel; var mouseInfoPanel: MouseInfoPanel; +var logPanel: LogPanel; +var hotgroupPanel: HotgroupPanel; -var scenarioDropdown: Dropdown; -var mapSourceDropdown: Dropdown; +var infoPopup: Popup; -var slowButton: Button; -var fastButton: Button; -var climbButton: Button; -var descendButton: Button; -var userVisibilityButton: Button; -var aiVisibilityButton: Button; -var weaponVisibilityButton: Button; -var deadVisibilityButton: Button; +var activeCoalition: string = "blue"; -var altitudeSlider: Slider; -var airspeedSlider: Slider; +var unitDataTable: UnitDataTable; -var connected: boolean; -var activeCoalition: string; +var featureSwitches; function setup() { - /* Initialize */ + featureSwitches = new FeatureSwitches(); + + /* Initialize base functionalitites */ map = new Map('map-container'); - selectionWheel = new SelectionWheel("selection-wheel"); - selectionScroll = new SelectionScroll("selection-scroll"); unitsManager = new UnitsManager(); + missionHandler = new MissionHandler(); + + /* Panels */ unitInfoPanel = new UnitInfoPanel("unit-info-panel"); unitControlPanel = new UnitControlPanel("unit-control-panel"); - scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Syria", "Marianas", "Nevada", "South Atlantic", "The channel"], () => { }); - mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option)); connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); - missionData = new MissionData(); + hotgroupPanel = new HotgroupPanel("hotgroup-panel"); + //logPanel = new LogPanel("log-panel"); - /* Unit control buttons */ - slowButton = new Button("slow-button", ["images/buttons/slow.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("slow"); }); - fastButton = new Button("fast-button", ["images/buttons/fast.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("fast"); }); - climbButton = new Button("climb-button", ["images/buttons/climb.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("climb"); }); - descendButton = new Button("descend-button", ["images/buttons/descend.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("descend"); }); + /* Popups */ + infoPopup = new Popup("info-popup"); - /* Unit control sliders */ - altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => getUnitsManager().selectedUnitsSetAltitude(value * 0.3048)); - airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => getUnitsManager().selectedUnitsSetSpeed(value / 1.94384)); + /* Controls */ + new Dropdown("app-icon", () => { }); - /* Visibility buttons */ - userVisibilityButton = new Button("user-visibility-button", ["images/buttons/user-full.svg", "images/buttons/user-partial.svg", "images/buttons/user-none.svg", "images/buttons/user-hidden.svg"], () => { }); - aiVisibilityButton = new Button("ai-visibility-button", ["images/buttons/ai-full.svg", "images/buttons/ai-partial.svg", "images/buttons/ai-none.svg", "images/buttons/ai-hidden.svg"], () => { }); - weaponVisibilityButton = new Button("weapon-visibility-button", ["images/buttons/weapon-partial.svg", "images/buttons/weapon-none.svg", "images/buttons/weapon-hidden.svg"], () => { }); - deadVisibilityButton = new Button("dead-visibility-button", ["images/buttons/dead.svg", "images/buttons/dead-hidden.svg"], () => { }); + /* Unit data table */ + unitDataTable = new UnitDataTable("unit-data-table"); - aiVisibilityButton.setState(1); - weaponVisibilityButton.setState(1); - deadVisibilityButton.setState(1); + /* AIC */ + let aicFeatureSwitch = featureSwitches.getSwitch("aic"); + if (aicFeatureSwitch?.isEnabled()) { + aic = new AIC(); + } - /* Default values */ - activeCoalition = "blue"; - connected = false; + /* ATC */ + let atcFeatureSwitch = featureSwitches.getSwitch("atc"); + if (atcFeatureSwitch?.isEnabled()) { + atc = new ATC(); + atc.startUpdates(); + } - requestUpdate(); + /* Setup event handlers */ + setupEvents(); + + /* Load the config file */ + getConfig(readConfig); } -function requestUpdate() { - getDataFromDCS(update); - /* Main update rate = 250ms is minimum time, equal to server update time. */ - setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); - connectionStatusPanel.update(getConnected()); +function readConfig(config: any) { + if (config && config["address"] != undefined && config["port"] != undefined) { + const address = config["address"]; + const port = config["port"]; + if (typeof address === 'string' && typeof port == 'number') + setAddress(address == "*" ? window.location.hostname : address, port); + } + else { + throw new Error('Could not read configuration file'); + } } -export function update(data: JSON) { - unitsManager.update(data); - missionData.update(data); +function setupEvents() { + + /* Generic clicks */ + document.addEventListener("click", (ev) => { + if (ev instanceof MouseEvent && ev.target instanceof HTMLElement) { + const target = ev.target; + + if (target.classList.contains("olympus-dialog-close")) { + target.closest("div.olympus-dialog")?.classList.add("hide"); + } + + const triggerElement = target.closest("[data-on-click]"); + + if (triggerElement instanceof HTMLElement) { + const eventName: string = triggerElement.dataset.onClick || ""; + let params = JSON.parse(triggerElement.dataset.onClickParams || "{}"); + params._element = triggerElement; + + if (eventName) { + document.dispatchEvent(new CustomEvent(eventName, { + detail: params + })); + } + } + } + }); + + /* Keyup events */ + document.addEventListener("keyup", ev => { + if (keyEventWasInInput(ev)) { + return; + } + switch (ev.code) { + case "KeyL": + document.body.toggleAttribute("data-hide-labels"); + break; + case "KeyT": + toggleDemoEnabled(); + break; + case "Quote": + unitDataTable.toggle(); + break + case "Space": + setPaused(!getPaused()); + break; + case "KeyW": case "KeyA": case "KeyS": case "KeyD": + case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": + getMap().handleMapPanning(ev); + break; + case "Digit1": case "Digit2": case "Digit3": case "Digit4": case "Digit5": case "Digit6": case "Digit7": case "Digit8": case "Digit9": + // Using the substring because the key will be invalid when pressing the Shift key + if (ev.ctrlKey && ev.shiftKey) + getUnitsManager().selectedUnitsAddToHotgroup(parseInt(ev.code.substring(5))); + else if (ev.ctrlKey && !ev.shiftKey) + getUnitsManager().selectedUnitsSetHotgroup(parseInt(ev.code.substring(5))); + else + getUnitsManager().selectUnitsByHotgroup(parseInt(ev.code.substring(5))); + break; + } + }); + + /* Keydown events */ + document.addEventListener("keydown", ev => { + if (keyEventWasInInput(ev)) { + return; + } + switch (ev.code) { + case "KeyW": case "KeyA": case "KeyS": case "KeyD": case "ArrowLeft": case "ArrowRight": case "ArrowUp": case "ArrowDown": + getMap().handleMapPanning(ev); + break; + } + }); + + document.addEventListener("closeDialog", (ev: CustomEventInit) => { + ev.detail._element.closest(".ol-dialog").classList.add("hide"); + }); + + document.addEventListener("toggleElements", (ev: CustomEventInit) => { + document.querySelectorAll(ev.detail.selector).forEach(el => { + el.classList.toggle("hide"); + }) + }); + + document.addEventListener("tryConnection", () => { + const form = document.querySelector("#splash-content")?.querySelector("#authentication-form"); + const username = ((form?.querySelector("#username"))).value; + const password = ((form?.querySelector("#password"))).value; + setCredentials(username, btoa("admin" + ":" + password)); + + /* Start periodically requesting updates */ + startUpdate(); + + setConnectionStatus("connecting"); + }) + + document.addEventListener("reloadPage", () => { + location.reload(); + }) + + document.querySelectorAll("[inject-svg]").forEach((el: Element) => { + var img = el as HTMLImageElement; + var isLoaded = img.complete && img.naturalHeight !== 0; + if (isLoaded) + SVGInjector(img); + else + img.onload = () => SVGInjector(img); + }) + } export function getMap() { @@ -101,15 +212,11 @@ export function getMap() { } export function getMissionData() { - return missionData; + return missionHandler; } -export function getSelectionWheel() { - return selectionWheel; -} - -export function getSelectionScroll() { - return selectionScroll; +export function getUnitDataTable() { + return unitDataTable; } export function getUnitsManager() { @@ -128,76 +235,35 @@ export function getMouseInfoPanel() { return mouseInfoPanel; } +export function getLogPanel() { + return logPanel; +} + +export function getConnectionStatusPanel() { + return connectionStatusPanel; +} + +export function getHotgroupPanel() { + return hotgroupPanel; +} + export function setActiveCoalition(newActiveCoalition: string) { activeCoalition = newActiveCoalition; + document.querySelectorAll('[data-active-coalition]').forEach((element: any) => { element.setAttribute("data-active-coalition", activeCoalition) }); } export function getActiveCoalition() { return activeCoalition; } -export function setConnected(newConnected: boolean) { - connected = newConnected +export function setConnectionStatus(status: string) { + const el = document.querySelector("#connection-status") as HTMLElement; + if (el) + el.dataset["status"] = status; } -export function getConnected() { - return connected; -} - -export function getVisibilitySettings() { - var visibility = { - user: "", - ai: "", - weapon: "", - dead: "" - }; - - switch (userVisibilityButton.getState()) { - case 0: - visibility.user = "full"; break; - case 1: - visibility.user = "partial"; break; - case 2: - visibility.user = "none"; break; - case 3: - visibility.user = "hidden"; break; - } - - switch (aiVisibilityButton.getState()) { - case 0: - visibility.ai = "full"; break; - case 1: - visibility.ai = "partial"; break; - case 2: - visibility.ai = "none"; break; - case 3: - visibility.ai = "hidden"; break; - } - - switch (weaponVisibilityButton.getState()) { - case 0: - visibility.weapon = "partial"; break; - case 1: - visibility.weapon = "none"; break; - case 2: - visibility.weapon = "hidden"; break; - } - - switch (deadVisibilityButton.getState()) { - case 0: - visibility.dead = "none"; break; - case 1: - visibility.dead = "hidden"; break; - } - return visibility; -} - -export function getVisibilityButtons() { - return {user: userVisibilityButton, ai: aiVisibilityButton, weapon: weaponVisibilityButton, dead: deadVisibilityButton} -} - -export function getUnitControlSliders() { - return {altitude: altitudeSlider, airspeed: airspeedSlider} +export function getInfoPopup() { + return infoPopup; } window.onload = setup; \ No newline at end of file diff --git a/client/src/map/boxselect.ts b/client/src/map/boxselect.ts index 5a742bea..2ea92fd8 100644 --- a/client/src/map/boxselect.ts +++ b/client/src/map/boxselect.ts @@ -1,10 +1,10 @@ import { Map } from 'leaflet'; -import { Handler} from 'leaflet'; +import { Handler } from 'leaflet'; import { Util } from 'leaflet'; import { DomUtil } from 'leaflet'; import { DomEvent } from 'leaflet'; import { LatLngBounds } from 'leaflet'; -import { Bounds } from 'leaflet'; +import { Bounds } from 'leaflet'; export var BoxSelect = Handler.extend({ initialize: function (map: Map) { @@ -45,25 +45,28 @@ export var BoxSelect = Handler.extend({ }, _onMouseDown: function (e: any) { - if (((e.which !== 1) && (e.button !== 0))) { return false; } + if ((e.which == 1 && e.button == 0 && e.shiftKey)) { - // Clear the deferred resetState if it hasn't executed yet, otherwise it - // will interrupt the interaction and orphan a box element in the container. - this._clearDeferredResetState(); - this._resetState(); + // Clear the deferred resetState if it hasn't executed yet, otherwise it + // will interrupt the interaction and orphan a box element in the container. + this._clearDeferredResetState(); + this._resetState(); - DomUtil.disableTextSelection(); - DomUtil.disableImageDrag(); + DomUtil.disableTextSelection(); + DomUtil.disableImageDrag(); - this._startPoint = this._map.mouseEventToContainerPoint(e); + this._startPoint = this._map.mouseEventToContainerPoint(e); - //@ts-ignore - DomEvent.on(document, { - contextmenu: DomEvent.stop, - mousemove: this._onMouseMove, - mouseup: this._onMouseUp, - keydown: this._onKeyDown - }, this); + //@ts-ignore + DomEvent.on(document, { + contextmenu: DomEvent.stop, + mousemove: this._onMouseMove, + mouseup: this._onMouseUp, + keydown: this._onKeyDown + }, this); + } else { + return false; + } }, _onMouseMove: function (e: any) { @@ -79,12 +82,12 @@ export var BoxSelect = Handler.extend({ this._point = this._map.mouseEventToContainerPoint(e); var bounds = new Bounds(this._point, this._startPoint), - size = bounds.getSize(); + size = bounds.getSize(); if (bounds.min != undefined) DomUtil.setPosition(this._box, bounds.min); - this._box.style.width = size.x + 'px'; + this._box.style.width = size.x + 'px'; this._box.style.height = size.y + 'px'; }, @@ -110,16 +113,16 @@ export var BoxSelect = Handler.extend({ if ((e.which !== 1) && (e.button !== 0)) { return; } this._finish(); - + if (!this._moved) { return; } // Postpone to next JS tick so internal click event handling // still see it as "moved". - setTimeout(Util.bind(this._resetState, this), 0); + window.setTimeout(Util.bind(this._resetState, this), 0); var bounds = new LatLngBounds( this._map.containerPointToLatLng(this._startPoint), this._map.containerPointToLatLng(this._point)); - - this._map.fire('selectionend', {selectionBounds: bounds}); + + this._map.fire('selectionend', { selectionBounds: bounds }); }, _onKeyDown: function (e: any) { diff --git a/client/src/map/clickableminimap.ts b/client/src/map/clickableminimap.ts new file mode 100644 index 00000000..00104f7e --- /dev/null +++ b/client/src/map/clickableminimap.ts @@ -0,0 +1,12 @@ +import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map"; + +export class ClickableMiniMap extends MiniMap { + constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) { + super(layer, options); + } + + getMap() { + //@ts-ignore needed to access not exported member. A bit of a hack, required to access click events //TODO: fix me + return this._miniMap; + } +} \ No newline at end of file diff --git a/client/src/map/custommarker.ts b/client/src/map/custommarker.ts new file mode 100644 index 00000000..a6ca5998 --- /dev/null +++ b/client/src/map/custommarker.ts @@ -0,0 +1,25 @@ +import { DivIcon, Map, Marker } from "leaflet"; +import { MarkerOptions } from "leaflet"; +import { LatLngExpression } from "leaflet"; + +export class CustomMarker extends Marker { + constructor(latlng: LatLngExpression, options?: MarkerOptions) { + super(latlng, options); + } + + onAdd(map: Map): this { + this.setIcon(new DivIcon()); // Default empty icon + super.onAdd(map); + this.createIcon(); + return this; + } + + onRemove(map: Map): this { + super.onRemove(map); + return this; + } + + createIcon() { + /* Overloaded by child classes */ + } +} \ No newline at end of file diff --git a/client/src/map/destinationpreviewmarker.ts b/client/src/map/destinationpreviewmarker.ts new file mode 100644 index 00000000..aa76f395 --- /dev/null +++ b/client/src/map/destinationpreviewmarker.ts @@ -0,0 +1,15 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class DestinationPreviewMarker extends CustomMarker { + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-destination-preview", + })); + var el = document.createElement("div"); + el.classList.add("ol-destination-preview-icon"); + this.getElement()?.appendChild(el); + } +} diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 5dbc9089..04b85d28 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -1,159 +1,244 @@ import * as L from "leaflet" -import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition, getMouseInfoPanel } from ".."; -import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../dcs/dcs"; -import { bearing, distance, zeroAppend } from "../other/utils"; -import { payloadNames } from "../units/payloadNames"; -import { unitTypes } from "../units/unitTypes"; +import { getUnitsManager } from ".."; import { BoxSelect } from "./boxselect"; +import { MapContextMenu, SpawnOptions } from "../controls/mapcontextmenu"; +import { UnitContextMenu } from "../controls/unitcontextmenu"; +import { AirbaseContextMenu } from "../controls/airbasecontextmenu"; +import { Dropdown } from "../controls/dropdown"; +import { Airbase } from "../missionhandler/airbase"; +import { Unit } from "../units/unit"; +import { bearing } from "../other/utils"; +import { DestinationPreviewMarker } from "./destinationpreviewmarker"; +import { TemporaryUnitMarker } from "./temporaryunitmarker"; +import { ClickableMiniMap } from "./clickableminimap"; +import { SVGInjector } from '@tanem/svg-injector' +import { layers as mapLayers, mapBounds, minimapBoundaries } from "../constants/constants"; +import { TargetMarker } from "./targetmarker"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); -export interface ClickEvent { - x: number; - y: number; - latlng: L.LatLng; -} +// TODO would be nice to convert to ts +require("../../public/javascripts/leaflet.nauticscale.js") -export interface SpawnEvent extends ClickEvent{ - airbaseName: string | null; - coalitionID: number | null; -} +/* Map constants */ +export const IDLE = "Idle"; +export const MOVE_UNIT = "Move unit"; +export const BOMBING = "Bombing"; +export const CARPET_BOMBING = "Carpet bombing"; +export const FIRE_AT_AREA = "Fire at area"; +export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; +export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; export class Map extends L.Map { + #ID: string; #state: string; - #layer?: L.TileLayer; + #layer: L.TileLayer | null = null; #preventLeftClick: boolean = false; #leftClickTimer: number = 0; - #measurePoint: L.LatLng | null; - #measureIcon: L.Icon; - #measureMarker: L.Marker; - #measureLine: L.Polyline = new L.Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1, interactive: false }); - #measureLineDiv: HTMLElement; + #deafultPanDelta: number = 100; + #panInterval: number | null = null; + #panLeft: boolean = false; + #panRight: boolean = false; + #panUp: boolean = false; + #panDown: boolean = false; #lastMousePosition: L.Point = new L.Point(0, 0); + #centerUnit: Unit | null = null; + #miniMap: ClickableMiniMap | null = null; + #miniMapLayerGroup: L.LayerGroup; + #temporaryMarkers: TemporaryUnitMarker[] = []; + #destinationPreviewMarkers: DestinationPreviewMarker[] = []; + #targetMarker: TargetMarker; + #destinationGroupRotation: number = 0; + #computeDestinationRotation: boolean = false; + #destinationRotationCenter: L.LatLng | null = null; + + #mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu"); + #unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu"); + #airbaseContextMenu: AirbaseContextMenu = new AirbaseContextMenu("airbase-contextmenu"); + + #mapSourceDropdown: Dropdown; + #optionButtons: { [key: string]: HTMLButtonElement[] } = {} constructor(ID: string) { /* Init the leaflet map */ + //@ts-ignore - super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true }); - this.setView([37.23, -115.8], 12); + super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); + this.setView([37.23, -115.8], 10); - this.setLayer("ArcGIS Satellite"); + this.#ID = ID; + + this.setLayer(Object.keys(mapLayers)[0]); + + /* Minimap */ + var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 }); + this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); + var miniMapPolyline = new L.Polyline(this.#getMinimapBoundaries(), { color: '#202831' }); + miniMapPolyline.addTo(this.#miniMapLayerGroup); + + /* Scale */ + //@ts-ignore TODO more hacking because the module is provided as a pure javascript module only + L.control.scalenautic({ position: "topright", maxWidth: 300, nautic: true, metric: true, imperial: false }).addTo(this); + + /* Map source dropdown */ + this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName), this.getLayers()) /* Init the state machine */ - this.#state = "IDLE"; - this.#measurePoint = null; - - this.#measureIcon = new L.Icon({ iconUrl: 'images/pin.png', iconAnchor: [16, 32]}); - this.#measureMarker = new L.Marker([0, 0], {icon: this.#measureIcon, interactive: false}); - this.#measureLineDiv = document.createElement("div"); - this.#measureLineDiv.classList.add("measure-box"); - this.#measureLineDiv.style.display = 'none'; - - document.body.appendChild(this.#measureLineDiv); + this.#state = IDLE; /* Register event handles */ this.on("click", (e: any) => this.#onClick(e)); - this.on("dblclick", (e: any) => this.#onDoubleClick(e)); + this.on("dblclick", (e: any) => this.#onDoubleClick(e)); + this.on("zoomstart", (e: any) => this.#onZoom(e)); + this.on("drag", (e: any) => this.centerOnUnit(null)); this.on("contextmenu", (e: any) => this.#onContextMenu(e)); this.on('selectionend', (e: any) => this.#onSelectionEnd(e)); this.on('mousedown', (e: any) => this.#onMouseDown(e)); this.on('mouseup', (e: any) => this.#onMouseUp(e)); this.on('mousemove', (e: any) => this.#onMouseMove(e)); - this.on('zoom', (e: any) => this.#onZoom(e)); + this.on('keydown', (e: any) => this.#updateDestinationPreview(e)); + this.on('keyup', (e: any) => this.#updateDestinationPreview(e)); + + /* Event listeners */ + document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { + const el = ev.detail._element; + el?.classList.toggle("off"); + getUnitsManager().setHiddenType(ev.detail.coalition, (el?.currentTarget as HTMLElement)?.classList.contains("off")); + Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); + }); + + document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { + const el = ev.detail._element; + el?.classList.toggle("off"); + getUnitsManager().setHiddenType(ev.detail.type, !el?.classList.contains("off")); + Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); + }); + + document.addEventListener("unitUpdated", (ev: CustomEvent) => { + if (this.#centerUnit != null && ev.detail == this.#centerUnit) + this.#panToUnit(this.#centerUnit); + }); + + /* Pan interval */ + this.#panInterval = window.setInterval(() => { + if (this.#panLeft || this.#panDown || this.#panRight || this.#panLeft) + this.panBy(new L.Point(((this.#panLeft? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta, + ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta)); + }, 20); + + /* Option buttons */ + this.#optionButtons["visibility"] = visibilityControls.map((option: string, index: number) => { + return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleUnitVisibility", `{"type": "${option}"}`); + }); + document.querySelector("#unit-visibility-control")?.append(...this.#optionButtons["visibility"]); + + /* Markers */ + this.#targetMarker = new TargetMarker(new L.LatLng(0, 0), {interactive: false}); } setLayer(layerName: string) { - if (this.#layer != null) { + if (this.#layer != null) this.removeLayer(this.#layer) + + if (layerName in mapLayers){ + const layerData = mapLayers[layerName as keyof typeof mapLayers]; + var options: L.TileLayerOptions = { + attribution: layerData.attribution, + minZoom: layerData.minZoom, + maxZoom: layerData.maxZoom + }; + this.#layer = new L.TileLayer(layerData.urlTemplate, options); } - if (layerName == "ArcGIS Satellite") { - this.#layer = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { - attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" - }); - } - else if (layerName == "USGS Topo") { - this.#layer = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', { - maxZoom: 20, - attribution: 'Tiles courtesy of the U.S. Geological Survey' - }); - } - else if (layerName == "OpenStreetMap Mapnik") { - this.#layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap contributors' - }); - } - else if (layerName == "OPENVKarte") { - this.#layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' - }); - } - else if (layerName == "Esri.DeLorme") { - this.#layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', - minZoom: 1, - maxZoom: 11 - }); - } - else if (layerName == "CyclOSM") { - this.#layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { - maxZoom: 20, - attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' - }); - } this.#layer?.addTo(this); } getLayers() { - return ["ArcGIS Satellite", "USGS Topo", "OpenStreetMap Mapnik", "OPENVKarte", "Esri.DeLorme", "CyclOSM"] + return Object.keys(mapLayers); } /* State machine */ setState(state: string) { this.#state = state; - - if (this.#state === "IDLE") { - L.DomUtil.removeClass(this.getContainer(),'crosshair-cursor-enabled'); + if (this.#state === IDLE) { + this.#resetDestinationMarkers(); + this.#resetTargetMarker(); + this.#showCursor(); } - else if (this.#state === "MOVE_UNIT") { - L.DomUtil.addClass(this.getContainer(),'crosshair-cursor-enabled'); + else if (this.#state === MOVE_UNIT) { + this.#resetTargetMarker(); + this.#createDestinationMarkers(); + if (this.#destinationPreviewMarkers.length > 0) + this.#hideCursor(); } - else if (this.#state === "ATTACK") { - - } - else if (this.#state === "FORMATION") { - + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#resetDestinationMarkers(); + this.#createTargetMarker(); + this.#hideCursor(); } + document.dispatchEvent(new CustomEvent("mapStateChanged")); } getState() { return this.#state; } - /* Selection wheel */ - showSelectionWheel(e: ClickEvent | SpawnEvent, options: any, showCoalition: boolean) { - var x = e.x; - var y = e.y; - getSelectionWheel().show(x, y, options, showCoalition); + /* Context Menus */ + hideAllContextMenus() { + this.hideMapContextMenu(); + this.hideUnitContextMenu(); + this.hideAirbaseContextMenu(); } - hideSelectionWheel() { - getSelectionWheel().hide(); + showMapContextMenu(e: any) { + this.hideAllContextMenus(); + var x = e.originalEvent.x; + var y = e.originalEvent.y; + this.#mapContextMenu.show(x, y, e.latlng); + document.dispatchEvent(new CustomEvent("mapContextMenu")); } - /* Selection scroll */ - showSelectionScroll(e: ClickEvent | SpawnEvent, title: string, options: any, callback: CallableFunction, showCoalition: boolean = false) { - var x = e.x; - var y = e.y; - getSelectionScroll().show(x, y, title, options, callback, showCoalition); + hideMapContextMenu() { + this.#mapContextMenu.hide(); + document.dispatchEvent(new CustomEvent("mapContextMenu")); } - hideSelectionScroll() { - getSelectionScroll().hide(); + getMapContextMenu() { + return this.#mapContextMenu; } + showUnitContextMenu(e: any) { + this.hideAllContextMenus(); + var x = e.originalEvent.x; + var y = e.originalEvent.y; + this.#unitContextMenu.show(x, y, e.latlng); + } + + getUnitContextMenu() { + return this.#unitContextMenu; + } + + hideUnitContextMenu() { + this.#unitContextMenu.hide(); + } + + showAirbaseContextMenu(e: any, airbase: Airbase) { + this.hideAllContextMenus(); + var x = e.originalEvent.x; + var y = e.originalEvent.y; + this.#airbaseContextMenu.show(x, y, e.latlng); + this.#airbaseContextMenu.setAirbase(airbase); + } + + getAirbaseContextMenu() { + return this.#airbaseContextMenu; + } + + hideAirbaseContextMenu() { + this.#airbaseContextMenu.hide(); + } + + /* Mouse coordinates */ getMousePosition() { return this.#lastMousePosition; } @@ -162,253 +247,291 @@ export class Map extends L.Map { return this.containerPointToLatLng(this.#lastMousePosition); } + /* Spawn from air base */ + spawnFromAirbase(e: any) { + //this.#aircraftSpawnMenu(e); + } + + centerOnUnit(ID: number | null) { + if (ID != null) { + this.options.scrollWheelZoom = 'center'; + this.#centerUnit = getUnitsManager().getUnitByID(ID); + } + else { + this.options.scrollWheelZoom = undefined; + this.#centerUnit = null; + } + } + + setTheatre(theatre: string) { + var bounds = new L.LatLngBounds([-90, -180], [90, 180]); + var miniMapZoom = 5; + if (theatre in mapBounds) { + bounds = mapBounds[theatre as keyof typeof mapBounds].bounds; + miniMapZoom = mapBounds[theatre as keyof typeof mapBounds].zoom; + } + + this.setView(bounds.getCenter(), 8); + //this.setMaxBounds(bounds); + + if (this.#miniMap) + this.#miniMap.remove(); + + //@ts-ignore // Needed because some of the inputs are wrong in the original module interface + this.#miniMap = new ClickableMiniMap(this.#miniMapLayerGroup, { position: "topright", width: 192 * 1.5, height: 108 * 1.5, zoomLevelFixed: miniMapZoom, centerFixed: bounds.getCenter() }).addTo(this); + this.#miniMap.disableInteractivity(); + this.#miniMap.getMap().on("click", (e: any) => { + if (this.#miniMap) + this.setView(e.latlng); + }) + } + + getMiniMapLayerGroup() { + return this.#miniMapLayerGroup; + } + + handleMapPanning(e: any) { + if (e.type === "keyup") { + switch (e.code) { + case "KeyA": + case "ArrowLeft": + this.#panLeft = false; + break; + case "KeyD": + case "ArrowRight": + this.#panRight = false; + break; + case "KeyW": + case "ArrowUp": + this.#panUp = false; + break; + case "KeyS": + case "ArrowDown": + this.#panDown = false; + break; + } + } + else { + switch (e.code) { + case 'KeyA': + case 'ArrowLeft': + this.#panLeft = true; + break; + case 'KeyD': + case 'ArrowRight': + this.#panRight = true; + break; + case 'KeyW': + case 'ArrowUp': + this.#panUp = true; + break; + case 'KeyS': + case 'ArrowDown': + this.#panDown = true; + break; + } + } + } + + addTemporaryMarker(latlng: L.LatLng) { + var marker = new TemporaryUnitMarker(latlng); + marker.addTo(this); + this.#temporaryMarkers.push(marker); + } + + removeTemporaryMarker(latlng: L.LatLng) { + var d: number | null = null; + var closest: L.Marker | null = null; + var i: number = 0; + this.#temporaryMarkers.forEach((marker: L.Marker, idx: number) => { + var t = latlng.distanceTo(marker.getLatLng()); + if (d == null || t < d) { + d = t; + closest = marker; + i = idx; + } + }); + if (closest) { + this.removeLayer(closest); + delete this.#temporaryMarkers[i]; + } + } + /* Event handlers */ #onClick(e: any) { if (!this.#preventLeftClick) { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - if (this.#state === "IDLE") { - if (e.originalEvent.ctrlKey) - if (!this.#measurePoint) - { - this.#measurePoint = e.latlng; - this.#measureMarker.setLatLng(e.latlng); - this.#measureMarker.addTo(this); - } - else - { - this.#measurePoint = null; - if (this.hasLayer(this.#measureMarker)) - this.removeLayer(this.#measureMarker); - } + this.hideAllContextMenus(); + if (this.#state === IDLE) { + } - else if (this.#state === "MOVE_UNIT") { - this.setState("IDLE"); + else { + this.setState(IDLE); getUnitsManager().deselectAllUnits(); - this.hideSelectionWheel(); - this.hideSelectionScroll(); } } } #onDoubleClick(e: any) { - + } #onContextMenu(e: any) { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - if (this.#state === "IDLE") { - var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null}; - if (this.#state == "IDLE") { - var options = [ - { "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) }, - { "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) }, - { "tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(spawnEvent) }, - //{ "tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e) } - ] - this.showSelectionScroll(spawnEvent, "Action", options, () => {}, false); + this.hideMapContextMenu(); + if (this.#state === IDLE) { + if (this.#state == IDLE) { + this.showMapContextMenu(e); } } - else if (this.#state === "MOVE_UNIT") { + else if (this.#state === MOVE_UNIT) { if (!e.originalEvent.ctrlKey) { getUnitsManager().selectedUnitsClearDestinations(); } - getUnitsManager().selectedUnitsAddDestination(e.latlng) + getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + } + else if (this.#state === BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + } + else if (this.#state === CARPET_BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + } + else if (this.#state === FIRE_AT_AREA) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); } } - #onSelectionEnd(e: any) - { + #executeAction(e: any, action: string) { + if (action === "bomb") + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + else if (action === "carpet-bomb") + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + else if (action === "building-bomb") + getUnitsManager().selectedUnitsBombBuilding(this.getMouseCoordinates()); + else if (action === "fire-at-area") + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); + } + + #onSelectionEnd(e: any) { clearTimeout(this.#leftClickTimer); this.#preventLeftClick = true; - this.#leftClickTimer = setTimeout(() => { - this.#preventLeftClick = false; + this.#leftClickTimer = window.setTimeout(() => { + this.#preventLeftClick = false; }, 200); getUnitsManager().selectFromBounds(e.selectionBounds); } - #onMouseDown(e: any) - { - if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0)) - { - this.dragging.disable(); + #onMouseDown(e: any) { + this.hideAllContextMenus(); + + if (this.#state == MOVE_UNIT) { + this.#destinationGroupRotation = 0; + this.#destinationRotationCenter = null; + this.#computeDestinationRotation = false; + if (e.originalEvent.button == 2) { + this.#computeDestinationRotation = true; + this.#destinationRotationCenter = this.getMouseCoordinates(); + } } } - #onMouseUp(e: any) - { - if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0)) - { - this.dragging.enable(); - } + #onMouseUp(e: any) { } - #onMouseMove(e: any) - { - var selectedUnitPosition = null; - var selectedUnits = getUnitsManager().getSelectedUnits(); - if (selectedUnits && selectedUnits.length == 1) - { - selectedUnitPosition = new L.LatLng(selectedUnits[0].latitude, selectedUnits[0].longitude); - } - getMouseInfoPanel().update(e.latlng, this.#measurePoint, selectedUnitPosition); - + #onMouseMove(e: any) { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; - if ( this.#measurePoint) - this.#drawMeasureLine(); - else - this.#hideMeasureLine(); - } - - #onZoom(e: any) - { - if (this.#measurePoint) - this.#drawMeasureLine(); - else - this.#hideMeasureLine(); - } - - /* Spawn from air base */ - spawnFromAirbase(e: SpawnEvent) - { - this.#aircraftSpawnMenu(e); - } - - /* Spawning menus */ - #groundUnitSpawnMenu(e: SpawnEvent) { - var options = [ - {'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.showSelectionScroll(e, "Spawn ground unit", options, () => {}, true); - } - - #smokeSpawnMenu(e: SpawnEvent) { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - var options = [ - {'tooltip': 'Red smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('red', e.latlng)}, 'tint': 'red'}, - {'tooltip': 'White smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('white', e.latlng)}, 'tint': 'white'}, - {'tooltip': 'Blue smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('blue', e.latlng)}, 'tint': 'blue'}, - {'tooltip': 'Green smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('green', e.latlng)}, 'tint': 'green'}, - {'tooltip': 'Orange smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('orange', e.latlng)}, 'tint': 'orange'}, - ] - this.showSelectionScroll(e, "Spawn smoke", options, () => {}, false); - } - - #explosionSpawnMenu(e: SpawnEvent) { - - } - - #aircraftSpawnMenu(e: SpawnEvent) { - 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") }, - ] - if (e.airbaseName != null) - this.showSelectionScroll(e, "Spawn at " + e.airbaseName, options, () => {}, true); - else - this.showSelectionScroll(e, "Spawn air unit", options, () => {}, true); - } - - /* Show unit selection for air units */ - #selectAircraft(e: SpawnEvent, group: string) { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - var options = unitTypes.air[group]; - if (options != undefined) - options.sort(); - else - options = []; - this.showSelectionScroll(e, "Select aircraft", options, (unitType: string) => { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - this.#unitSelectPayload(e, unitType); - }, true); - } - - /* Show weapon selection for air units */ - #unitSelectPayload(e: SpawnEvent, unitType: string) { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - var options = []; - options = payloadNames[unitType] - if (options != undefined && options.length > 0) { - options.sort(); - this.showSelectionScroll({x: e.x, y: e.y, latlng: e.latlng}, "Select loadout", options, (payloadName: string) => { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - spawnAircraft(unitType, e.latlng, getActiveCoalition(), payloadName, e.airbaseName); - }, true); + if (this.#state === MOVE_UNIT){ + if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) + this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); + this.#updateDestinationPreview(e); } - else { - spawnAircraft(unitType, e.latlng, getActiveCoalition()); + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#targetMarker.setLatLng(this.getMouseCoordinates()); } } - /* Show unit selection for ground units */ - #selectGroundUnit(e: any, group: string) - { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - var options = unitTypes.vehicles[group]; - options.sort(); - this.showSelectionScroll(e, "Select ground unit", options, (unitType: string) => { - this.hideSelectionWheel(); - this.hideSelectionScroll(); - spawnGroundUnit(unitType, e.latlng, getActiveCoalition()); - }, true); + #onZoom(e: any) { + if (this.#centerUnit != null) + this.#panToUnit(this.#centerUnit); } - #drawMeasureLine() - { - var mouseLatLng = this.containerPointToLatLng(this.#lastMousePosition); - if (this.#measurePoint != null) - { - var points = [this.#measurePoint, mouseLatLng]; - this.#measureLine.setLatLngs(points); - var dist = distance(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); - var bear = bearing(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); - var startXY = this.latLngToContainerPoint(this.#measurePoint); - var dx = (this.#lastMousePosition.x - startXY.x); - var dy = (this.#lastMousePosition.y - startXY.y); + #panToUnit(unit: Unit) { + var unitPosition = new L.LatLng(unit.getFlightData().latitude, unit.getFlightData().longitude); + this.setView(unitPosition, this.getZoom(), { animate: false }); + } - var angle = Math.atan2(dy, dx); - if (angle > Math.PI / 2) - angle = angle - Math.PI; + #getMinimapBoundaries() { + /* Draw the limits of the maps in the minimap*/ + return minimapBoundaries; + } - if (angle < -Math.PI / 2) - angle = angle + Math.PI; + #updateDestinationPreview(e: any) { + Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(), this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { + if (idx < this.#destinationPreviewMarkers.length) + this.#destinationPreviewMarkers[idx].setLatLng(e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); + }) + } - this.#measureLineDiv.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM` - this.#measureLineDiv.style.left = (this.#lastMousePosition.x + startXY.x) / 2 - this.#measureLineDiv.offsetWidth / 2 + "px"; - this.#measureLineDiv.style.top = (this.#lastMousePosition.y + startXY.y) / 2 - this.#measureLineDiv.offsetHeight / 2 + "px"; - this.#measureLineDiv.style.rotate = angle + "rad"; - this.#measureLineDiv.style.display = ""; + #createOptionButton(value: string, url: string, title: string, callback: string, argument: string) { + var button = document.createElement("button"); + const img = document.createElement("img"); + img.src = `/resources/theme/images/buttons/${url}`; + img.onload = () => SVGInjector(img); + button.title = title; + button.value = value; + button.appendChild(img); + button.setAttribute("data-on-click", callback); + button.setAttribute("data-on-click-params", argument); + return button; + } + + #createDestinationMarkers() { + this.#resetDestinationMarkers(); + + if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 0) { + /* Create the unit destination preview markers */ + this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }).map((unit: Unit) => { + var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); + marker.addTo(this); + return marker; + }) } - - if (!this.hasLayer(this.#measureLine)) - this.#measureLine.addTo(this); } - #hideMeasureLine() - { - this.#measureLineDiv.style.display = "none"; + #resetDestinationMarkers() { + /* Remove all the destination preview markers */ + this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { + this.removeLayer(marker); + }) + this.#destinationPreviewMarkers = []; - if (this.hasLayer(this.#measureLine)) - this.removeLayer(this.#measureLine) + this.#destinationGroupRotation = 0; + this.#computeDestinationRotation = false; + this.#destinationRotationCenter = null; + } + + #createTargetMarker(){ + this.#resetTargetMarker(); + this.#targetMarker.addTo(this); + } + + #resetTargetMarker() { + this.#targetMarker.setLatLng(new L.LatLng(0, 0)); + this.removeLayer(this.#targetMarker); + } + + #showCursor() { + document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); + } + + #hideCursor() { + document.getElementById(this.#ID)?.classList.add("hidden-cursor"); } } + diff --git a/client/src/map/targetmarker.ts b/client/src/map/targetmarker.ts new file mode 100644 index 00000000..30232dc1 --- /dev/null +++ b/client/src/map/targetmarker.ts @@ -0,0 +1,18 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class TargetMarker extends CustomMarker { + #interactive: boolean = false; + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-target-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-target-icon"); + el.classList.toggle("ol-target-icon-interactive", this.#interactive) + this.getElement()?.appendChild(el); + } +} diff --git a/client/src/map/temporaryunitmarker.ts b/client/src/map/temporaryunitmarker.ts new file mode 100644 index 00000000..904ee83d --- /dev/null +++ b/client/src/map/temporaryunitmarker.ts @@ -0,0 +1,13 @@ +import { Icon } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class TemporaryUnitMarker extends CustomMarker { + createIcon() { + var icon = new Icon({ + iconUrl: '/resources/theme/images/markers/temporary-icon.png', + iconSize: [52, 52], + iconAnchor: [26, 26] + }); + this.setIcon(icon); + } +} \ No newline at end of file diff --git a/client/src/missiondata/airbasemarker.ts b/client/src/missiondata/airbasemarker.ts deleted file mode 100644 index 3f934418..00000000 --- a/client/src/missiondata/airbasemarker.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as L from 'leaflet' - -export interface AirbaseOptions -{ - name: string, - position: L.LatLng, - src: string -} - -export class AirbaseMarker extends L.Marker -{ - #name: string = ""; - #coalitionID: number = -1; - - constructor(options: AirbaseOptions) - { - super(options.position, { riseOnHover: true }); - - this.#name = options.name; - - var icon = new L.DivIcon({ - html: ` - - - -
- -
${options.name}
-
`, - className: 'airbase-marker'}); // Set the marker, className must be set to avoid white square - this.setIcon(icon); - } - - setCoalitionID(coalitionID: number) - { - this.#coalitionID = coalitionID; - var element = this.getElement(); - if (element != null) - { - var img = element.querySelector("#icon"); - if (img != null) - { - img.classList.remove("airbasemarker-icon-blue"); - img.classList.remove("airbasemarker-icon-red"); - if (this.#coalitionID == 2) - { - img.classList.add("airbasemarker-icon-blue"); - } - else if (this.#coalitionID == 1) - { - img.classList.add("airbasemarker-icon-red"); - } - } - } - } - - getName() - { - return this.#name; - } - - getCoalitionID() - { - return this.#coalitionID; - } -} diff --git a/client/src/missiondata/missiondata.ts b/client/src/missiondata/missiondata.ts deleted file mode 100644 index f5ccedad..00000000 --- a/client/src/missiondata/missiondata.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Marker, LatLng, Icon } from "leaflet"; -import { getMap, getUnitsManager } from ".."; -import { SpawnEvent } from "../map/map"; -import { AirbaseMarker } from "./airbasemarker"; - -var bullseyeIcons = [ - new Icon({ iconUrl: 'images/bullseye0.png', iconAnchor: [30, 30]}), - new Icon({ iconUrl: 'images/bullseye1.png', iconAnchor: [30, 30]}), - new Icon({ iconUrl: 'images/bullseye2.png', iconAnchor: [30, 30]}) -] - -export class MissionData -{ - #bullseyes : any; //TODO declare interface - #bullseyeMarkers: any; - #airbases : any; //TODO declare interface - #airbasesMarkers: {[name: string]: AirbaseMarker}; - - constructor() - { - this.#bullseyes = undefined; - this.#bullseyeMarkers = [ - new Marker([0, 0], {icon: bullseyeIcons[0]}).addTo(getMap()), - new Marker([0, 0], {icon: bullseyeIcons[1]}).addTo(getMap()), - new Marker([0, 0], {icon: bullseyeIcons[2]}).addTo(getMap()) - ] - this.#airbasesMarkers = {}; - } - - update(data: any) - { - this.#bullseyes = data.bullseye; - this.#airbases = data.airbases; - if (this.#bullseyes != null && this.#airbases != null) - { - this.#drawBullseye(); - this.#drawAirbases(); - } - } - - getBullseyes() - { - return this.#bullseyes; - } - - #drawBullseye() - { - for (let idx in this.#bullseyes) - { - var bullseye = this.#bullseyes[idx]; - this.#bullseyeMarkers[idx].setLatLng(new LatLng(bullseye.lat, bullseye.lng)); - } - } - - #drawAirbases() - { - for (let idx in this.#airbases) - { - var airbase = this.#airbases[idx] - if (this.#airbasesMarkers[idx] === undefined) - { - this.#airbasesMarkers[idx] = new AirbaseMarker({ - position: new LatLng(airbase.lat, airbase.lng), - name: airbase.callsign, - src: "images/airbase.png"}).addTo(getMap()); - this.#airbasesMarkers[idx].on('contextmenu', (e) => this.#onAirbaseClick(e)); - } - else - { - this.#airbasesMarkers[idx].setCoalitionID(airbase.coalition); - } - } - } - - #onAirbaseClick(e: any) - { - var options = []; - if (getUnitsManager().getSelectedUnits().length > 0) - options = ["Spawn unit", "Land here"]; - else - options = ["Spawn unit"]; - getMap().showSelectionScroll(e.originalEvent, e.sourceTarget.getName(), options, (option: string) => this.#onAirbaseOptionSelection(e, option), false); - - } - - #onAirbaseOptionSelection(e: any, option: string) { - if (option === "Spawn unit") { - var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: e.sourceTarget.getName(), coalitionID: e.sourceTarget.getCoalitionID()}; - getMap().spawnFromAirbase(spawnEvent); - } - else if (option === "Land here") - { - getMap().hideSelectionWheel(); - getMap().hideSelectionScroll(); - getUnitsManager().selectedUnitsLandAt(e.latlng); - } - } -} \ No newline at end of file diff --git a/client/src/missionhandler/airbase.ts b/client/src/missionhandler/airbase.ts new file mode 100644 index 00000000..4128001b --- /dev/null +++ b/client/src/missionhandler/airbase.ts @@ -0,0 +1,83 @@ +import { DivIcon } from 'leaflet'; +import { CustomMarker } from '../map/custommarker'; +import { SVGInjector } from '@tanem/svg-injector'; + +export interface AirbaseOptions +{ + name: string, + position: L.LatLng +} + +export class Airbase extends CustomMarker +{ + #name: string = ""; + #coalition: string = ""; + #properties: string[] = []; + #parkings: string[] = []; + + constructor(options: AirbaseOptions) + { + super(options.position, { riseOnHover: true }); + + this.#name = options.name; + } + + createIcon() { + var icon = new DivIcon({ + className: 'leaflet-airbase-marker', + iconSize: [40, 40], + iconAnchor: [20, 20] + }); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("airbase-icon"); + el.setAttribute("data-object", "airbase"); + var img = document.createElement("img"); + img.src = "/resources/theme/images/markers/airbase.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + } + + setCoalition(coalition: string) + { + this.#coalition = coalition; + ( this.getElement()?.querySelector(".airbase-icon")).dataset.coalition = this.#coalition; + } + + getCoalition() + { + return this.#coalition; + } + + setName(name: string) + { + this.#name = name; + } + + getName() + { + return this.#name; + } + + setProperties(properties: string[]) + { + this.#properties = properties; + } + + getProperties() + { + return this.#properties; + } + + setParkings(parkings: string[]) + { + this.#parkings = parkings; + } + + getParkings() + { + return this.#parkings; + } +} diff --git a/client/src/missionhandler/bullseye.ts b/client/src/missionhandler/bullseye.ts new file mode 100644 index 00000000..b50b1a7b --- /dev/null +++ b/client/src/missionhandler/bullseye.ts @@ -0,0 +1,36 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "../map/custommarker"; +import { SVGInjector } from "@tanem/svg-injector"; + +export class Bullseye extends CustomMarker { + #coalition: string = ""; + + createIcon() { + var icon = new DivIcon({ + className: 'leaflet-bullseye-marker', + iconSize: [40, 40], + iconAnchor: [20, 20] + }); // Set the marker, className must be set to avoid white square + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("bullseye-icon"); + el.setAttribute("data-object", "bullseye"); + var img = document.createElement("img"); + img.src = "/resources/theme/images/markers/bullseye.svg"; + img.onload = () => SVGInjector(img); + el.appendChild(img); + this.getElement()?.appendChild(el); + } + + setCoalition(coalition: string) + { + this.#coalition = coalition; + ( this.getElement()?.querySelector(".bullseye-icon")).dataset.coalition = this.#coalition; + } + + getCoalition() + { + return this.#coalition; + } +} \ No newline at end of file diff --git a/client/src/missionhandler/missionhandler.ts b/client/src/missionhandler/missionhandler.ts new file mode 100644 index 00000000..867e08bf --- /dev/null +++ b/client/src/missionhandler/missionhandler.ts @@ -0,0 +1,169 @@ +import { LatLng } from "leaflet"; +import { getInfoPopup, getMap } from ".."; +import { Airbase } from "./airbase"; +import { Bullseye } from "./bullseye"; + +export class MissionHandler +{ + #bullseyes : {[name: string]: Bullseye} = {}; + #airbases : {[name: string]: Airbase} = {}; + #theatre : string = ""; + + #airbaseData : { [name: string]: object } = {}; + + // Time + #date : any; + #elapsedTime : any; + #startTime : any; + #time : any; + + #updateTime : any; + + constructor() + { + + } + + update(data: BullseyesData | AirbasesData | any) + { + if ("bullseyes" in data) + { + for (let idx in data.bullseyes) + { + const bullseye = data.bullseyes[idx]; + if (!(idx in this.#bullseyes)) + this.#bullseyes[idx] = new Bullseye([0, 0]).addTo(getMap()); + + if (bullseye.latitude && bullseye.longitude && bullseye.coalition) + { + this.#bullseyes[idx].setLatLng(new LatLng(bullseye.latitude, bullseye.longitude)); + this.#bullseyes[idx].setCoalition(bullseye.coalition); + } + } + } + + + if ("mission" in data) + { + if (data.mission != null && data.mission.theatre != this.#theatre) + { + this.#theatre = data.mission.theatre; + getMap().setTheatre(this.#theatre); + + getInfoPopup().setText("Map set to " + this.#theatre); + } + } + + + if ("airbases" in data) + { +/* + console.log( Object.values( data.airbases ).sort( ( a:any, b:any ) => { + const aVal = a.callsign.toLowerCase(); + const bVal = b.callsign.toLowerCase(); + + return aVal > bVal ? 1 : -1; + }) ); +//*/ + for (let idx in data.airbases) + { + var airbase = data.airbases[idx] + if (this.#airbases[idx] === undefined && airbase.callsign != '') + { + this.#airbases[idx] = new Airbase({ + position: new LatLng(airbase.latitude, airbase.longitude), + name: airbase.callsign + }).addTo(getMap()); + this.#airbases[idx].on('contextmenu', (e) => this.#onAirbaseClick(e)); + } + + if (this.#airbases[idx] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) + { + this.#airbases[idx].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); + this.#airbases[idx].setCoalition(airbase.coalition); + } + //this.#airbases[idx].setProperties(["Runway 1: 31L / 13R", "Runway 2: 31R / 13L", "TCN: 17X", "ILS: ---" ]); + //this.#airbases[idx].setParkings(["2x big", "5x small"]); + } + } + + if ("mission" in data && data.mission != null) + { + if (data.mission != null && data.mission.theatre != this.#theatre) + { + this.#theatre = data.mission.theatre; + getMap().setTheatre(this.#theatre); + + getInfoPopup().setText("Map set to " + this.#theatre); + } + + if ( "date" in data.mission ) { + this.#date = data.mission.date; + } + + if ( "elapsedTime" in data.mission ) { + this.#elapsedTime = data.mission.elapsedTime; + } + + if ( "startTime" in data.mission ) { + this.#startTime = data.mission.startTime; + } + + if ( "time" in data.mission ) { + this.#time = data.mission.time; + } + + } + + + if ( "time" in data ) { + this.#updateTime = data.time; + } + + } + + getBullseyes() + { + return this.#bullseyes; + } + + getDate() { + return this.#date; + } + + + getNowDate() { + + const date = this.getDate(); + const time = this.getTime(); + + if ( !date ) { + return new Date(); + } + + let year = date.Year; + let month = date.Month - 1; + + if ( month < 0 ) { + month = 11; + year--; + } + + return new Date( year, month, date.Day, time.h, time.m, time.s ); + } + + + getTime() { + return this.#time; + } + + + getUpdateTime() { + return this.#updateTime; + } + + #onAirbaseClick(e: any) + { + getMap().showAirbaseContextMenu(e, e.sourceTarget); + } +} \ No newline at end of file diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 4b169360..d6ef4fd9 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -1,3 +1,16 @@ +export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { + const φ1 = deg2rad(lat1); // φ, λ in radians + const φ2 = deg2rad(lat2); + const λ1 = deg2rad(lon1); // φ, λ in radians + const λ2 = deg2rad(lon2); + const y = Math.sin(λ2 - λ1) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); + const θ = Math.atan2(y, x); + const brng = (rad2deg(θ) + 360) % 360; // in degrees + + return brng; +} + export function distance(lat1: number, lon1: number, lat2: number, lon2: number) { const R = 6371e3; // metres const φ1 = deg2rad(lat1); // φ, λ in radians @@ -13,35 +26,6 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number) return d; } -export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { - const φ1 = deg2rad(lat1); // φ, λ in radians - const φ2 = deg2rad(lat2); - const λ1 = deg2rad(lon1); // φ, λ in radians - const λ2 = deg2rad(lon2); - const y = Math.sin(λ2 - λ1) * Math.cos(φ2); - const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); - const θ = Math.atan2(y, x); - const brng = (rad2deg(θ) + 360) % 360; // in degrees - - return brng; -} - -export const zeroPad = function (num: number, places: number) { - var string = String(num); - while (string.length < places) { - string += "0"; - } - return string; -} - -export const zeroAppend = function (num: number, places: number) { - var string = String(num); - while (string.length < places) { - string = "0" + string; - } - return string; -} - export function ConvertDDToDMS(D: number, lng: boolean) { var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; var deg = 0 | (D < 0 ? (D = -D) : D); @@ -55,6 +39,20 @@ export function ConvertDDToDMS(D: number, lng: boolean) { return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; } +export function dataPointMap( container:HTMLElement, data:any) { + Object.keys( data ).forEach( ( key ) => { + const val = "" + data[ key ]; // Ensure a string + container.querySelectorAll( `[data-point="${key}"]`).forEach( el => { + // We could probably have options here + if ( el instanceof HTMLInputElement ) { + el.value = val; + } else if ( el instanceof HTMLElement ) { + el.innerText = val; + } + }); + }); +} + export function deg2rad(deg: number) { var pi = Math.PI; return deg * (pi / 180); @@ -63,4 +61,127 @@ export function deg2rad(deg: number) { export function rad2deg(rad: number) { var pi = Math.PI; return rad / (pi / 180); +} + +export function generateUUIDv4() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export function keyEventWasInInput( event:KeyboardEvent ) { + const target = event.target; + return ( target instanceof HTMLElement && ( [ "INPUT", "TEXTAREA" ].includes( target.nodeName ) ) ); +} + +export function reciprocalHeading(heading: number): number { + return heading > 180? heading - 180: heading + 180; +} + +export const zeroAppend = function (num: number, places: number, decimal: boolean = false) { + var string = decimal? num.toFixed(2): String(num); + while (string.length < places) { + string = "0" + string; + } + return string; +} + +export const zeroPad = function (num: number, places: number) { + var string = String(num); + while (string.length < places) { + string += "0"; + } + return string; +} + +export function similarity(s1: string, s2: string) { + var longer = s1; + var shorter = s2; + if (s1.length < s2.length) { + longer = s2; + shorter = s1; + } + var longerLength = longer.length; + if (longerLength == 0) { + return 1.0; + } + return (longerLength - editDistance(longer, shorter)) / longerLength; +} + +export function editDistance(s1: string, s2: string) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + var costs = new Array(); + for (var i = 0; i <= s1.length; i++) { + var lastValue = i; + for (var j = 0; j <= s2.length; j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + var newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length] = lastValue; + } + return costs[s2.length]; +} + +export function latLngToMercator(lat: number, lng: number): {x: number, y: number} { + var rMajor = 6378137; //Equatorial Radius, WGS84 + var shift = Math.PI * rMajor; + var x = lng * shift / 180; + var y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); + y = y * shift / 180; + + return {x: x, y: y}; +} + +export function mercatorToLatLng(x: number, y: number) { + var rMajor = 6378137; //Equatorial Radius, WGS84 + var shift = Math.PI * rMajor; + var lng = x / shift * 180.0; + var lat = y / shift * 180.0; + lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0); + + return { lng: lng, lat: lat }; +} + +export function createDivWithClass(className: string) { + var el = document.createElement("div"); + el.classList.add(className); + return el; +} + +export function knotsToMs(knots: number) { + return knots / 1.94384; +} + +export function msToKnots(ms: number) { + return ms * 1.94384; +} + +export function ftToM(ft: number) { + return ft * 0.3048; +} + +export function mToFt(m: number) { + return m / 0.3048; +} + +export function mToNm(m: number) { + return m * 0.000539957; +} + +export function nmToFt(nm: number) { + return nm * 6076.12; } \ No newline at end of file diff --git a/client/src/panels/connectionstatuspanel.ts b/client/src/panels/connectionstatuspanel.ts index f1ece049..54a3f7f6 100644 --- a/client/src/panels/connectionstatuspanel.ts +++ b/client/src/panels/connectionstatuspanel.ts @@ -1,25 +1,11 @@ -export class ConnectionStatusPanel { - #element: HTMLElement +import { Panel } from "./panel"; +export class ConnectionStatusPanel extends Panel { constructor(ID: string) { - this.#element = document.getElementById(ID); + super(ID); } update(connected: boolean) { - if (this.#element != null) { - var div = this.#element.querySelector("#status-string"); - if (div != null) { - if (connected) { - div.innerHTML = "Connected"; - div.classList.add("olympus-status-connected"); - div.classList.remove("olympus-status-disconnected"); - } - else { - div.innerHTML = "Disconnected"; - div.classList.add("olympus-status-disconnected"); - div.classList.remove("olympus-status-connected"); - } - } - } + this.getElement().toggleAttribute( "data-is-connected", connected ); } } \ No newline at end of file diff --git a/client/src/panels/hotgrouppanel.ts b/client/src/panels/hotgrouppanel.ts new file mode 100644 index 00000000..f0abdb85 --- /dev/null +++ b/client/src/panels/hotgrouppanel.ts @@ -0,0 +1,58 @@ +import { getUnitsManager } from ".."; +import { Unit } from "../units/unit"; +import { Panel } from "./panel"; + +export class HotgroupPanel extends Panel { + constructor(ID: string) { + super(ID); + document.addEventListener("unitDeath", () => this.refreshHotgroups()); + } + + refreshHotgroups() { + for (let hotgroup = 1; hotgroup <= 9; hotgroup++){ + this.removeHotgroup(hotgroup); + if (getUnitsManager().getUnitsByHotgroup(hotgroup).length > 0) + this.addHotgroup(hotgroup); + + } + } + + addHotgroup(hotgroup: number) { + // Hotgroup number + var hotgroupDiv = document.createElement("div"); + hotgroupDiv.classList.add("unit-hotgroup"); + var idDiv = document.createElement("div"); + idDiv.classList.add("unit-hotgroup-id"); + idDiv.innerText = String(hotgroup); + hotgroupDiv.appendChild(idDiv); + + // Hotgroup unit count + var countDiv = document.createElement("div"); + countDiv.innerText = `x${getUnitsManager().getUnitsByHotgroup(hotgroup).length}`; + + var el = document.createElement("div"); + el.appendChild(hotgroupDiv); + el.appendChild(countDiv); + el.classList.add("hotgroup-selector"); + el.toggleAttribute(`data-hotgroup-${hotgroup}`, true) + + this.getElement().appendChild(el); + + el.addEventListener("click", () => { + getUnitsManager().selectUnitsByHotgroup(hotgroup); + }); + + el.addEventListener("mouseover", () => { + getUnitsManager().getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHighlighted(true)); + }); + + el.addEventListener("mouseout", () => { + getUnitsManager().getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHighlighted(false)); + }); + } + + removeHotgroup(hotgroup: number) { + const el = this.getElement().querySelector(`[data-hotgroup-${hotgroup}]`) as HTMLElement; + if (el) el.remove(); + } +} \ No newline at end of file diff --git a/client/src/panels/logpanel.ts b/client/src/panels/logpanel.ts new file mode 100644 index 00000000..367a23c0 --- /dev/null +++ b/client/src/panels/logpanel.ts @@ -0,0 +1,28 @@ +import { Panel } from "./panel"; + +export class LogPanel extends Panel +{ + #logs: String[]; + + constructor(ID: string) + { + super(ID); + this.#logs = []; + } + + update(data: any) + { + var logs = data["logs"]; + for (let idx in logs) + { + if (parseInt(idx) >= this.#logs.length) { + this.#logs.push(logs[idx]); + var el = document.createElement("div"); + el.innerText = logs[idx]; + el.classList.add("js-log-element", "ol-log-element"); + this.getElement().appendChild(el); + this.getElement().scrollTop = this.getElement().scrollHeight; + } + } + } +} \ No newline at end of file diff --git a/client/src/panels/mouseInfoPanel.ts b/client/src/panels/mouseInfoPanel.ts deleted file mode 100644 index 74ed0a72..00000000 --- a/client/src/panels/mouseInfoPanel.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { LatLng } from "leaflet"; -import { getMissionData } from ".."; -import { distance, bearing, zeroPad, zeroAppend } from "../other/utils"; -import { Unit } from "../units/unit"; - -export class MouseInfoPanel { - #element: HTMLElement - #display: string; - - constructor(ID: string) { - this.#element = document.getElementById(ID); - this.#display = ''; - if (this.#element != null) { - this.#display = this.#element.style.display; - var el = this.#element.querySelector(`#measure-position`); - this.show(); - } - } - - show() { - this.#element.style.display = this.#display; - } - - hide() { - this.#element.style.display = "none"; - } - - update(mousePosition: LatLng, measurePosition: LatLng | null, unitPosition: LatLng | null) { - var bullseyes = getMissionData().getBullseyes(); - for (let idx in bullseyes) - { - var dist = distance(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng); - var bear = bearing(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng); - var el = this.#element.querySelector(`#bullseye-${idx}`); - if (el != null) - el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM` - } - - if (measurePosition) { - var dist = distance(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng); - var bear = bearing(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng); - var el = this.#element.querySelector(`#measure-position`); - if (el != null) - { - el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM` - if (el.parentElement != null) - el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded - } - } - else { - var el = this.#element.querySelector(`#measure-position`); - if (el != null && el.parentElement != null) - el.parentElement.style.display = 'none'; - } - - if (unitPosition) { - var dist = distance(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng); - var bear = bearing(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng); - var el = this.#element.querySelector(`#unit-position`); - if (el != null) - { - el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM` - if (el.parentElement != null) - el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded - } - } - else { - var el = this.#element.querySelector(`#unit-position`); - if (el != null && el.parentElement != null) - el.parentElement.style.display = 'none'; - } - } -} \ No newline at end of file diff --git a/client/src/panels/mouseinfopanel.ts b/client/src/panels/mouseinfopanel.ts new file mode 100644 index 00000000..b5e7e8eb --- /dev/null +++ b/client/src/panels/mouseinfopanel.ts @@ -0,0 +1,186 @@ +import { Icon, LatLng, Marker, Polyline } from "leaflet"; +import { getMap, getMissionData, getUnitsManager } from ".."; +import { distance, bearing, zeroAppend, mToNm, nmToFt } from "../other/utils"; +import { Unit } from "../units/unit"; +import { Panel } from "./panel"; +import formatcoords from "formatcoords"; + +export class MouseInfoPanel extends Panel { + #measureMarker: Marker; + #measurePoint: LatLng | null = null; + #measureIcon: Icon; + #measureLine: Polyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1, interactive: false }); + #measureBox: HTMLElement; + + constructor(ID: string) { + super(ID); + + this.#measureIcon = new Icon({ iconUrl: 'resources/theme/images/icons/pin.svg', iconAnchor: [16, 32] }); + this.#measureMarker = new Marker([0, 0], { icon: this.#measureIcon, interactive: false }); + + this.#measureBox = document.createElement("div"); + this.#measureBox.classList.add("ol-measure-box", "hide"); + document.body.appendChild(this.#measureBox); + + getMap()?.on("click", (e: any) => this.#onMapClick(e)); + getMap()?.on('zoom', (e: any) => this.#onZoom(e)); + getMap()?.on('mousemove', (e: any) => this.#onMouseMove(e)); + + document.addEventListener('unitsSelection', (e: CustomEvent) => this.#update()); + document.addEventListener('clearSelection', () => this.#update()); + } + + #update() { + const mousePosition = getMap().getMouseCoordinates(); + + var selectedUnitPosition = null; + var selectedUnits = getUnitsManager().getSelectedUnits(); + if (selectedUnits && selectedUnits.length == 1) + selectedUnitPosition = new LatLng(selectedUnits[0].getFlightData().latitude, selectedUnits[0].getFlightData().longitude); + + /* Draw measures from selected unit, from pin location, and from bullseyes */ + this.#drawMeasure("ref-measure-position", "measure-position", this.#measurePoint, mousePosition); + this.#drawMeasure("ref-unit-position", "unit-position", selectedUnitPosition, mousePosition); + + this.getElement().querySelector(`#measuring-tool`)?.classList.toggle("hide", this.#measurePoint === null && selectedUnitPosition === null); + + var bullseyes = getMissionData().getBullseyes(); + for (let idx in bullseyes) + this.#drawMeasure(null, `bullseye-${idx}`, bullseyes[idx].getLatLng(), mousePosition); + + /* Draw coordinates */ + var coords = formatcoords(mousePosition.lat, mousePosition.lng); + var coordString = coords.format('XDDMMss', {decimalPlaces: 4}); + this.#drawCoordinates("ref-mouse-position-latitude", "mouse-position-latitude", coordString.split(" ")[0]); + this.#drawCoordinates("ref-mouse-position-longitude", "mouse-position-longitude", coordString.split(" ")[1]); + } + + #onMapClick(e: any) { + if (e.originalEvent.ctrlKey) { + if (!this.#measurePoint) { + this.#measureBox.classList.toggle("hide", false); + this.#measurePoint = e.latlng; + this.#measureMarker.setLatLng(e.latlng); + this.#measureMarker.addTo(getMap()); + if (!getMap().hasLayer(this.#measureLine)) + this.#measureLine.addTo(getMap()); + } + else { + this.#measureBox.classList.toggle("hide", true); + this.#measurePoint = null; + if (getMap().hasLayer(this.#measureMarker)) + getMap().removeLayer(this.#measureMarker); + + this.#measureLine.setLatLngs([]); + if (getMap().hasLayer(this.#measureLine)) + getMap().removeLayer(this.#measureLine); + } + } + + this.#update(); + } + + #drawMeasureLine() { + var mouseLatLng = getMap().containerPointToLatLng(getMap().getMousePosition()); + if (this.#measurePoint != null) { + var points = [this.#measurePoint, mouseLatLng]; + this.#measureLine.setLatLngs(points); + var dist = distance(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); + var bear = bearing(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); + var startXY = getMap().latLngToContainerPoint(this.#measurePoint); + var dx = (getMap().getMousePosition().x - startXY.x); + var dy = (getMap().getMousePosition().y - startXY.y); + + var angle = Math.atan2(dy, dx); + if (angle > Math.PI / 2) + angle = angle - Math.PI; + + if (angle < -Math.PI / 2) + angle = angle + Math.PI; + + let bng = zeroAppend(Math.floor(bear), 3); + + if (bng === "000") + bng = "360"; + + var [str, unit] = this.#computeDistanceString(dist) + + let data = [`${bng}°`, `${str} ${unit}`]; + + this.#measureBox.innerText = data.join(" / "); + this.#measureBox.style.left = (getMap().getMousePosition().x + startXY.x) / 2 - this.#measureBox.offsetWidth / 2 + "px"; + this.#measureBox.style.top = (getMap().getMousePosition().y + startXY.y) / 2 - this.#measureBox.offsetHeight / 2 + "px"; + this.#measureBox.style.rotate = angle + "rad"; + } + } + + #onMouseMove(e: any) { + + this.#update(); + this.#drawMeasureLine(); + } + + #onZoom(e: any) { + this.#drawMeasureLine(); + } + + #drawMeasure(imgId: string | null, textId: string, value: LatLng | null, mousePosition: LatLng) { + var el = this.getElement().querySelector(`#${textId}`) as HTMLElement; + var img = imgId != null ? this.getElement().querySelector(`#${imgId}`) as HTMLElement : null; + if (value) { + if (el != null) { + el.classList.remove("hide"); + + var bear = bearing(value.lat, value.lng, mousePosition.lat, mousePosition.lng); + var dist = distance(value.lat, value.lng, mousePosition.lat, mousePosition.lng); + + let bng = zeroAppend(Math.floor(bear), 3); + + if (bng === "000") + bng = "360"; + + var [str, unit] = this.#computeDistanceString(dist) + + el.dataset.bearing = bng; + el.dataset.distance = str; + el.dataset.distanceUnits = unit; + } + if (img != null) + img.classList.remove("hide"); + } + else { + if (el != null) + el.classList.add("hide"); + if (img != null) + img.classList.add("hide"); + } + } + + #drawCoordinates(imgId: string, textId: string, value: string) { + const el = this.getElement().querySelector(`#${textId}`) as HTMLElement; + const img = this.getElement().querySelector(`#${imgId}`) as HTMLElement; + if (img && el) { + el.dataset.value = value.substring(1); + img.dataset.label = value[0]; + } + } + + #computeDistanceString(dist: number) { + var val = mToNm(dist); + var strVal = 0; + var decimal = false; + var unit = "NM"; + if (val > 10) + strVal = Math.floor(val); + else if (val > 1 && val <= 10) { + strVal = Math.floor(val * 100) / 100; + decimal = true; + } + else { + strVal = Math.floor(nmToFt(val)); + unit = "ft"; + } + + return [zeroAppend(strVal, 3, decimal), unit]; + } +} diff --git a/client/src/panels/panel.ts b/client/src/panels/panel.ts new file mode 100644 index 00000000..df0a2f48 --- /dev/null +++ b/client/src/panels/panel.ts @@ -0,0 +1,34 @@ +export class Panel { + #element: HTMLElement + #visible: boolean = true; + + constructor(ID: string) { + this.#element = document.getElementById(ID); + } + + show() { + this.#element.classList.toggle("hide", false); + this.#visible = true; + } + + hide() { + this.#element.classList.toggle("hide", true); + this.#visible = false; + } + + toggle() { + // Simple way to track if currently visible + if (this.#visible) + this.hide(); + else + this.show(); + } + + getElement() { + return this.#element; + } + + getVisible(){ + return this.#visible; + } +} \ No newline at end of file diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 9a143ffd..c5d6f489 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -1,363 +1,328 @@ -import { imageOverlay } from "leaflet"; -import { getUnitControlSliders, getUnitsManager } from ".."; -import { ConvertDDToDMS, rad2deg } from "../other/utils"; -import { Aircraft, AirUnit, GroundUnit, Helicopter, NavyUnit, Unit } from "../units/unit"; +import { SVGInjector } from "@tanem/svg-injector"; +import { getUnitsManager } from ".."; +import { Dropdown } from "../controls/dropdown"; +import { Slider } from "../controls/slider"; +import { aircraftDatabase } from "../units/aircraftdatabase"; +import { Unit } from "../units/unit"; +import { Panel } from "./panel"; +import { Switch } from "../controls/switch"; +import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, emissionsCountermeasuresDescriptions, maxAltitudeValues, maxSpeedValues, minAltitudeValues, minSpeedValues, reactionsToThreat, reactionsToThreatDescriptions, speedIncrements } from "../constants/constants"; +import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils"; -export class UnitControlPanel { - #element: HTMLElement - #display: string; +export class UnitControlPanel extends Panel { + #altitudeSlider: Slider; + #altitudeTypeSwitch: Switch; + #speedSlider: Slider; + #speedTypeSwitch: Switch; + #onOffSwitch: Switch; + #followRoadsSwitch: Switch; + #TACANXYDropdown: Dropdown; + #radioDecimalsDropdown: Dropdown; + #radioCallsignDropdown: Dropdown; + #optionButtons: { [key: string]: HTMLButtonElement[] } = {} + #advancedSettingsDialog: HTMLElement; constructor(ID: string) { - this.#element = document.getElementById(ID); - this.#display = ''; - if (this.#element != null) { - this.#display = this.#element.style.display; - var formationCreationContainer = (this.#element.querySelector("#formation-creation-container")); - if (formationCreationContainer != null) - { - var createButton = formationCreationContainer.querySelector("#create-formation"); - createButton?.addEventListener("click", () => getUnitsManager().selectedUnitsCreateFormation()); + super(ID); - var undoButton = formationCreationContainer.querySelector("#undo-formation"); - undoButton?.addEventListener("click", () => getUnitsManager().selectedUnitsUndoFormation()); - } - var ROEButtonsContainer = (this.#element.querySelector("#roe-buttons-container")); - if (ROEButtonsContainer != null) - { - (ROEButtonsContainer.querySelector("#free"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Free")); - (ROEButtonsContainer.querySelector("#designated-free"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Designated free")); - (ROEButtonsContainer.querySelector("#designated"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Designated")); - (ROEButtonsContainer.querySelector("#return"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Return")); - (ROEButtonsContainer.querySelector("#hold"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Hold")); - } + /* Unit control sliders */ + this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getUnitsManager().selectedUnitsSetAltitude(ftToM(value)); }); + this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetAltitudeType(value? "AGL": "ASL"); }); - var reactionToThreatButtonsContainer = (this.#element.querySelector("#reaction-to-threat-buttons-container")); - if (reactionToThreatButtonsContainer != null) - { - (reactionToThreatButtonsContainer.querySelector("#none"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("None")); - (reactionToThreatButtonsContainer.querySelector("#passive"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Passive")); - (reactionToThreatButtonsContainer.querySelector("#evade"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Evade")); - (reactionToThreatButtonsContainer.querySelector("#escape"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Escape")); - (reactionToThreatButtonsContainer.querySelector("#abort"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Abort")); - } - this.hide(); - } + this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getUnitsManager().selectedUnitsSetSpeed(knotsToMs(value)); }); + this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetSpeedType(value? "GS": "CAS"); }); + + /* Option buttons */ + this.#optionButtons["ROE"] = ROEs.map((option: string, index: number) => { + return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions[index], () => { getUnitsManager().selectedUnitsSetROE(option); }); + }); + + this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => { + return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getUnitsManager().selectedUnitsSetReactionToThreat(option); }); + }); + + this.#optionButtons["emissionsCountermeasures"] = emissionsCountermeasures.map((option: string, index: number) => { + return this.#createOptionButton(option, `emissions/${option.toLowerCase()}.svg`, emissionsCountermeasuresDescriptions[index],() => { getUnitsManager().selectedUnitsSetEmissionsCountermeasures(option); }); + }); + + this.getElement().querySelector("#roe-buttons-container")?.append(...this.#optionButtons["ROE"]); + this.getElement().querySelector("#reaction-to-threat-buttons-container")?.append(...this.#optionButtons["reactionToThreat"]); + this.getElement().querySelector("#emissions-countermeasures-buttons-container")?.append(...this.#optionButtons["emissionsCountermeasures"]); + + /* On off switch */ + this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetOnOff(value); + }); + + /* Follow roads switch */ + this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetFollowRoads(value); + }); + + /* Advanced settings dialog */ + this.#advancedSettingsDialog = document.querySelector("#advanced-settings-dialog"); + + /* Advanced settings dropdowns */ + this.#TACANXYDropdown = new Dropdown("TACAN-XY", () => {}); + this.#TACANXYDropdown.setOptions(["X", "Y"]); + this.#radioDecimalsDropdown = new Dropdown("radio-decimals", () => {}); + this.#radioDecimalsDropdown.setOptions([".000", ".250", ".500", ".750"]); + this.#radioCallsignDropdown = new Dropdown("radio-callsign", () => {}); + + /* Events and timer */ + window.setInterval(() => {this.update();}, 25); + + document.addEventListener("unitsSelection", (e: CustomEvent) => { this.show(); this.addButtons();}); + document.addEventListener("clearSelection", () => { this.hide() }); + document.addEventListener("applyAdvancedSettings", () => {this.#applyAdvancedSettings();}) + document.addEventListener("showAdvancedSettings", () => { + this.#updateAdvancedSettingsDialog(getUnitsManager().getSelectedUnits()); + this.#advancedSettingsDialog.classList.remove("hide"); + }); + + this.hide(); } show() { - this.#element.style.display = this.#display; + super.show(); + this.#speedTypeSwitch.resetExpectedValue(); + this.#altitudeTypeSwitch.resetExpectedValue(); + this.#onOffSwitch.resetExpectedValue(); + this.#followRoadsSwitch.resetExpectedValue(); + this.#altitudeSlider.resetExpectedValue(); + this.#speedSlider.resetExpectedValue(); } - hide() { - this.#element.style.display = "none"; + addButtons() { + var units = getUnitsManager().getSelectedUnits(); + if (units.length < 20) { + this.getElement().querySelector("#selected-units-container")?.replaceChildren(...units.map((unit: Unit, index: number) => { + var button = document.createElement("button"); + var callsign = unit.getBaseData().unitName || ""; + var label = unit.getDatabase()?.getByName(unit.getBaseData().name)?.label || unit.getBaseData().name; + + button.setAttribute("data-label", label); + button.setAttribute("data-callsign", callsign); + + button.setAttribute("data-coalition", unit.getMissionData().coalition); + button.classList.add("pill", "highlight-coalition") + + button.addEventListener("click", () => { + getUnitsManager().deselectAllUnits(); + getUnitsManager().selectUnit(unit.ID, true); + }); + return (button); + })); + } else { + var el = document.createElement("div"); + el.innerText = "Too many units selected"; + this.getElement().querySelector("#selected-units-container")?.replaceChildren(el); + } } - update(units: Unit[]) { - if (this.#element != null) - { - var selectedUnitsContainer = (this.#element.querySelector("#selected-units-container")); - var formationCreationContainer = (this.#element.querySelector("#formation-creation-container")); - if (selectedUnitsContainer != null && formationCreationContainer != null) - { - this.#addUnitsButtons(units, selectedUnitsContainer); - this.#showFlightControlSliders(units); - this.#showFormationButtons(units, formationCreationContainer); - } + update() { + if (this.getVisible()){ + const element = this.getElement(); + const units = getUnitsManager().getSelectedUnits(); + const selectedUnitsTypes = getUnitsManager().getSelectedUnitsTypes(); + + if (element != null && units.length > 0) { + /* Toggle visibility of control elements */ + element.toggleAttribute("data-show-categories-tooltip", selectedUnitsTypes.length > 1); + element.toggleAttribute("data-show-speed-slider", selectedUnitsTypes.length == 1); + element.toggleAttribute("data-show-altitude-slider", selectedUnitsTypes.length == 1 && (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); + element.toggleAttribute("data-show-roe", true); + element.toggleAttribute("data-show-threat", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); + element.toggleAttribute("data-show-emissions-countermeasures", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); + element.toggleAttribute("data-show-on-off", (selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit")) && !(selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); + element.toggleAttribute("data-show-follow-roads", (selectedUnitsTypes.length == 1 && selectedUnitsTypes.includes("GroundUnit"))); + element.toggleAttribute("data-show-advanced-settings-button", units.length == 1); + + /* Flight controls */ + var desiredAltitude: number | undefined = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().desiredAltitude}); + var desiredAltitudeType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().desiredAltitudeType}); + var desiredSpeed = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().desiredSpeed}); + var desiredSpeedType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().desiredSpeedType}); + var onOff = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().onOff}); + var followRoads = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().followRoads}); - var ROEButtonsContainer = (this.#element.querySelector("#roe-buttons-container")); - if (ROEButtonsContainer != null) - { - (ROEButtonsContainer.querySelector("#free"))?.classList.toggle("white", this.#getROE(units) === "Free"); - (ROEButtonsContainer.querySelector("#designated-free"))?.classList.toggle("white", this.#getROE(units) === "Designated free"); - (ROEButtonsContainer.querySelector("#designated"))?.classList.toggle("white", this.#getROE(units) === "Designated"); - (ROEButtonsContainer.querySelector("#return"))?.classList.toggle("white", this.#getROE(units) === "Return"); - (ROEButtonsContainer.querySelector("#hold"))?.classList.toggle("white", this.#getROE(units) === "Hold"); - } + if (selectedUnitsTypes.length == 1) { + this.#altitudeTypeSwitch.setValue(desiredAltitudeType != undefined? desiredAltitudeType == "AGL": undefined, false); + this.#speedTypeSwitch.setValue(desiredSpeedType != undefined? desiredSpeedType == "GS": undefined, false); - var reactionToThreatButtonsContainer = (this.#element.querySelector("#reaction-to-threat-buttons-container")); - if (reactionToThreatButtonsContainer != null) - { - (reactionToThreatButtonsContainer.querySelector("#none"))?.classList.toggle("white", this.#getReactionToThreat(units) === "None"); - (reactionToThreatButtonsContainer.querySelector("#passive"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Passive"); - (reactionToThreatButtonsContainer.querySelector("#evade"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Evade"); - (reactionToThreatButtonsContainer.querySelector("#escape"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Escape"); - (reactionToThreatButtonsContainer.querySelector("#abort"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Abort"); + this.#speedSlider.setMinMax(minSpeedValues[selectedUnitsTypes[0]], maxSpeedValues[selectedUnitsTypes[0]]); + this.#altitudeSlider.setMinMax(minAltitudeValues[selectedUnitsTypes[0]], maxAltitudeValues[selectedUnitsTypes[0]]); + this.#speedSlider.setIncrement(speedIncrements[selectedUnitsTypes[0]]); + this.#altitudeSlider.setIncrement(altitudeIncrements[selectedUnitsTypes[0]]); + + this.#speedSlider.setActive(desiredSpeed != undefined); + if (desiredSpeed != undefined) + this.#speedSlider.setValue(msToKnots(desiredSpeed), false); + + this.#altitudeSlider.setActive(desiredAltitude != undefined); + if (desiredAltitude != undefined) + this.#altitudeSlider.setValue(mToFt(desiredAltitude), false); + } + else { + this.#speedSlider.setActive(false); + this.#altitudeSlider.setActive(false); + } + + /* Option buttons */ + this.#optionButtons["ROE"].forEach((button: HTMLButtonElement) => { + button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().ROE === button.value)) + }); + + this.#optionButtons["reactionToThreat"].forEach((button: HTMLButtonElement) => { + button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().reactionToThreat === button.value)) + }); + + this.#optionButtons["emissionsCountermeasures"].forEach((button: HTMLButtonElement) => { + button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().emissionsCountermeasures === button.value)) + }); + + this.#onOffSwitch.setValue(onOff, false); + this.#followRoadsSwitch.setValue(followRoads, false); } } } - #showFlightControlSliders(units: Unit[]) + #updateAdvancedSettingsDialog(units: Unit[]) { - var sliders = getUnitControlSliders(); - sliders.airspeed.show(); - sliders.altitude.show(); - - if (this.#checkAllUnitsAircraft(units)) + if (units.length == 1) { - sliders.airspeed.setMinMax(100, 600); - sliders.altitude.setMinMax(0, 50000); - } - else if (this.#checkAllUnitsHelicopter(units)) - { - sliders.airspeed.setMinMax(0, 200); - sliders.altitude.setMinMax(0, 10000); - } - else if (this.#checkAllUnitsGroundUnit(units)) - { - sliders.airspeed.setMinMax(0, 60); - sliders.altitude.hide(); - } - else if (this.#checkAllUnitsNavyUnit(units)) - { - sliders.airspeed.setMinMax(0, 60); - sliders.altitude.hide(); - } - else { - sliders.airspeed.hide(); - sliders.altitude.hide(); - } - - var targetSpeed = this.#getTargetAirspeed(units); - if (targetSpeed != null) - { - sliders.airspeed.setActive(true); - sliders.airspeed.setValue(targetSpeed * 1.94384); - } - else - { - sliders.airspeed.setActive(false); - } - - var targetAltitude = this.#getTargetAltitude(units); - if (targetAltitude != null) - { - sliders.altitude.setActive(true); - sliders.altitude.setValue(targetAltitude / 0.3048); - } - else - { - sliders.altitude.setActive(false); - } - } - - #addUnitsButtons(units: Unit[], selectedUnitsContainer: HTMLElement) - { - /* Remove any pre-existing unit button */ - var elements = selectedUnitsContainer.getElementsByClassName("js-unit-container"); - while (elements.length > 0) - selectedUnitsContainer.removeChild(elements[0]) - - /* Create all the units buttons */ - for (let unit of units) - { - this.#addUnitButton(unit, selectedUnitsContainer); - if (unit.isLeader) - for (let wingman of unit.getWingmen()) - this.#addUnitButton(wingman, selectedUnitsContainer); - } - } - - #addUnitButton(unit: Unit, container: HTMLElement) - { - var el = document.createElement("div"); + /* HTML Elements */ + const unitNameEl = this.#advancedSettingsDialog.querySelector("#unit-name") as HTMLElement; + const prohibitJettisonCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-jettison-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAfterburnerCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-afterburner-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAACheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-AA-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAGCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-AG-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAirWpnCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-air-wpn-checkbox")?.querySelector("input") as HTMLInputElement; + const tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") as HTMLInputElement; + const AWACSCheckbox = this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input") as HTMLInputElement; + const TACANCheckbox = this.#advancedSettingsDialog.querySelector("#TACAN-checkbox")?.querySelector("input") as HTMLInputElement; + const TACANChannelInput = this.#advancedSettingsDialog.querySelector("#TACAN-channel")?.querySelector("input") as HTMLInputElement; + const TACANCallsignInput = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input") as HTMLInputElement; + const radioMhzInput = this.#advancedSettingsDialog.querySelector("#radio-mhz")?.querySelector("input") as HTMLInputElement; + const radioCallsignNumberInput = this.#advancedSettingsDialog.querySelector("#radio-callsign-number")?.querySelector("input") as HTMLInputElement; - /* Unit name (actually type, but DCS calls it name for some reason) */ - var nameDiv = document.createElement("div"); - nameDiv.classList.add("rounded-container-small"); - if (unit.name.length >= 7) - nameDiv.innerHTML = `${unit.name.substring(0, 4)} ...`; - else - nameDiv.innerHTML = `${unit.name}`; + const unit = units[0]; + const roles = aircraftDatabase.getByName(unit.getBaseData().name)?.loadouts.map((loadout) => {return loadout.roles}) + const tanker = roles != undefined && Array.prototype.concat.apply([], roles)?.includes("Tanker"); + const AWACS = roles != undefined && Array.prototype.concat.apply([], roles)?.includes("AWACS"); + const radioMHz = Math.floor(unit.getOptionsData().radio.frequency / 1000000); + const radioDecimals = (unit.getOptionsData().radio.frequency / 1000000 - radioMHz) * 1000; - /* Unit icon */ - var icon = document.createElement("img"); - if (unit.isLeader) - icon.src = "images/icons/formation.png" - else if (unit.isWingman) - { - var wingmen = unit.getLeader()?.getWingmen(); - if (wingmen && wingmen.lastIndexOf(unit) == wingmen.length - 1) - icon.src = "images/icons/formation-end.svg" + /* Activate the correct options depending on unit type */ + this.#advancedSettingsDialog.toggleAttribute("data-show-settings", !tanker && !AWACS); + this.#advancedSettingsDialog.toggleAttribute("data-show-tasking", tanker || AWACS); + this.#advancedSettingsDialog.toggleAttribute("data-show-tanker", tanker); + this.#advancedSettingsDialog.toggleAttribute("data-show-AWACS", AWACS); + this.#advancedSettingsDialog.toggleAttribute("data-show-TACAN", tanker); + this.#advancedSettingsDialog.toggleAttribute("data-show-radio", tanker || AWACS); + + /* Set common properties */ + // Name + unitNameEl.innerText = unit.getBaseData().unitName; + + // General settings + prohibitJettisonCheckbox.checked = unit.getOptionsData().generalSettings.prohibitJettison; + prohibitAfterburnerCheckbox.checked = unit.getOptionsData().generalSettings.prohibitAfterburner; + prohibitAACheckbox.checked = unit.getOptionsData().generalSettings.prohibitAA; + prohibitAGCheckbox.checked = unit.getOptionsData().generalSettings.prohibitAG; + prohibitAirWpnCheckbox.checked = unit.getOptionsData().generalSettings.prohibitAirWpn; + + // Tasking + tankerCheckbox.checked = unit.getTaskData().isTanker; + AWACSCheckbox.checked = unit.getTaskData().isAWACS; + + // TACAN + TACANCheckbox.checked = unit.getOptionsData().TACAN.isOn; + TACANChannelInput.value = String(unit.getOptionsData().TACAN.channel); + TACANCallsignInput.value = String(unit.getOptionsData().TACAN.callsign); + this.#TACANXYDropdown.setValue(unit.getOptionsData().TACAN.XY); + + // Radio + radioMhzInput.value = String(radioMHz); + radioCallsignNumberInput.value = String(unit.getOptionsData().radio.callsignNumber); + this.#radioDecimalsDropdown.setValue("." + radioDecimals); + + if (tanker) /* Set tanker specific options */ + this.#radioCallsignDropdown.setOptions(["Texaco", "Arco", "Shell"]); + else if (AWACS) /* Set AWACS specific options */ + this.#radioCallsignDropdown.setOptions(["Overlord", "Magic", "Wizard", "Focus", "Darkstar"]); else - icon.src = "images/icons/formation-middle.svg" - } - - else - icon.src = "images/icons/singleton.png" + this.#radioCallsignDropdown.setOptions(["Enfield", "Springfield", "Uzi", "Colt", "Dodge", "Ford", "Chevy", "Pontiac"]); - el.innerHTML = unit.unitName; - - el.prepend(nameDiv); - - /* Show the icon only for air units */ - if ((unit instanceof AirUnit)) - el.append(icon); - - el.classList.add("rounded-container", "js-unit-container"); - - if (!unit.getSelected()) - el.classList.add("not-selected") - - /* Set background color */ - if (unit.coalitionID == 1) - { - el.classList.add("red"); - icon.classList.add("red"); - } - else if (unit.coalitionID == 2) - { - el.classList.add("blue"); - icon.classList.add("blue"); - } - else - { - el.classList.add("neutral"); - icon.classList.add("neutral"); - } - - el.addEventListener("click", () => getUnitsManager().selectUnit(unit.ID)); - container.appendChild(el); - } - - #showFormationButtons(units: Unit[], formationCreationContainer: HTMLElement) - { - var createButton = formationCreationContainer.querySelector("#create-formation"); - var undoButton = formationCreationContainer.querySelector("#undo-formation"); - if (createButton && undoButton && this.#checkAllUnitsAir(units)) - { - if (!this.#checkUnitsAlreadyInFormation(units)) - { - createButton.style.display = ''; - undoButton.style.display = 'none'; - } - else if (this.#checkUnitsAlreadyInFormation(units) && this.#checkAllUnitsSameFormation(units)) - { - createButton.style.display = 'none'; - undoButton.style.display = ''; - } - else - { - createButton.style.display = 'none'; - undoButton.style.display = 'none'; - } + // This must be done after setting the options + if (!this.#radioCallsignDropdown.selectValue(unit.getOptionsData().radio.callsign - 1)) // Ensure the selected value is in the acceptable range + this.#radioCallsignDropdown.selectValue(0); } } - #checkAllUnitsAir(units: Unit[]) + #applyAdvancedSettings() { - for (let unit of units) - if (!(unit instanceof AirUnit)) - return false - return true - } + /* HTML Elements */ + const prohibitJettisonCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-jettison-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAfterburnerCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-afterburner-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAACheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-AA-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAGCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-AG-checkbox")?.querySelector("input") as HTMLInputElement; + const prohibitAirWpnCheckbox = this.#advancedSettingsDialog.querySelector("#prohibit-air-wpn-checkbox")?.querySelector("input") as HTMLInputElement; + const tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") as HTMLInputElement; + const AWACSCheckbox = this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input") as HTMLInputElement; + const TACANCheckbox = this.#advancedSettingsDialog.querySelector("#TACAN-checkbox")?.querySelector("input") as HTMLInputElement; + const TACANChannelInput = this.#advancedSettingsDialog.querySelector("#TACAN-channel")?.querySelector("input") as HTMLInputElement; + const TACANCallsignInput = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input") as HTMLInputElement; + const radioMhzInput = this.#advancedSettingsDialog.querySelector("#radio-mhz")?.querySelector("input") as HTMLInputElement; + const radioCallsignNumberInput = this.#advancedSettingsDialog.querySelector("#radio-callsign-number")?.querySelector("input") as HTMLInputElement; - #checkAllUnitsAircraft(units: Unit[]) - { - for (let unit of units) - if (!(unit instanceof Aircraft)) - return false - return true - } + /* Tasking */ + const isTanker = tankerCheckbox.checked? true: false; + const isAWACS = AWACSCheckbox.checked? true: false; - #checkAllUnitsHelicopter(units: Unit[]) - { - for (let unit of units) - if (!(unit instanceof Helicopter)) - return false - return true - } - - #checkAllUnitsGroundUnit(units: Unit[]) - { - for (let unit of units) - if (!(unit instanceof GroundUnit)) - return false - return true - } - - #checkAllUnitsNavyUnit(units: Unit[]) - { - for (let unit of units) - if (!(unit instanceof NavyUnit)) - return false - return true - } - - #checkAllUnitsSameFormation(units: Unit[]) - { - var leaderFound = false; - for (let unit of units) - { - if (unit.isLeader) - { - if (leaderFound) - return false - else - leaderFound = true; - } - if (!unit.isLeader) - return false + /* TACAN */ + const TACAN: TACAN = { + isOn: TACANCheckbox.checked? true: false, + channel: Number(TACANChannelInput.value), + XY: this.#TACANXYDropdown.getValue(), + callsign: TACANCallsignInput.value as string } - return true - } - #checkUnitsAlreadyInFormation(units: Unit[]) - { - for (let unit of units) - if (unit.isLeader) - return true - return false - } - - #getTargetAirspeed(units: Unit[]) - { - var airspeed = null; - for (let unit of units) - { - if (unit.targetSpeed != airspeed && airspeed != null) - return null - else - airspeed = unit.targetSpeed; + /* Radio */ + const radioMHz = Number(radioMhzInput.value); + const radioDecimals = this.#radioDecimalsDropdown.getValue(); + const radio: Radio = { + frequency: (radioMHz * 1000 + Number(radioDecimals.substring(1))) * 1000, + callsign: this.#radioCallsignDropdown.getIndex() + 1, + callsignNumber: Number(radioCallsignNumberInput.value) } - return airspeed; + + /* General settings */ + const generalSettings: GeneralSettings = { + prohibitJettison: prohibitJettisonCheckbox.checked? true: false, + prohibitAfterburner: prohibitAfterburnerCheckbox.checked? true: false, + prohibitAA: prohibitAACheckbox.checked? true: false, + prohibitAG: prohibitAGCheckbox.checked? true: false, + prohibitAirWpn: prohibitAirWpnCheckbox.checked? true: false + } + + /* Send command and close */ + var units = getUnitsManager().getSelectedUnits(); + if (units.length > 0) + units[0].setAdvancedOptions(isTanker, isAWACS, TACAN, radio, generalSettings); + + this.#advancedSettingsDialog.classList.add("hide"); } - #getTargetAltitude(units: Unit[]) - { - var altitude = null; - for (let unit of units) - { - if (unit.targetAltitude != altitude && altitude != null) - return null - else - altitude = unit.targetAltitude; - } - return altitude; - } - - #getROE(units: Unit[]) - { - var ROE = null; - for (let unit of units) - { - if (unit.ROE !== ROE && ROE != null) - return null - else - ROE = unit.ROE; - } - return ROE; - } - - #getReactionToThreat(units: Unit[]) - { - var reactionToThreat = null; - for (let unit of units) - { - if (unit.reactionToThreat !== reactionToThreat && reactionToThreat != null) - return null - else - reactionToThreat = unit.reactionToThreat; - } - return reactionToThreat; + #createOptionButton(value: string, url: string, title: string, callback: EventListenerOrEventListenerObject) { + var button = document.createElement("button"); + button.title = title; + button.value = value; + var img = document.createElement("img"); + img.src = `/resources/theme/images/buttons/${url}`; + img.onload = () => SVGInjector(img); + button.appendChild(img); + button.addEventListener("click", callback); + return button; } } \ No newline at end of file diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts index 070d1dda..91e18920 100644 --- a/client/src/panels/unitinfopanel.ts +++ b/client/src/panels/unitinfopanel.ts @@ -1,62 +1,112 @@ +import { getUnitsManager } from ".."; import { ConvertDDToDMS, rad2deg } from "../other/utils"; +import { aircraftDatabase } from "../units/aircraftdatabase"; import { Unit } from "../units/unit"; +import { Panel } from "./panel"; -export class UnitInfoPanel { - #element: HTMLElement - #display: string; +export class UnitInfoPanel extends Panel { + #altitude: HTMLElement; + #currentTask: HTMLElement; + #fuelBar: HTMLElement; + #fuelPercentage: HTMLElement; + #groundSpeed: HTMLElement; + #groupName: HTMLElement; + #heading: HTMLElement; + #name: HTMLElement; + #latitude: HTMLElement; + #longitude: HTMLElement; + #loadoutContainer: HTMLElement; + #silhouette: HTMLImageElement; + #unitControl: HTMLElement; + #unitLabel: HTMLElement; + #unitName: HTMLElement; constructor(ID: string) { - this.#element = document.getElementById(ID); - this.#display = ''; - if (this.#element != null) { - this.#display = this.#element.style.display; - this.hide(); - } + super(ID); + + this.#altitude = (this.getElement().querySelector("#altitude")) as HTMLElement; + this.#currentTask = (this.getElement().querySelector("#current-task")) as HTMLElement; + this.#groundSpeed = (this.getElement().querySelector("#ground-speed")) as HTMLElement; + this.#fuelBar = (this.getElement().querySelector("#fuel-bar")) as HTMLElement; + this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")) as HTMLElement; + this.#groupName = (this.getElement().querySelector("#group-name")) as HTMLElement; + this.#heading = (this.getElement().querySelector("#heading")) as HTMLElement; + this.#name = (this.getElement().querySelector("#name")) as HTMLElement; + this.#latitude = (this.getElement().querySelector("#latitude")) as HTMLElement; + this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")) as HTMLElement; + this.#longitude = (this.getElement().querySelector("#longitude")) as HTMLElement; + this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")) as HTMLImageElement; + this.#unitControl = (this.getElement().querySelector("#unit-control")) as HTMLElement; + this.#unitLabel = (this.getElement().querySelector("#unit-label")) as HTMLElement; + this.#unitName = (this.getElement().querySelector("#unit-name")) as HTMLElement; + + document.addEventListener("unitsSelection", (e: CustomEvent) => this.#onUnitsSelection(e.detail)); + document.addEventListener("unitsDeselection", (e: CustomEvent) => this.#onUnitsDeselection(e.detail)); + document.addEventListener("clearSelection", (e: CustomEvent) => this.#onUnitsDeselection([])); + document.addEventListener("unitUpdated", (e: CustomEvent) => this.#onUnitUpdate(e.detail)); + + this.hide(); } - show() { - this.#element.style.display = this.#display; - } + #onUnitUpdate(unit: Unit) { + if (this.getElement() != null && this.getVisible() && unit.getSelected()) { - hide() { - this.#element.style.display = "none"; - } + const baseData = unit.getBaseData(); - update(unit: Unit) { - if (this.#element != null) { - var els = this.#element.getElementsByClassName("js-loadout-element"); - while (els.length > 0) - this.#element.querySelector("#loadout-data")?.removeChild(els[0]); - - for (let index in unit.ammo) { - var ammo = unit.ammo[index]; - var displayName = ammo.desc.displayName; - var amount = ammo.count; - var el = document.createElement("div") - el.classList.add("js-loadout-element", "rectangular-container-dark") - el.innerHTML = amount + "x" + displayName; - this.#element.querySelector("#loadout-data")?.appendChild(el); - } - - this.#element.querySelector("#unit-name")!.innerHTML = unit.unitName; - this.#element.querySelector("#group-name")!.innerHTML = unit.groupName; - this.#element.querySelector("#name")!.innerHTML = unit.name; - this.#element.querySelector("#heading")!.innerHTML = String(Math.floor(rad2deg(unit.heading)) + " °"); - this.#element.querySelector("#altitude")!.innerHTML = String(Math.floor(unit.altitude / 0.3048) + " ft"); - this.#element.querySelector("#ground-speed")!.innerHTML = String(Math.floor(unit.speed * 1.94384) + " kts"); - this.#element.querySelector("#fuel")!.innerHTML = String(unit.fuel + "%"); - this.#element.querySelector("#latitude")!.innerHTML = ConvertDDToDMS(unit.latitude, false); - this.#element.querySelector("#longitude")!.innerHTML = ConvertDDToDMS(unit.longitude, true); - this.#element.querySelector("#task")!.innerHTML = unit.currentTask !== ""? unit.currentTask: "Not controlled"; - - this.#element.querySelector("#task")!.classList.remove("red", "blue", "neutral"); - if (unit.coalitionID == 1) - this.#element.querySelector("#task")!.classList.add("red"); - else if (unit.coalitionID == 2) - this.#element.querySelector("#task")!.classList.add("blue"); + /* Set the unit info */ + this.#unitLabel.innerText = aircraftDatabase.getByName(baseData.name)?.label || baseData.name; + this.#unitName.innerText = baseData.unitName; + if (unit.getMissionData().flags.Human) + this.#unitControl.innerText = "Human"; + else if (baseData.controlled) + this.#unitControl.innerText = "Olympus controlled"; else - this.#element.querySelector("#task")!.classList.add("neutral"); - + this.#unitControl.innerText = "DCS Controlled"; + this.#fuelBar.style.width = String(unit.getMissionData().fuel + "%"); + this.#fuelPercentage.dataset.percentage = "" + unit.getMissionData().fuel; + this.#currentTask.dataset.currentTask = unit.getTaskData().currentTask !== "" ? unit.getTaskData().currentTask : "No task"; + this.#currentTask.dataset.coalition = unit.getMissionData().coalition; + + this.#silhouette.src = `/images/units/${unit.getDatabase()?.getByName(baseData.name)?.filename}`; + this.#silhouette.classList.toggle("hide", unit.getDatabase()?.getByName(baseData.name)?.filename == undefined || unit.getDatabase()?.getByName(baseData.name)?.filename == ''); + + /* Add the loadout elements */ + const items = this.#loadoutContainer.querySelector("#loadout-items"); + + if (items) { + const ammo = Object.values(unit.getMissionData().ammo); + if (ammo.length > 0) { + items.replaceChildren(...Object.values(unit.getMissionData().ammo).map( + (ammo: any) => { + var el = document.createElement("div"); + el.dataset.qty = ammo.count; + el.dataset.item = ammo.desc.displayName; + return el; + } + )); + + } else { + items.innerText = "No loadout"; + } + } } } + + #onUnitsSelection(units: Unit[]) { + if (units.length == 1) { + this.show(); + this.#onUnitUpdate(units[0]); + } + else + this.hide(); + } + + #onUnitsDeselection(units: Unit[]) { + if (units.length == 1) { + this.show(); + this.#onUnitUpdate(units[0]); + } + else + this.hide(); + } } \ No newline at end of file diff --git a/client/src/popups/popup.ts b/client/src/popups/popup.ts new file mode 100644 index 00000000..55f828ac --- /dev/null +++ b/client/src/popups/popup.ts @@ -0,0 +1,26 @@ +import { Panel } from "../panels/panel"; + +export class Popup extends Panel { + #fadeTime: number = 2000; // Milliseconds + #hideTimer: number | undefined = undefined; + #visibilityTimer: number | undefined = undefined; + + setFadeTime(fadeTime: number) { + this.#fadeTime = fadeTime; + } + + setText(text: string) { + ( this.getElement().querySelector("div")).innerText = text; + this.show(); + this.getElement().classList.remove("invisible"); + this.getElement().classList.add("visible"); + + clearTimeout(this.#visibilityTimer); + clearTimeout(this.#hideTimer); + this.#visibilityTimer = window.setTimeout(() => { + this.getElement().classList.remove("visible"); + this.getElement().classList.add("invisible"); + this.#hideTimer = window.setTimeout(() => this.hide(), 2000); + }, this.#fadeTime); + } +} \ No newline at end of file diff --git a/client/src/server/server.ts b/client/src/server/server.ts new file mode 100644 index 00000000..01317c18 --- /dev/null +++ b/client/src/server/server.ts @@ -0,0 +1,373 @@ +import { LatLng } from 'leaflet'; +import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; +import { SpawnOptions } from '../controls/mapcontextmenu'; + +var connected: boolean = false; +var paused: boolean = false; + +var REST_ADDRESS = "http://localhost:30000/olympus"; +var DEMO_ADDRESS = window.location.href + "demo"; +const UNITS_URI = "units"; +const LOGS_URI = "logs"; +const AIRBASES_URI = "airbases"; +const BULLSEYE_URI = "bullseyes"; +const MISSION_URI = "mission"; + +var username = ""; +var credentials = ""; + +var sessionHash: string | null = null; +var lastUpdateTime = 0; +var demoEnabled = false; + +export function toggleDemoEnabled() { + demoEnabled = !demoEnabled; +} + +export function setCredentials(newUsername: string, newCredentials: string) { + username = newUsername; + credentials = newCredentials; +} + +export function GET(callback: CallableFunction, uri: string, options?: { time?: number }) { + var xmlHttp = new XMLHttpRequest(); + + /* Assemble the request options string */ + var optionsString = ''; + if (options?.time != undefined) + optionsString = `time=${options.time}`; + + + xmlHttp.open("GET", `${demoEnabled ? DEMO_ADDRESS : REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); + xmlHttp.onload = function (e) { + if (xmlHttp.status == 200) { + var data = JSON.parse(xmlHttp.responseText); + if (uri !== UNITS_URI || (options?.time == 0) || parseInt(data.time) > lastUpdateTime) { + callback(data); + lastUpdateTime = parseInt(data.time); + if (isNaN(lastUpdateTime)) + lastUpdateTime = 0; + setConnected(true); + } + } else if (xmlHttp.status == 401) { + console.error("Incorrect username/password"); + setConnectionStatus("failed"); + } else { + setConnected(false); + } + }; + xmlHttp.onerror = function (res) { + console.error("An error occurred during the XMLHttpRequest"); + setConnected(false); + }; + xmlHttp.send(null); +} + +export function POST(request: object, callback: CallableFunction) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("PUT", demoEnabled ? DEMO_ADDRESS : REST_ADDRESS); + xmlHttp.setRequestHeader("Content-Type", "application/json"); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); + xmlHttp.onreadystatechange = () => { + callback(); + }; + xmlHttp.send(JSON.stringify(request)); +} + +export function getConfig(callback: CallableFunction) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", window.location.href + "config", true); + xmlHttp.onload = function (e) { + var data = JSON.parse(xmlHttp.responseText); + callback(data); + }; + xmlHttp.onerror = function () { + console.error("An error occurred during the XMLHttpRequest, could not retrieve configuration file"); + }; + xmlHttp.send(null); +} + +export function setAddress(address: string, port: number) { + REST_ADDRESS = `http://${address}:${port}/olympus` + console.log(`Setting REST address to ${REST_ADDRESS}`) +} + +export function getAirbases(callback: CallableFunction) { + GET(callback, AIRBASES_URI); +} + +export function getBullseye(callback: CallableFunction) { + GET(callback, BULLSEYE_URI); +} + +export function getLogs(callback: CallableFunction) { + GET(callback, LOGS_URI); +} + +export function getMission(callback: CallableFunction) { + GET(callback, MISSION_URI); +} + +export function getUnits(callback: CallableFunction, refresh: boolean = false) { + GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }); +} + +export function addDestination(ID: number, path: any) { + var command = { "ID": ID, "path": path } + var data = { "setPath": command } + POST(data, () => { }); +} + +export function spawnSmoke(color: string, latlng: LatLng) { + var command = { "color": color, "location": latlng }; + var data = { "smoke": command } + POST(data, () => { }); +} + +export function spawnExplosion(intensity: number, latlng: LatLng) { + var command = { "intensity": intensity, "location": latlng }; + var data = { "explosion": command } + POST(data, () => { }); +} + +export function spawnGroundUnit(spawnOptions: SpawnOptions) { + var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition }; + var data = { "spawnGround": command } + POST(data, () => { }); +} + +export function spawnAircraft(spawnOptions: SpawnOptions) { + var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "" }; + var data = { "spawnAir": command } + POST(data, () => { }); +} + +export function attackUnit(ID: number, targetID: number) { + var command = { "ID": ID, "targetID": targetID }; + var data = { "attackUnit": command } + POST(data, () => { }); +} + +export function followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }) { + // X: front-rear, positive front + // Y: top-bottom, positive bottom + // Z: left-right, positive right + + var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] }; + var data = { "followUnit": command } + POST(data, () => { }); +} + +export function cloneUnit(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng }; + var data = { "cloneUnit": command } + POST(data, () => { }); +} + +export function deleteUnit(ID: number, explosion: boolean) { + var command = { "ID": ID, "explosion": explosion }; + var data = { "deleteUnit": command } + POST(data, () => { }); +} + +export function landAt(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng }; + var data = { "landAt": command } + POST(data, () => { }); +} + +export function changeSpeed(ID: number, speedChange: string) { + var command = { "ID": ID, "change": speedChange } + var data = { "changeSpeed": command } + POST(data, () => { }); +} + +export function setSpeed(ID: number, speed: number) { + var command = { "ID": ID, "speed": speed } + var data = { "setSpeed": command } + POST(data, () => { }); +} + +export function setSpeedType(ID: number, speedType: string) { + var command = { "ID": ID, "speedType": speedType } + var data = { "setSpeedType": command } + POST(data, () => { }); +} + +export function changeAltitude(ID: number, altitudeChange: string) { + var command = { "ID": ID, "change": altitudeChange } + var data = { "changeAltitude": command } + POST(data, () => { }); +} + +export function setAltitudeType(ID: number, altitudeType: string) { + var command = { "ID": ID, "altitudeType": altitudeType } + var data = { "setAltitudeType": command } + POST(data, () => { }); +} + +export function setAltitude(ID: number, altitude: number) { + var command = { "ID": ID, "altitude": altitude } + var data = { "setAltitude": command } + POST(data, () => { }); +} + +export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { + var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader } + var data = { "setLeader": command } + POST(data, () => { }); +} + +export function setROE(ID: number, ROE: string) { + var command = { "ID": ID, "ROE": ROE } + var data = { "setROE": command } + POST(data, () => { }); +} + +export function setReactionToThreat(ID: number, reactionToThreat: string) { + var command = { "ID": ID, "reactionToThreat": reactionToThreat } + var data = { "setReactionToThreat": command } + POST(data, () => { }); +} + +export function setEmissionsCountermeasures(ID: number, emissionCountermeasure: string) { + var command = { "ID": ID, "emissionsCountermeasures": emissionCountermeasure } + var data = { "setEmissionsCountermeasures": command } + POST(data, () => { }); +} + +export function setOnOff(ID: number, onOff: boolean) { + var command = { "ID": ID, "onOff": onOff } + var data = { "setOnOff": command } + POST(data, () => { }); +} + +export function setFollowRoads(ID: number, followRoads: boolean) { + var command = { "ID": ID, "followRoads": followRoads } + var data = { "setFollowRoads": command } + POST(data, () => { }); +} + +export function refuel(ID: number) { + var command = { "ID": ID }; + var data = { "refuel": command } + POST(data, () => { }); +} + +export function bombPoint(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombPoint": command } + POST(data, () => { }); +} + +export function carpetBomb(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "carpetBomb": command } + POST(data, () => { }); +} + +export function bombBuilding(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombBuilding": command } + POST(data, () => { }); +} + +export function fireAtArea(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "fireAtArea": command } + POST(data, () => { }); +} + +export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { + var command = { + "ID": ID, + "isTanker": isTanker, + "isAWACS": isAWACS, + "TACAN": TACAN, + "radio": radio, + "generalSettings": generalSettings + }; + + var data = { "setAdvancedOptions": command }; + POST(data, () => { }); +} + +export function startUpdate() { + /* On the first connection, force request of full data */ + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { getMissionData()?.update(data) }); + getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); + + requestUpdate(); + requestRefresh(); +} + +export function requestUpdate() { + /* Main update rate = 250ms is minimum time, equal to server update time. */ + getUnits((data: UnitsData) => { + if (!getPaused()) { + getUnitsManager()?.update(data); + checkSessionHash(data.sessionHash); + } + }, false); + window.setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000); + + getConnectionStatusPanel()?.update(getConnected()); +} + +export function requestRefresh() { + /* Main refresh rate = 5000ms. */ + getUnits((data: UnitsData) => { + if (!getPaused()) { + getUnitsManager()?.update(data); + getAirbases((data: AirbasesData) => getMissionData()?.update(data)); + getBullseye((data: BullseyesData) => getMissionData()?.update(data)); + getMission((data: any) => { + getMissionData()?.update(data) + }); + + // Update the list of existing units + getUnitDataTable()?.update(); + + checkSessionHash(data.sessionHash); + } + }, true); + window.setTimeout(() => requestRefresh(), 5000); +} + +export function checkSessionHash(newSessionHash: string) { + if (sessionHash != null) { + if (newSessionHash != sessionHash) + location.reload(); + } + else + sessionHash = newSessionHash; +} + +export function setConnected(newConnected: boolean) { + if (connected != newConnected) + newConnected ? getInfoPopup().setText("Connected to DCS Olympus server") : getInfoPopup().setText("Disconnected from DCS Olympus server"); + connected = newConnected; + + if (connected) { + document.querySelector("#splash-screen")?.classList.add("hide"); + document.querySelector("#gray-out")?.classList.add("hide"); + } +} + +export function getConnected() { + return connected; +} + +export function setPaused(newPaused: boolean) { + paused = newPaused; + paused ? getInfoPopup().setText("View paused") : getInfoPopup().setText("View unpaused"); +} + +export function getPaused() { + return paused; +} \ No newline at end of file diff --git a/client/src/units/aircraftdatabase.ts b/client/src/units/aircraftdatabase.ts new file mode 100644 index 00000000..6092ed81 --- /dev/null +++ b/client/src/units/aircraftdatabase.ts @@ -0,0 +1,3912 @@ +import { UnitDatabase } from "./unitdatabase" + +export class AircraftDatabase extends UnitDatabase { + constructor() { + super(); + this.blueprints = { + "A-10C_2": { + "name": "A-10C_2", + "era": ["Late Cold War", "Modern"], + "label": "A-10C Warthog", + "shortLabel": "10", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Mk-84", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "Mk-82", + "quantity": 6 + } + ], + "roles": [ + "CAS" + ], + "code": "Mk-82*6,Mk-84*2,AIM-9*2,ECM", + "name": "Heavy / Mk-84 / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AGM-65D", + "quantity": 4 + }, + { + "name": "CBU-97", + "quantity": 4 + }, + { + "name": "TGP", + "quantity": 1 + }, + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "AIM-9", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "AGM-65D*4, CBU-97*4,TGP, ECM, AIM-9*2", + "name": "Heavy / AGM-65D / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "GBU-12", + "quantity": 6 + }, + { + "name": "GBU-10", + "quantity": 2 + }, + { + "name": "TGP", + "quantity": 1 + }, + { + "name": "AIM-9", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "GBU-12*6,GBU-10*2,TGP, AIM-9*2", + "name": "Heavy / GBU-12 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "a-10.png" + }, + "AJS37": { + "name": "AJS37", + "label": "AJS37 Viggen", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "37", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "BK90", + "quantity": 2 + }, + { + "name": "RB-74", + "quantity": 2 + }, + { + "name": "XT", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "Strike: BK90 (MJ1)*2, RB-74*2, XT", + "name": "Heavy / BK90 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "ARAK M70 HE", + "quantity": 4 + }, + { + "name": "XT", + "quantity": 1 + } + ], + "roles": [ + "CAS" + ], + "code": "CAS: ARAK M70 HE*4, XT", + "name": "Heavy / ARAK M79 HE / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "RB05", + "quantity": 2 + }, + { + "name": "RB74", + "quantity": 2 + }, + { + "name": "XT", + "quantity": 1 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "Anti-ship (RB05): RB-05A*2, RB-74*2, XT", + "name": "Heavy / RB05 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "viggen.png" + }, + "AV8BNA": { + "name": "AV8BNA", + "label": "AV8BNA Harrier", + "era": ["Late Cold War", "Modern"], + "shortLabel": "8", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "GBU-38", + "quantity": 2 + }, + { + "name": "AIM-9M", + "quantity": 1 + }, + { + "name": "AGM-122 Sidearm", + "quantity": 1 + }, + { + "name": "Fuel 300", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "H-M-H 3", + "name": "Heavy / GBU-38 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AGM-65F", + "quantity": 4 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "GAU-12", + "quantity": 1 + } + ], + "roles": [ + "CAS" + ], + "code": "Anti Armor", + "name": "Heavy / AGM-65F / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "av8bna.png" + }, + "C-101CC": { + "name": "C-101CC", + "label": "C-101CC", + "era": ["Late Cold War", "Modern"], + "shortLabel": "101", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "DEFA 553 CANNON (I)", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "2*AIM-9M, DEFA 553 CANNON (I)", + "name": "Light / Fox 2 / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "BELOUGA", + "quantity": 2 + }, + { + "name": "BIN-200", + "quantity": 2 + }, + { + "name": "AN-M3 CANNON", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "2*AIM-9M ,2*BELOUGA,2*BIN-200, AN-M3 CANNON", + "name": "Heavy / BELOUGA, BIN-200 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "c-101.png" + }, + "H-6J": { + "name": "H-6J", + "label": "H-6J Badger", + "era": ["Mid Cold War, Late Cold War", "Modern"], + "shortLabel": "H6", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "250-3 LD Bomb", + "quantity": 36 + } + ], + "roles": [ + "Strike" + ], + "code": "250-3 LD Bomb x 36", + "name": "Heavy / Bombs / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "KD-20", + "quantity": 4 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "KD-20 x 4", + "name": "Heavy / KD-20 / Long Range" + } + ], + "filename": "h-6.png" + }, + "J-11A": { + "name": "J-11A", + "label": "J-11A Flaming Dragon", + "era": ["Modern"], + "shortLabel": "11", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "FAB-500", + "quantity": 8 + }, + { + "name": "R-73", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500x8,R-73x2,ECM", + "name": "Heavy / Fox 2 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-77", + "quantity": 2 + }, + { + "name": "R-73", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-77x2, R-73x2", + "name": "Light / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-27ER", + "quantity": 2 + }, + { + "name": "R-73", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-27ERx2, R-73x2", + "name": "Light / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-27.png" + }, + "JF-17": { + "name": "JF-17", + "label": "JF-17 Thunder", + "era": ["Modern"], + "shortLabel": "17", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "PL-5E2", + "quantity": 2 + }, + { + "name": "C802AK", + "quantity": 2 + }, + { + "name": "800L Tank", + "quantity": 1 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "PL-5Ex2, C802AKx2, 800L Tank", + "name": "Heavy / C802AK ASM / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "PL-5E2", + "quantity": 2 + }, + { + "name": "GBU-12", + "quantity": 2 + }, + { + "name": "800L Tank", + "quantity": 1 + }, + { + "name": "WMD7", + "quantity": 1 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "PL-5Ex2, 2*GBU-12x2, 800L Tank, WMD7", + "name": "Heavy / C802AK ASM / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "PL-5E2", + "quantity": 2 + }, + { + "name": "SD-10", + "quantity": 2 + }, + { + "name": "1100L Tank", + "quantity": 2 + }, + { + "name": "WMD7", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "PL-5Ex2, SD-10x2, 1100L Tankx2, WMD7", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "jf-17.png" + }, + "F-16C_50": { + "name": "F-16C_50", + "label": "F-16C Viper", + "era": ["Late Cold War", "Modern"], + "shortLabel": "16", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-120C", + "quantity": 2 + }, + { + "name": "AIM-9X", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-120C*2, AIM-9X*4, FUEL*2", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-120C", + "quantity": 2 + }, + { + "name": "AIM-9X", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "TGP", + "quantity": 1 + }, + { + "name": "AGM-65D", + "quantity": 4 + } + ], + "roles": [ + "CAS" + ], + "code": "AIM-120C*2, AIM-9X*2, AGM-65D*4, FUEL*2, ECM, TGP", + "name": "Heavy / Fox 3, AGM-65D / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-120C", + "quantity": 2 + }, + { + "name": "AIM-9X", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "TGP", + "quantity": 1 + }, + { + "name": "GBU-10", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "AIM-120C*2, AIM-9X*2, GBU-10*2, FUEL*2, ECM, TGP", + "name": "Heavy / Fox 3, GBU-10 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-16c.png" + }, + "F-5E-3": { + "name": "F-5E-3", + "label": "F-5E Tiger", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "5", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Fuel 275", + "quantity": 3 + }, + { + "name": "AIM-9P5", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9P5*2, Fuel 275*3", + "name": "Heavy / Fox 2 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "Mk-82", + "quantity": 4 + }, + { + "name": "AIM-9P5", + "quantity": 2 + }, + { + "name": "Fuel 275", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "Mk-82LD*4,AIM-9P*2,Fuel 275", + "name": "Heavy / Fox 2 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-5.png" + }, + "F-86F Sabre": { + "name": "F-86F Sabre", + "label": "F-86F Sabre", + "era": ["Early Cold War, Mid Cold War"], + "shortLabel": "86", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120gal Fuel", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "120gal Fuel*2", + "name": "Light / Guns / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "HVAR", + "quantity": 16 + } + ], + "roles": [ + "CAS" + ], + "code": "HVAR*16", + "name": "Light / HVAR / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AN-M64", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "AN-M64*2", + "name": "Light / AN-M64 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Light / Guns / Short Range" + } + ], + "filename": "f-5.png" + }, + "F-14A-135-GR": { + "name": "F-14A-135-GR", + "label": "F-14A-135-GR Tomcat", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "14A", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-54A", + "quantity": 2 + }, + { + "name": "AIM-7F", + "quantity": 1 + }, + { + "name": "AIM-9L", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-54A-MK47*2, AIM-7F*1, AIM-9L*4, XT*2", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-7F", + "quantity": 4 + }, + { + "name": "AIM-9L", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-7F*4, AIM-9L*4, XT*2", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-7M", + "quantity": 1 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "GBU-12", + "quantity": 2 + }, + { + "name": "LANTIRN", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "AIM-7M*1, AIM-9M*2, XT*2, GBU-12*2, LANTIRN", + "name": "Heavy / Fox 3, GBU-12 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-14.png" + }, + "F-14B": { + "name": "F-14B", + "label": "F-14B Tomcat", + "era": ["Late Cold War", "Modern"], + "shortLabel": "14B", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-54C", + "quantity": 2 + }, + { + "name": "AIM-7M", + "quantity": 3 + }, + { + "name": "AIM-9M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-54C-MK47*2, AIM-7M*3, AIM-9M*2, XT*2", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-7M", + "quantity": 6 + }, + { + "name": "AIM-9M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-7M*6, AIM-9M*2, XT*2", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-7M", + "quantity": 1 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "GBU-12", + "quantity": 2 + }, + { + "name": "LANTIRN", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "AIM-7M*1, AIM-9M*2, XT*2, GBU-12*2, LANTIRN", + "name": "Heavy / Fox 3, GBU-12 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-14.png" + }, + "FA-18C_hornet": { + "name": "FA-18C_hornet", + "era": ["Late Cold War", "Modern"], + "label": "F/A-18C", + "shortLabel": "18", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-120C-5", + "quantity": 6 + }, + { + "name": "AIM-9X", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9X*2, AIM-120C-5*6, FUEL*3", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-7M", + "quantity": 4 + }, + { + "name": "AIM-9M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9M*2, AIM-7M*4, FUEL*3", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 1 + }, + { + "name": "AIM-120C-5", + "quantity": 1 + }, + { + "name": "AIM-9X", + "quantity": 2 + }, + { + "name": "AGM-88C", + "quantity": 2 + } + ], + "roles": [ + "SEAD" + ], + "code": "AIM-9X*2, AIM-120C-5*2, AGM-88C*2, FUEL", + "name": "Heavy / Fox 3, AGM-88C / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 1 + }, + { + "name": "AIM-120C-5", + "quantity": 1 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "AGM-84D", + "quantity": 4 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "AIM-9M*2, AIM-120C-5*1, AGM-84D*4, ATFLIR, FUEL", + "name": "Heavy / Fox 3, AGM-84D / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 1 + }, + { + "name": "AIM-120C-5", + "quantity": 1 + }, + { + "name": "AIM-9X", + "quantity": 2 + }, + { + "name": "GBU-12", + "quantity": 4 + }, + { + "name": "GBU-38", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "AIM-9X*2, AIM-120C-5*1, GBU-38*4, GBU-12*4, ATFLIR, FUEL", + "name": "Heavy / Fox 3, GBU-12, GBU-38 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "fa-18c.png" + }, + "I-16": { + "name": "I-16", + "label": "I-16", + "era": ["WW2"], + "shortLabel": "I16", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "i-16.png" + }, + "L-39ZA": { + "name": "L-39ZA", + "label": "L-39ZA", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "39", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "S-5KO", + "quantity": 32 + } + ], + "roles": [ + "CAS" + ], + "code": "S-5KOx32", + "name": "Heavy / S-5KO / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-100", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-100x4", + "name": "Heavy / FAB-100 / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-60Mx2", + "name": "Light / Fox 2 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "l-39.png" + }, + "M-2000C": { + "name": "M-2000C", + "label": "M-2000C Mirage", + "era": ["Late Cold War", "Modern"], + "shortLabel": "M2KC", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Matra Magic II", + "quantity": 2 + }, + { + "name": "Super 530D", + "quantity": 2 + }, + { + "name": "Eclair", + "quantity": 1 + }, + { + "name": "fuel", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "Fox / S530D / Magic / Eclair", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "Matra Magic II", + "quantity": 2 + }, + { + "name": "Mk82", + "quantity": 4 + }, + { + "name": "fuel", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "Kilo / 4xMk-82 / Magic", + "name": "Heavy / Mk 82 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "Matra Magic II", + "quantity": 2 + }, + { + "name": "BAP-100", + "quantity": 18 + }, + { + "name": "fuel", + "quantity": 2 + } + ], + "roles": [ + "Runway Strike" + ], + "code": "Bravo / BAP-100 / Magic", + "name": "Heavy / BAP-100 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "m2000.png" + }, + "MB-339A": { + "name": "MB-339A", + "label": "MB-339A", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "339A", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "320L TipTanks", + "quantity": 2 + }, + { + "name": "DEFA 553 GunPods", + "quantity": 2 + }, + { + "name": "Mk83", + "quantity": 2 + }, + { + "name": "Mk81", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "A - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*Mk.83 + 2*Mk.81 ", + "name": "Heavy / Mk81, Mk83 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "320L TipTanks", + "quantity": 2 + }, + { + "name": "DEFA GunPods", + "quantity": 2 + }, + { + "name": "LAU-10(Zuni Rockets)", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "AA - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*LAU-10(Zuni Rockets) [ARMADA]", + "name": "Heavy / Mk 82 / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "c-101.png" + }, + "MiG-19P": { + "name": "MiG-19P", + "label": "MiG-19 Farmer", + "era": ["Early Cold War", "Mid Cold War"], + "shortLabel": "19", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "K-13A Atoll", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "K-13A x 2", + "name": "Light / Fox-2 / Short range" + }, + { + "fuel": 1, + "items": [ + { + "name": "K-13A Atoll", + "quantity": 2 + }, + { + "name": "167 gal tanks", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "K-13A x 2, PTB-760 x 2", + "name": "Medium / Fox-2 / Medium range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-250", + "quantity": 2 + }, + { + "name": "ORO-57K", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-250 x 2, ORO-57K x 2", + "name": "Medium / FAB250, ORO57K / Short range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Short range" + } + ], + "filename": "mig-19.png" + }, + "MiG-21Bis": { + "name": "MiG-21Bis", + "label": "MiG-21 Fishbed", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "21", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-3 Atoll", + "quantity": 2 + }, + { + "name": "R-60 Aphid", + "quantity": 2 + }, + { + "name": "130 gal tanks", + "quantity": 1 + }, + { + "name": "ASO-2 Countermeasures", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "Patrol, short range", + "name": "Light / Fox-2 / Short range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-3 Atoll", + "quantity": 2 + }, + { + "name": "R-60 Aphid", + "quantity": 2 + }, + { + "name": "210 gal tanks", + "quantity": 1 + }, + { + "name": "ASO-2 Countermeasures", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "Patrol, medium range", + "name": "Medium / Fox-2 / Medium range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-3R Atoll", + "quantity": 2 + }, + { + "name": "210 gal tanks", + "quantity": 1 + }, + { + "name": "ASO-2 Countermeasures", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "Patrol, R-3R Only", + "name": "Medium / Fox-1 / Medium range" + }, + { + "fuel": 1, + "items": [ + { + "name": "GROM", + "quantity": 2 + }, + { + "name": "FAB-250", + "quantity": 2 + }, + { + "name": "210 gal tanks", + "quantity": 1 + }, + { + "name": "ASO-2 Countermeasures", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "Few big targets, GROM + BOMBS", + "name": "Heavy / GROM, FAB250 / Medium range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-21.png" + }, + "Mirage-F1EE": { + "name": "Mirage-F1EE", + "label": "Mirage-F1EE", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "F1EE", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AIM-9JULI", + "quantity": 2 + }, + { + "name": "R530EM", + "quantity": 2 + }, + { + "name": "1137L Fuel Tank", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "2*AIM9-JULI, 2*R530EM, 1*Fuel Tank", + "name": "Medium / Fox 1 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AIM-9JULI", + "quantity": 2 + }, + { + "name": "SAMP 400 LD", + "quantity": 8 + } + ], + "roles": [ + "Strike" + ], + "code": "2*AIM-9JULI, 8*SAMP 400 LD", + "name": "Heavy / SAMP400 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-5.png" + }, + "A-20G": { + "name": "A-20G", + "label": "A-20G Havoc", + "era": ["WW2"], + "shortLabel": "A20", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 6 + }, + { + "name": "500lb Bomb LD", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "500 lb GP bomb LD*4", + "name": "Medium / Bombs / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "a-20.png" + }, + "Bf-109K-4": { + "name": "Bf-109K-4", + "label": "Bf-109K-4 Fritz", + "era": ["WW2"], + "shortLabel": "109", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "30mm MK108 Gun", + "quantity": 1 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + }, + { + "name": "SC500", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "500 lb GP bomb LD*4", + "name": "Medium / Bombs / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "30mm MK108 Gun", + "quantity": 1 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + }, + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Short Range" + } + ], + "filename": "bf109.png" + }, + "FW-190A8": { + "name": "FW-190A8", + "label": "FW-190A8 Bosch", + "era": ["WW2"], + "shortLabel": "190A8", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "20mm MG151 Gun", + "quantity": 4 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + }, + { + "name": "SD500", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "SD 500 A", + "name": "Medium / Bombs / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "20mm MG151 Gun", + "quantity": 4 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Short Range" + } + ], + "filename": "fw190.png" + }, + "FW-190D9": { + "name": "FW-190D9", + "label": "FW-190D9 Jerry", + "era": ["WW2"], + "shortLabel": "190D9", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "20mm MG151 Gun", + "quantity": 4 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + }, + { + "name": "SC500", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "SD 500 A", + "name": "Medium / Bombs / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "20mm MG151 Gun", + "quantity": 4 + }, + { + "name": "13mm MG131 Gun", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Short Range" + } + ], + "filename": "fw190.png" + }, + "MosquitoFBMkVI": { + "name": "MosquitoFBMkVI", + "label": "Mosquito FB MkVI", + "era": ["WW2"], + "shortLabel": "Mosquito", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "20mm Hispano Gun", + "quantity": 4 + }, + { + "name": "7.7mm MG", + "quantity": 4 + }, + { + "name": "500 lb GP Mk.V", + "quantity": 2 + }, + { + "name": "500 lb GP Short tail", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "500 lb GP Mk.V*2, 500 lb GP Short tail*2", + "name": "Medium / Bombs / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "20mm Hispano Gun", + "quantity": 4 + }, + { + "name": "7.7mm MG", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Medium Range" + } + ], + "filename": "mosquito.png" + }, + "P-47D-40": { + "name": "P-47D-40", + "label": "P-47D Thunderbolt", + "era": ["WW2"], + "shortLabel": "P47", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm HMG", + "quantity": 8 + }, + { + "name": "AN-M65", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "AN-M65*2", + "name": "Medium / Bombs / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "12.7mm HMG", + "quantity": 8 + } + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Medium Range" + } + ], + "filename": "p-47.png" + }, + "P-51D-30-NA": { + "name": "P-51D-30-NA", + "label": "P-51D Mustang", + "era": ["WW2", "Early Cold War"], + "shortLabel": "P51", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm HMG", + "quantity": 6 + }, + { + "name": "HVAR", + "quantity": 10 + } + ], + "roles": [ + "Strike" + ], + "code": "HVAR*10", + "name": "Medium / Rockets / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "12.7mm HMG", + "quantity": 6 + } + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Medium Range" + } + ], + "filename": "p-51.png" + }, + "A-50": { + "name": "A-50", + "label": "A-50 Mainstay", + "era": ["Late Cold War", "Modern"], + "shortLabel": "A50", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "AWACS" + ], + "code": "", + "name": "Default AWACS" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "a-50.png" + }, + "An-26B": { + "name": "An-26B", + "label": "An-26B Curl", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "26", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Default Transport" + } + ], + "filename": "an-26.png" + }, + "An-30M": { + "name": "An-30M", + "label": "An-30M Clank", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "30", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default Reconnaissance" + } + ], + "filename": "a-50.png" + }, + "B-1B": { + "name": "B-1B", + "label": "B-1B Lancer", + "era": ["Late Cold War", "Modern"], + "shortLabel": "1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Mk-84", + "quantity": 24 + } + ], + "roles": [ + "Strike" + ], + "code": "Mk-84*24", + "name": "Heavy / Mk-84 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "b-1.png" + }, + "B-52H": { + "name": "B-52H", + "label": "B-52H Stratofortress", + "era": ["Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "52", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Mk-84", + "quantity": 18 + } + ], + "roles": [ + "Strike" + ], + "code": "Mk-84*18", + "name": "Heavy / Mk-84 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "b-52.png" + }, + "C-130": { + "name": "C-130", + "label": "C-130 Hercules", + "era": ["Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "130", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "C-130", + "name": "Default Transport" + } + ], + "filename": "c-130.png" + }, + "C-17A": { + "name": "C-17A", + "label": "C-17A Globemaster", + "era": ["Modern"], + "shortLabel": "C17", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Default Transport" + } + ], + "filename": "c-17.png" + }, + "E-3A": { + "name": "E-3A", + "label": "E-3A Sentry", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "E3", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "AWACS" + ], + "code": "", + "name": "Blue Air Force AWACS" + } + ], + "filename": "e-3.png" + }, + "E-2C": { + "name": "E-2C", + "label": "E-2C Hawkeye", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "2C", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "AWACS" + ], + "code": "", + "name": "Blue Naval AWACS" + } + ], + "filename": "e-2.png" + }, + "F-117A": { + "name": "F-117A", + "label": "F-117A Nighthawk", + "era": ["Late Cold War", "Modern"], + "shortLabel": "117", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "GBU-10", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "GBU-10*2", + "name": "Heavy / GBU-10 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-117.png" + }, + "F-15C": { + "name": "F-15C", + "label": "F-15C Eagle", + "era": ["Late Cold War", "Modern"], + "shortLabel": "15", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-120B", + "quantity": 6 + }, + { + "name": "AIM-9M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9*2,AIM-120*6,Fuel*3", + "name": "Heavy / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-7", + "quantity": 4 + }, + { + "name": "AIM-9M", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9*4,AIM-7*4,Fuel", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-15.png" + }, + "F-15E": { + "name": "F-15E", + "label": "F-15E Strike Eagle", + "era": ["Late Cold War", "Modern"], + "shortLabel": "15", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 3 + }, + { + "name": "AIM-120B", + "quantity": 2 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "Mk-84", + "quantity": 8 + } + ], + "roles": [ + "CAS" + ], + "code": "AIM-120B*2,AIM-9M*2,FUEL*3,Mk-84*8", + "name": "Heavy / Fox 3, Mk-84 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 1 + }, + { + "name": "AIM-120B", + "quantity": 2 + }, + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "GBU-12", + "quantity": 4 + }, + { + "name": "GBU-38", + "quantity": 4 + }, + { + "name": "AGM-154C", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-154C*2", + "name": "Heavy / Fox 3, GBU-12, GBU-38, AGM-154C / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-15.png" + }, + "F-4E": { + "name": "F-4E", + "label": "F-4E Phantom II", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "4", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "AIM-7M", + "quantity": 4 + }, + { + "name": "AIM-9M", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "AIM-9*4,AIM-7*4,Fuel*2", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "AIM-7M", + "quantity": 2 + }, + { + "name": "Mk-82", + "quantity": 18 + } + ], + "roles": [ + "Strike" + ], + "code": "Mk-82*18,AIM-7*2,ECM", + "name": "Heavy / Fox 1, Mk-82 / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "ECM", + "quantity": 1 + }, + { + "name": "AIM-7M", + "quantity": 2 + }, + { + "name": "AGM-65K", + "quantity": 4 + }, + { + "name": "Fuel", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "AGM-65K*4,AIM-7*2,Fuel*2,ECM", + "name": "Heavy / Fox 1, AGM-65K / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "f-4.png" + }, + "IL-76MD": { + "name": "IL-76MD", + "label": "IL-76MD Candid", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "76", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Default Transport" + } + ], + "filename": "il-76.png" + }, + "IL-78M": { + "name": "IL-78M", + "label": "IL-78M Midas", + "era": ["Late Cold War", "Modern"], + "shortLabel": "78", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Tanker" + ], + "code": "", + "name": "Default Tanker" + } + ], + "filename": "il-76.png" + }, + "KC-135": { + "name": "KC-135", + "label": "KC-135 Stratotanker", + "era": ["Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "135", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Tanker" + ], + "code": "", + "name": "Default Tanker" + } + ], + "filename": "kc-135.png" + }, + "KC135MPRS": { + "name": "KC135MPRS", + "label": "KC-135 MPRS Stratotanker", + "era": ["Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "135M", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Tanker" + ], + "code": "", + "name": "Default Tanker" + } + ], + "filename": "kc-135.png" + }, + "S-3B Tanker": { + "name": "S-3B Tanker", + "label": "S-3B Tanker", + "era": ["Early Cold War", "Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "S3B", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Tanker" + ], + "code": "", + "name": "Default Tanker" + } + ], + "filename": "s-3.png" + }, + "MiG-15bis": { + "name": "MiG-15bis", + "label": "MiG-15 Fagot", + "era": ["Early Cold War", "Mid Cold War"], + "shortLabel": "M15", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "300L Fuel Tanks", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "2*300L", + "name": "Medium / Guns / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-100M", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "2*FAB-100M", + "name": "Medium / FAB-100M / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "CAP" + ], + "code": "", + "name": "Light / Guns / Short Range" + }, + ], + "filename": "mig-15.png" + }, + "MiG-23MLD": { + "name": "MiG-23MLD", + "label": "MiG-23 Flogger", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "23", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Fuel-800", + "quantity": 1 + }, + { + "name": "R-60M", + "quantity": 4 + }, + { + "name": "R-24R", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-24R*2,R-60M*4,Fuel-800", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "Fuel-800", + "quantity": 1 + }, + { + "name": "FAB-500", + "quantity": 2 + }, + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500*2,R-60M*2,Fuel-800", + "name": "Heavy / FAB-500 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-23.png" + }, + "MiG-25RBT": { + "name": "MiG-25RBT", + "label": "MiG-25RBT Foxbat", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "25", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "R-60M*2", + "name": "Heavy / Fox 2 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-500", + "quantity": 2 + }, + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500x2_60x2", + "name": "Heavy / FAB-500 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-25.png" + }, + "MiG-25PD": { + "name": "MiG-25PD", + "label": "MiG-25PD Foxbat", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "25", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-40R", + "quantity": 2 + }, + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-40R*2,R-60M*2", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-25.png" + }, + "MiG-27K": { + "name": "MiG-27K", + "label": "MiG-27K Flogger-D", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "27", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "B-8", + "quantity": 4 + } + ], + "roles": [ + "CAS" + ], + "code": "B-8*4", + "name": "Heavy / B-8 / Short Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "Kh-29L", + "quantity": 2 + }, + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "Kh-29L*2,R-60M*2,Fuel", + "name": "Heavy / Fox 2, Kh-29L / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-250", + "quantity": 6 + }, + { + "name": "R-60M", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-250*6,R-60M*2,Fuel", + "name": "Heavy / Fox 2, FAB250 / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-23.png" + }, + "MiG-29A": { + "name": "MiG-29A", + "label": "MiG-29A Fulcrum", + "era": ["Late Cold War", "Modern"], + "shortLabel": "29A", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 4 + }, + { + "name": "R-27R", + "quantity": 2 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27R*2,Fuel-1500", + "name": "Heavy / Fox 1, HOBS Fox 2 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 4 + }, + { + "name": "R-27R", + "quantity": 2 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "R-60M*4,R-27R*2,Fuel-1500", + "name": "Heavy / Fox 1 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "FAB-500", + "quantity": 4 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500*4,R-73*2,Fuel", + "name": "Heavy / Fox 2, FAB500 / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-29.png" + }, + "MiG-29S": { + "name": "MiG-29S", + "label": "MiG-29S Fulcrum", + "era": ["Late Cold War", "Modern"], + "shortLabel": "29", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 4 + }, + { + "name": "R-27R", + "quantity": 2 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27R*2,Fuel-1500", + "name": "Heavy / Fox 1 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 4 + }, + { + "name": "R-27R", + "quantity": 2 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "CAP" + ], + "code": "R-60M*4,R-27R*2", + "name": "Heavy / Fox 1 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "S-24", + "quantity": 4 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "CAS" + ], + "code": "S-24*4,R-73*2,Fuel", + "name": "Heavy / Fox 2, S-24 / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "FAB-500", + "quantity": 4 + }, + { + "name": "Fuel-1500", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500*4,R-73*2,Fuel", + "name": "Heavy / Fox 2, FAB500 / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-29.png" + }, + "MiG-31": { + "name": "MiG-31", + "label": "MiG-31 Foxhound", + "era": ["Late Cold War", "Modern"], + "shortLabel": "31", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-33", + "quantity": 4 + }, + { + "name": "R-40T", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-40T*2,R-33*4", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mig-23.png" + }, + "MQ-9 Reaper": { + "name": "MQ-9 Reaper", + "label": "MQ-9 Reaper", + "era": ["Modern"], + "shortLabel": "9", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AGM-114K", + "quantity": 12 + } + ], + "roles": [ + "Drone" + ], + "code": "AGM-114K*12", + "name": "Default Drone" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "i-16.png" + }, + "Su-17M4": { + "name": "Su-17M4", + "label": "Su-17M4 Fitter", + "era": ["Mid Cold War", "Late Cold War"], + "shortLabel": "17M4", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + }, + { + "name": "B-8", + "quantity": 4 + }, + { + "name": "fuel", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "B-8*4,R-60M*2,Fuel*2", + "name": "Heavy / B-8 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-17.png" + }, + "Su-24M": { + "name": "Su-24M", + "label": "Su-24M Fencer", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "24", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + }, + { + "name": "FAB-1500", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-1500*2,R-60M*2", + "name": "Heavy / FAB-500 / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-24.png" + }, + "Su-25": { + "name": "Su-25", + "label": "Su-25A Frogfoot", + "era": ["Late Cold War"], + "shortLabel": "S25", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + }, + { + "name": "UB-13", + "quantity": 6 + }, + { + "name": "fuel", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "UB-13*6,R-60M*2,Fuel*2", + "name": "Heavy / Rockets / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-60M", + "quantity": 2 + }, + { + "name": "B-8MI", + "quantity": 2 + }, + { + "name": "RBK-500", + "quantity": 2 + }, + { + "name": "Kh-25ML", + "quantity": 2 + }, + { + "name": "2-25L", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "2-25L*2, KH-25ML*2, RBK-500*2, B-8MI*2, R-60M*2", + "name": "Heavy / Everything A-G / Medium Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-25.png" + }, + "Su-25T": { + "name": "Su-25", + "label": "Su-25T Frogfoot", + "era": ["Late Cold War", "Modern"], + "shortLabel": "S25T", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Kh-29L", + "quantity": 2 + }, + { + "name": "Kh-25ML", + "quantity": 4 + }, + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "Mercury LLTV Pod", + "quantity": 1 + }, + { + "name": "MPS-410", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "Kh-29L*2,Kh-25ML*4,R-73*2,Mercury LLTV Pod,MPS-410", + "name": "Heavy / Everything A-G / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "APU-8 Vikhr-M", + "quantity": 2 + }, + { + "name": "Kh-25ML", + "quantity": 2 + }, + { + "name": "SPPU-22*2", + "quantity": 2 + }, + { + "name": "Mercury LLTV Pod", + "quantity": 1 + }, + { + "name": "MPS-410", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "APU-8 Vikhr-M*2,Kh-25ML,R-73*2,SPPU-22*2,Mercury LLTV Pod,MPS-410", + "name": "Heavy / Everything A-G / Medium Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-500", + "quantity": 6 + }, + { + "name": "R-60M", + "quantity": 2 + }, + { + "name": "Fuel", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-500*6,R-60M*2,Fuel*2", + "name": "Medium / FAB-500 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-25.png" + }, + "Su-27": { + "name": "Su-27", + "label": "Su-27 Flanker", + "era": ["Late Cold War", "Modern"], + "shortLabel": "27", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-27ER", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27ER*2,ECM", + "name": "Light / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-27ER", + "quantity": 2 + }, + { + "name": "R-27ET", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27ER*2,R-27ET*2,ECM", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-27ET", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27ET*2,ECM", + "name": "Heavy / Fox 2 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "S-25", + "quantity": 4 + }, + { + "name": "FAB-500", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "S-25*4, FAB-500*4, R-73*2, ECM", + "name": "Heavy / Fox 2, Bombs, Rockets / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-27.png" + }, + "Su-30": { + "name": "Su-30", + "label": "Su-30 Super Flanker", + "era": ["Modern"], + "shortLabel": "30", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-77", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-77*2", + "name": "Light / Fox 3 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-77", + "quantity": 2 + }, + { + "name": "R-27ER", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-77*2,R-27ER*2,ECM", + "name": "Heavy / Fox 3, Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-77", + "quantity": 2 + }, + { + "name": "FAB-1500", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-1500*2,R-73*2,R-77*2,ECM", + "name": "Heavy / Fox 3, Bombs / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-34.png" + }, + "Su-33": { + "name": "Su-33", + "label": "Su-33 Naval Flanker", + "era": ["Late Cold War", "Modern"], + "shortLabel": "33", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-27ER", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2, R-27ER*2", + "name": "Light / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "R-27ER", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 2 + } + ], + "roles": [ + "CAP" + ], + "code": "R-73*2,R-27ET*2,R-27ER*2,ECM", + "name": "Heavy / Fox 1 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "S-25", + "quantity": 4 + }, + { + "name": "FAB-250", + "quantity": 4 + }, + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 2 + } + ], + "roles": [ + "Strike" + ], + "code": "S-25*4,FAB-250*4,R-73*2,ECM", + "name": "Heavy / Rockets, Bombs / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-34.png" + }, + "Su-34": { + "name": "Su-34", + "label": "Su-34 Hellduck", + "era": ["Modern"], + "shortLabel": "34", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "R-73", + "quantity": 2 + }, + { + "name": "FAB-250", + "quantity": 4 + }, + { + "name": "UB-13", + "quantity": 4 + }, + { + "name": "ECM", + "quantity": 1 + } + ], + "roles": [ + "CAS" + ], + "code": "UB-13*4,FAB-250*4,R-73*2,ECM", + "name": "Heavy / Mixed Ground Ordinance / Short Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "su-34.png" + }, + "Tornado IDS": { + "name": "Tornado IDS", + "label": "Tornado IDS", + "era": ["Late Cold War", "Modern"], + "shortLabel": "IDS", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "Mk-82", + "quantity": 4 + } + ], + "roles": [ + "CAS" + ], + "code": "Mk-82*4,AIM-9*2,Fuel*2", + "name": "Heavy / Mk-84 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tornado.png" + }, + "Tornado GR4": { + "name": "Tornado GR4", + "label": "Tornado GR4", + "era": ["Late Cold War", "Modern"], + "shortLabel": "GR4", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "ALARM", + "quantity": 4 + }, + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + } + ], + "roles": [ + "SEAD" + ], + "code": "ALARM*4, Fuel*2, ECM", + "name": "Heavy / ALARM / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "GBU-16", + "quantity": 2 + }, + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + } + ], + "roles": [ + "Strike" + ], + "code": "GBU-16*2, AIM-9M*2, Fuel*2, ECM", + "name": "Heavy / GBU-16 / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "AIM-9M", + "quantity": 2 + }, + { + "name": "Sea Eagle", + "quantity": 2 + }, + { + "name": "fuel", + "quantity": 2 + }, + { + "name": "ECM", + "quantity": 1 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "Sea Eagle*2, AIM-9M*2, Fuel*2, ECM", + "name": "Heavy / Sea Eagle / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tornado.png" + }, + "Tu-142": { + "name": "Tu-142", + "label": "Tu-142 Bear", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "142", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Kh-35", + "quantity": 6 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "Kh-35*6", + "name": "Heavy / Kh-35 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tu-95.png" + }, + "Tu-160": { + "name": "Tu-160", + "label": "Tu-160 Blackjack", + "era": ["Late Cold War", "Modern"], + "shortLabel": "160", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Kh-65", + "quantity": 12 + } + ], + "roles": [ + "Strike" + ], + "code": "Kh-65*12", + "name": "Heavy / Kh-65 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tu-160.png" + }, + "Tu-22M3": { + "name": "Tu-22M3", + "label": "Tu-22M3 Backfire", + "era": ["Late Cold War", "Modern"], + "shortLabel": "T22", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Kh-22n", + "quantity": 2 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "Kh-22N*2", + "name": "Heavy / Kh-22N / Long Range" + }, + { + "fuel": 1, + "items": [ + { + "name": "FAB-250", + "quantity": 69 + } + ], + "roles": [ + "Strike" + ], + "code": "FAB-250*69", + "name": "Heavy / Kh-22n / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tu-22.png" + }, + "Tu-95MS": { + "name": "Tu-95MS", + "label": "Tu-95MS Bear", + "era": ["Mid Cold War", "Late Cold War", "Modern"], + "shortLabel": "95", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Kh-65", + "quantity": 6 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "Kh-65*6", + "name": "Heavy / Kh-65 / Long Range" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "tu-95.png" + } + } + } +} + +export var aircraftDatabase = new AircraftDatabase(); + diff --git a/client/src/units/groundunitsdatabase.ts b/client/src/units/groundunitsdatabase.ts new file mode 100644 index 00000000..b3ed6b62 --- /dev/null +++ b/client/src/units/groundunitsdatabase.ts @@ -0,0 +1,3530 @@ +import { UnitDatabase } from "./unitdatabase" + +export class GroundUnitsDatabase extends UnitDatabase { + constructor() { + super(); + this.blueprints = { + "SA-2 SAM Battery": { + "name": "SA-2 SAM Battery", + "label": "SA-2 SAM Battery", + "shortLabel": "SA-2 SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [ + ], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-3 SAM Battery": { + "name": "SA-3 SAM Battery", + "label": "SA-3 SAM Battery", + "shortLabel": "SA-3 SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-6 SAM Battery": { + "name": "SA-6 SAM Battery", + "label": "SA-6 SAM Battery", + "shortLabel": "SA-6 SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-10 SAM Battery": { + "name": "SA-10 SAM Battery", + "label": "SA-10 SAM Battery", + "shortLabel": "SA-10 SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-11 SAM Battery": { + "name": "SA-11 SAM Battery", + "label": "SA-11 SAM Battery", + "shortLabel": "SA-11 SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot site": { + "name": "Patriot site", + "label": "Patriot site", + "shortLabel": "Patriot site", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk SAM Battery": { + "name": "Hawk SAM Battery", + "label": "Hawk SAM Battery", + "shortLabel": "Hawk SAM Battery", + "loadouts": [ + { + "fuel": 1, + "items": [], + "roles": [ + "Template" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "2B11 mortar": { + "name": "2B11 mortar", + "label": "2B11 mortar", + "shortLabel": "2B11 mortar", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm Mortar Tube", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SAU Gvozdika": { + "name": "SAU Gvozdika", + "label": "SAU Gvozdika", + "shortLabel": "SAU Gvozdika", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "122mm Howitzer", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SAU Msta": { + "name": "SAU Msta", + "label": "SAU Msta", + "shortLabel": "SAU Msta", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "152mm Howitzer", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SAU Akatsia": { + "name": "SAU Akatsia", + "label": "SAU Akatsia", + "shortLabel": "SAU Akatsia", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "152mm Howitzer", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SAU 2-C9": { + "name": "SAU 2-C9", + "label": "SAU Nona", + "shortLabel": "SAU Nona", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm Mortar", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M-109": { + "name": "M-109", + "label": "M-109 Paladin", + "shortLabel": "M-109", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "155mm Howitzer", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Gun Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "AAV7": { + "name": "AAV7", + "label": "AAV7", + "shortLabel": "AAV7", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BMD-1": { + "name": "BMD-1", + "label": "BMD-1", + "shortLabel": "BMD-1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "73mm Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 3, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "AT-3 Sagger ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BMP-1": { + "name": "BMP-1", + "label": "BMP-1", + "shortLabel": "BMP-1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "73mm Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 3, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "AT-3 Sagger ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BMP-2": { + "name": "BMP-2", + "label": "BMP-2", + "shortLabel": "BMP-2", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "30mm Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 3, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "AT-5 Konkurs ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BMP-3": { + "name": "BMP-3", + "label": "BMP-3", + "shortLabel": "BMP-3", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "100mm Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "30mm Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 3, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "AT-10 Stabber ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Boman": { + "name": "Boman", + "label": "Grad Fire Direction Manager", + "shortLabel": "Boman", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "7.62mm PKMB GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "RPG-7", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BRDM-2": { + "name": "BRDM-2", + "label": "BRDM-2", + "shortLabel": "BRDM-2", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "14.5mm KPVT HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BTR-80": { + "name": "BTR-80", + "label": "BTR-80", + "shortLabel": "BTR-80", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "14.5mm KPVT HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "BTR_D": { + "name": "BTR_D", + "label": "BTR_D", + "shortLabel": "BTR_D", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "7.62mm PKT GPMG", + "quantity": 2, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "AT-5 Konkurs ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Bunker": { + "name": "Bunker", + "label": "Bunker", + "shortLabel": "Bunker", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Static" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Cobra": { + "name": "Cobra", + "label": "Otokar Cobra", + "shortLabel": "Cobra", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "LAV-25": { + "name": "LAV-25", + "label": "LAV-25", + "shortLabel": "LAV-25", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "25mm M242 Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm M240 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1043 HMMWV Armament": { + "name": "M1043 HMMWV Armament", + "label": "HMMWV M2 Browning", + "shortLabel": "HMMWV M2", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1045 HMMWV TOW": { + "name": "M1045 HMMWV TOW", + "label": "HMMWV TOW", + "shortLabel": "HMMWV TOW", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "BGM-71 TOW ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Reconnaissance" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1126 Stryker ICV": { + "name": "M1126 Stryker ICV", + "label": "Stryker MG", + "shortLabel": "Stryker MG", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M-113": { + "name": "M-113", + "label": "M-113", + "shortLabel": "M-113", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1134 Stryker ATGM": { + "name": "M1134 Stryker ATGM", + "label": "Stryker ATGM", + "shortLabel": "Stryker ATGM", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "BGM-71 TOW", + "quantity": 2, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M-2 Bradley": { + "name": "M-2 Bradley", + "label": "M-2A2 Bradley", + "shortLabel": "M-2 Bradley", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "25mm M242 Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "BGM-71 TOW", + "quantity": 2, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm M240 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Marder": { + "name": "Marder", + "label": "Marder", + "shortLabel": "Marder", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "20mm MK 20 Rh 202 Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm MG3 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "MCV-80": { + "name": "MCV-80", + "label": "Warrior IFV", + "shortLabel": "Warrior", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "30mm L21A1 Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm L94A1 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "IFV" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "MTLB": { + "name": "MTLB", + "label": "MT-LB", + "shortLabel": "MT-LB", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Paratrooper RPG-16": { + "name": "Paratrooper RPG-16", + "label": "Paratrooper RPG-16", + "shortLabel": "Paratrooper RPG-16", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "RPG-16", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Paratrooper AKS-74": { + "name": "Paratrooper AKS-74", + "label": "Paratrooper AKS-74", + "shortLabel": "Paratrooper AKS-74", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.45mm AKS-74", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Sandbox": { + "name": "Sandbox", + "label": "Sandbox", + "shortLabel": "Sandbox", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Static" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Soldier AK": { + "name": "Soldier AK", + "label": "Soldier AK", + "shortLabel": "Soldier AK", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.45mm AK-74", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Infantry AK": { + "name": "Infantry AK", + "label": "Infantry AK", + "shortLabel": "Infantry AK", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.45mm AK-74", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Soldier M249": { + "name": "Soldier M249", + "label": "Soldier M249", + "shortLabel": "Soldier M249", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.56mm M249 SAW", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Soldier M4": { + "name": "Soldier M4", + "label": "Soldier M4", + "shortLabel": "Soldier M4", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.56mm M4", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Soldier M4 GRG": { + "name": "Soldier M4 GRG", + "label": "Soldier M4 GRG", + "shortLabel": "Soldier M4 GRG", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "5.56mm M4", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Soldier RPG": { + "name": "Soldier RPG", + "label": "Soldier RPG", + "shortLabel": "Soldier RPG", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "RPG-16", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Infantry" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "TPZ": { + "name": "TPZ", + "label": "TPz Fuchs", + "shortLabel": "TPz Fuchs", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "7.62mm M3 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (soft)" + } + ], + "roles": [ + "APC" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Grad-URAL": { + "name": "Grad-URAL", + "label": "Grad", + "shortLabel": "Grad", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "122mm Grad 9M21 Rocket", + "quantity": 40, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Rocket Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Uragan_BM-27": { + "name": "Uragan_BM-27", + "label": "Uragan", + "shortLabel": "Uragan", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "220mm Uragan 9M27 Rocket", + "quantity": 16, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Rocket Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Smerch": { + "name": "Smerch", + "label": "Smerch", + "shortLabel": "Smerch", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "300mm Smerch 9M55 Rocket", + "quantity": 12, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Rocket Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "MLRS": { + "name": "MLRS", + "label": "M270", + "shortLabel": "M270", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "227mm with 644 DPICM Submunitions", + "quantity": 12, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Rocket Artillery" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "2S6 Tunguska": { + "name": "2S6 Tunguska", + "label": "SA-19 Tunguska", + "shortLabel": "SA-19", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Twin Barrel 30mm 2A38M Autocannons", + "quantity": 2, + "effectiveAgainst": "Surface (Soft), Aircraft" + }, + { + "name": "9M311 SAM (Radio Command Guidance)", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA/SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Kub 2P25 ln": { + "name": "Kub 2P25 ln", + "label": "SA-6 Kub 2P25 ln", + "shortLabel": "Kub 2P25 ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "3M9M SAM (SARH)", + "quantity": 3, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "5p73 s-125 ln": { + "name": "5p73 s-125 ln", + "label": "SA-3 5p73 s-125 ln", + "shortLabel": "5p73 s-125 ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "SA-3 3M9M SAM (RF CLOS)", + "quantity": 3, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 5P85C ln": { + "name": "S-300PS 5P85C ln", + "label": "SA-10 S-300PS 5P85C ln", + "shortLabel": "S-300PS 5P85C ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "48N6 SAM (SARH)", + "quantity": 2, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 5P85D ln": { + "name": "S-300PS 5P85D ln", + "label": "SA-10 S-300PS 5P85D ln", + "shortLabel": "S-300PS 5P85D ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "48N6 SAM (SARH)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-11 Buk LN 9A310M1": { + "name": "SA-11 Buk LN 9A310M1", + "label": "SA-11 Buk LN 9A310M1", + "shortLabel": "SA-11 Buk LN 9A310M1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M38M1 SAM (SARH)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Osa 9A33 ln": { + "name": "Osa 9A33 ln", + "label": "SA-8 Osa 9A33 ln", + "shortLabel": "Osa 9A33 ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M33 SAM (SARH)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Tor 9A331": { + "name": "Tor 9A331", + "label": "SA-15 Tor 9A331", + "shortLabel": "Tor 9A331", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M330 SAM (Radio Command Guidance)", + "quantity": 8, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Strela-10M3": { + "name": "Strela-10M3", + "label": "SA-13 Strela-10M3", + "shortLabel": "Strela-10M3", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M333 SAM (IR)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Strela-1 9P31": { + "name": "Strela-1 9P31", + "label": "SA-9 Strela-1 9P31", + "shortLabel": "Strela-1 9P31", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M31 SAM (IR)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-11 Buk CC 9S470M1": { + "name": "SA-11 Buk CC 9S470M1", + "label": "SA-11 Buk CC 9S470M1", + "shortLabel": "SA-11 Buk CC 9S470M1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Command Post", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-8 Osa LD 9T217": { + "name": "SA-8 Osa LD 9T217", + "label": "SA-8 Osa LD 9T217", + "shortLabel": "SA-8 Osa LD 9T217", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Transloader", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot AMG": { + "name": "Patriot AMG", + "label": "Patriot AMG", + "shortLabel": "Patriot AMG", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Antenna Mast Group", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot ECS": { + "name": "Patriot ECS", + "label": "Patriot ECS", + "shortLabel": "Patriot ECS", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Engagement Control Station", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Gepard": { + "name": "Gepard", + "label": "Gepard", + "shortLabel": "Gepard", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "35mm KDA Autocannon", + "quantity": 2, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk pcp": { + "name": "Hawk pcp", + "label": "Hawk pcp", + "shortLabel": "Hawk pcp", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Command Post", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-18 Igla manpad": { + "name": "SA-18 Igla manpad", + "label": "SA-18 Igla manpad", + "shortLabel": "SA-18 Igla manpad", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9K38 SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-18 Igla comm": { + "name": "SA-18 Igla comm", + "label": "SA-18 Igla comm", + "shortLabel": "SA-18 Igla comm", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Commander", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Igla manpad INS": { + "name": "Igla manpad INS", + "label": "SA-18 Igla manpad INS", + "shortLabel": "Igla manpad INS", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9K38 SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-18 Igla-S manpad": { + "name": "SA-18 Igla-S manpad", + "label": "SA-18 Igla-S manpad", + "shortLabel": "SA-18 Igla-S manpad", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9K338 SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-18 Igla-S comm": { + "name": "SA-18 Igla-S comm", + "label": "SA-18 Igla-S comm", + "shortLabel": "SA-18 Igla-S comm", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Commander", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Vulcan": { + "name": "Vulcan", + "label": "Vulcan", + "shortLabel": "Vulcan", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "M168 20mm Vulcan", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk ln": { + "name": "Hawk ln", + "label": "Hawk ln", + "shortLabel": "Hawk ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "MIM 23B SAM (SARH)", + "quantity": 3, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M48 Chaparral": { + "name": "M48 Chaparral", + "label": "M48 Chaparral", + "shortLabel": "M48 Chaparral", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "MIM-72G SAM (IR)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M6 Linebacker": { + "name": "M6 Linebacker", + "label": "M6 Linebacker", + "shortLabel": "M6 Linebacker", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "M242 25mm Autocannon", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm M240C GPMG", + "quantity": 4, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "Stinger SAM (IR)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot ln": { + "name": "Patriot ln", + "label": "Patriot ln", + "shortLabel": "Patriot ln", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "MIM-104 SAM (SARH)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1097 Avenger": { + "name": "M1097 Avenger", + "label": "M1097 Avenger", + "shortLabel": "M1097 Avenger", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "12.7mm M2 HMG", + "quantity": 2, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "Stinger SAM (IR)", + "quantity": 4, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot EPP": { + "name": "Patriot EPP", + "label": "Patriot EPP", + "shortLabel": "Patriot EPP", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Diesel-Electric Generator", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot cp": { + "name": "Patriot cp", + "label": "Patriot cp", + "shortLabel": "Patriot cp", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Command Post", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Roland ADS": { + "name": "Roland ADS", + "label": "Roland ADS", + "shortLabel": "Roland ADS", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "MIM-115 SAM (Radio Command Guidance)", + "quantity": 2, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 54K6 cp": { + "name": "S-300PS 54K6 cp", + "label": "SA-10 S-300PS 54K6 cp", + "shortLabel": "S-300PS 54K6 cp", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Command Post", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Stinger manpad GRG": { + "name": "Stinger manpad GRG", + "label": "Stinger manpad GRG", + "shortLabel": "Stinger manpad GRG", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Stinger SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Stinger manpad dsr": { + "name": "Stinger manpad dsr", + "label": "Stinger manpad dsr", + "shortLabel": "Stinger manpad dsr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Stinger SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Stinger comm dsr": { + "name": "Stinger comm dsr", + "label": "Stinger comm dsr", + "shortLabel": "Stinger comm dsr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Commander", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Stinger manpad": { + "name": "Stinger manpad", + "label": "Stinger manpad", + "shortLabel": "Stinger manpad", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Stinger SAM (IR)", + "quantity": 1, + "effectiveAgainst": "Aircraft" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Stinger comm": { + "name": "Stinger comm", + "label": "Stinger comm", + "shortLabel": "Stinger comm", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Commander", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZSU-23-4 Shilka": { + "name": "ZSU-23-4 Shilka", + "label": "ZSU-23-4 Shilka", + "shortLabel": "ZSU-23-4 Shilka", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm AZP-23M Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZU-23 Emplacement Closed": { + "name": "ZU-23 Emplacement Closed", + "label": "ZU-23 Emplacement Closed", + "shortLabel": "ZU-23 Emplacement Closed", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZU-23 Emplacement": { + "name": "ZU-23 Emplacement", + "label": "ZU-23 Emplacement", + "shortLabel": "ZU-23 Emplacement", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZU-23 Closed Insurgent": { + "name": "ZU-23 Closed Insurgent", + "label": "ZU-23 Closed Insurgent", + "shortLabel": "ZU-23 Closed Insurgent", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-375 ZU-23 Insurgent": { + "name": "Ural-375 ZU-23 Insurgent", + "label": "Ural-375 ZU-23 Insurgent", + "shortLabel": "Ural-375 ZU-23 Insurgent", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZU-23 Insurgent": { + "name": "ZU-23 Insurgent", + "label": "ZU-23 Insurgent", + "shortLabel": "ZU-23 Insurgent", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-375 ZU-23": { + "name": "Ural-375 ZU-23", + "label": "Ural-375 ZU-23", + "shortLabel": "Ural-375 ZU-23", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "23mm 2A14 Autocannon", + "quantity": 4, + "effectiveAgainst": "Surface (Soft), Aircraft" + } + ], + "roles": [ + "AAA" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "1L13 EWR": { + "name": "1L13 EWR", + "label": "1L13 EWR", + "shortLabel": "1L13 EWR", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Early Warning Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "Radar" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Kub 1S91 str": { + "name": "Kub 1S91 str", + "label": "SA-6 Kub 1S91 str", + "shortLabel": "Kub 1S91 str", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search and Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 40B6M tr": { + "name": "S-300PS 40B6M tr", + "label": "SA-10 S-300PS 40B6M tr", + "shortLabel": "S-300PS 40B6M tr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 40B6MD sr": { + "name": "S-300PS 40B6MD sr", + "label": "SA-10 S-300PS 40B6MD sr", + "shortLabel": "S-300PS 40B6MD sr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "55G6 EWR": { + "name": "55G6 EWR", + "label": "55G6 EWR", + "shortLabel": "55G6 EWR", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Early Warning Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "Radar" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "S-300PS 64H6E sr": { + "name": "S-300PS 64H6E sr", + "label": "SA-10 S-300PS 64H6E sr", + "shortLabel": "S-300PS 64H6E sr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SA-11 Buk SR 9S18M1": { + "name": "SA-11 Buk SR 9S18M1", + "label": "SA-11 Buk SR 9S18M1", + "shortLabel": "SA-11 Buk SR 9S18M1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Dog Ear radar": { + "name": "Dog Ear radar", + "label": "Dog Ear radar", + "shortLabel": "Dog Ear radar", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk tr": { + "name": "Hawk tr", + "label": "Hawk tr", + "shortLabel": "Hawk tr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk sr": { + "name": "Hawk sr", + "label": "Hawk sr", + "shortLabel": "Hawk sr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Patriot str": { + "name": "Patriot str", + "label": "Patriot str", + "shortLabel": "Patriot str", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search and Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hawk cwar": { + "name": "Hawk cwar", + "label": "Hawk cwar", + "shortLabel": "Hawk cwar", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search and Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "p-19 s-125 sr": { + "name": "p-19 s-125 sr", + "label": "SA-3 p-19 s-125 sr", + "shortLabel": "p-19 s-125 sr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Roland Radar": { + "name": "Roland Radar", + "label": "Roland Radar", + "shortLabel": "Roland Radar", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Search Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "snr s-125 tr": { + "name": "snr s-125 tr", + "label": "SA-3 snr s-125 tr", + "shortLabel": "snr s-125 tr", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Track Radar", + "quantity": 1, + "effectiveAgainst": "None" + } + ], + "roles": [ + "SAM" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "house1arm": { + "name": "house1arm", + "label": "house1arm", + "shortLabel": "house1arm", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Structure" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "house2arm": { + "name": "house2arm", + "label": "house2arm", + "shortLabel": "house2arm", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Structure" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "outpost_road": { + "name": "outpost_road", + "label": "outpost_road", + "shortLabel": "outpost_road", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Structure" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "outpost": { + "name": "outpost", + "label": "outpost", + "shortLabel": "outpost", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Structure" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "houseA_arm": { + "name": "houseA_arm", + "label": "houseA_arm", + "shortLabel": "houseA_arm", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Structure" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Challenger2": { + "name": "Challenger2", + "label": "Challenger2", + "shortLabel": "Challenger2", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm L30A1 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm L94A1 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "7.62mm L37A2 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Leclerc": { + "name": "Leclerc", + "label": "Leclerc", + "shortLabel": "Leclerc", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm F1 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "12.7mm M2HB HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Leopard1A3": { + "name": "Leopard1A3", + "label": "Leopard1A3", + "shortLabel": "Leopard1A3", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "105mm L7A3 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm MG3 GPMG", + "quantity": 2, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Leopard-2": { + "name": "Leopard-2", + "label": "Leopard-2", + "shortLabel": "Leopard-2", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm Rh L/44 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm MG3 GPMG", + "quantity": 2, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M-60": { + "name": "M-60", + "label": "M-60", + "shortLabel": "M-60", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "105mm M68 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm M73 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm M85 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M1128 Stryker MGS": { + "name": "M1128 Stryker MGS", + "label": "M1128 Stryker MGS", + "shortLabel": "M1128 Stryker MGS", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "105mm M68 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm M240 GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "SPG" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M-1 Abrams": { + "name": "M-1 Abrams", + "label": "M-1 Abrams", + "shortLabel": "M-1 Abrams", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "120mm Rh L/44 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm M240 GPMG", + "quantity": 2, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm M2 HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "T-55": { + "name": "T-55", + "label": "T-55", + "shortLabel": "T-55", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "100mm D-10T Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm SGMT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm DShK HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "T-72B": { + "name": "T-72B", + "label": "T-72B", + "shortLabel": "T-72B", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "125mm 2A46M Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm NSVT HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "T-80UD": { + "name": "T-80UD", + "label": "T-80UD", + "shortLabel": "T-80UD", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "125mm 2A46M Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm NSVT HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "T-90": { + "name": "T-90", + "label": "T-90", + "shortLabel": "T-90", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "125mm 2A46M-5 Gun", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + }, + { + "name": "7.62mm PKT GPMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "12.7mm NSVT HMG", + "quantity": 1, + "effectiveAgainst": "Surface (Soft)" + }, + { + "name": "9K119M ATGM", + "quantity": 1, + "effectiveAgainst": "Surface (Hard)" + } + ], + "roles": [ + "Tank" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-4320 APA-5D": { + "name": "Ural-4320 APA-5D", + "label": "Ural-4320 APA-5D", + "shortLabel": "Ural-4320 APA-5D", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ATMZ-5": { + "name": "ATMZ-5", + "label": "ATMZ-5", + "shortLabel": "ATMZ-5", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ATZ-10": { + "name": "ATZ-10", + "label": "ATZ-10", + "shortLabel": "ATZ-10", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "GAZ-3307": { + "name": "GAZ-3307", + "label": "GAZ-3307", + "shortLabel": "GAZ-3307", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "GAZ-3308": { + "name": "GAZ-3308", + "label": "GAZ-3308", + "shortLabel": "GAZ-3308", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "GAZ-66": { + "name": "GAZ-66", + "label": "GAZ-66", + "shortLabel": "GAZ-66", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M978 HEMTT Tanker": { + "name": "M978 HEMTT Tanker", + "label": "M978 HEMTT Tanker", + "shortLabel": "M978 HEMTT Tanker", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "HEMTT TFFT": { + "name": "HEMTT TFFT", + "label": "HEMTT TFFT", + "shortLabel": "HEMTT TFFT", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "IKARUS Bus": { + "name": "IKARUS Bus", + "label": "IKARUS Bus", + "shortLabel": "IKARUS Bus", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "KAMAZ Truck": { + "name": "KAMAZ Truck", + "label": "KAMAZ Truck", + "shortLabel": "KAMAZ Truck", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "LAZ Bus": { + "name": "LAZ Bus", + "label": "LAZ Bus", + "shortLabel": "LAZ Bus", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Hummer": { + "name": "Hummer", + "label": "Hummer", + "shortLabel": "Hummer", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "M 818": { + "name": "M 818", + "label": "M 818", + "shortLabel": "M 818", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "MAZ-6303": { + "name": "MAZ-6303", + "label": "MAZ-6303", + "shortLabel": "MAZ-6303", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Predator GCS": { + "name": "Predator GCS", + "label": "Predator GCS", + "shortLabel": "Predator GCS", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Predator TrojanSpirit": { + "name": "Predator TrojanSpirit", + "label": "Predator TrojanSpirit", + "shortLabel": "Predator TrojanSpirit", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Suidae": { + "name": "Suidae", + "label": "Suidae", + "shortLabel": "Suidae", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Tigr_233036": { + "name": "Tigr_233036", + "label": "Tigr_233036", + "shortLabel": "Tigr_233036", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Trolley bus": { + "name": "Trolley bus", + "label": "Trolley bus", + "shortLabel": "Trolley bus", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "UAZ-469": { + "name": "UAZ-469", + "label": "UAZ-469", + "shortLabel": "UAZ-469", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural ATsP-6": { + "name": "Ural ATsP-6", + "label": "Ural ATsP-6", + "shortLabel": "Ural ATsP-6", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-375 PBU": { + "name": "Ural-375 PBU", + "label": "Ural-375 PBU", + "shortLabel": "Ural-375 PBU", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-375": { + "name": "Ural-375", + "label": "Ural-375", + "shortLabel": "Ural-375", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-4320-31": { + "name": "Ural-4320-31", + "label": "Ural-4320-31", + "shortLabel": "Ural-4320-31", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "Ural-4320T": { + "name": "Ural-4320T", + "label": "Ural-4320T", + "shortLabel": "Ural-4320T", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "VAZ Car": { + "name": "VAZ Car", + "label": "VAZ Car", + "shortLabel": "VAZ Car", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZiL-131 APA-80": { + "name": "ZiL-131 APA-80", + "label": "ZiL-131 APA-80", + "shortLabel": "ZiL-131 APA-80", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "SKP-11": { + "name": "SKP-11", + "label": "SKP-11", + "shortLabel": "SKP-11", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZIL-131 KUNG": { + "name": "ZIL-131 KUNG", + "label": "ZIL-131 KUNG", + "shortLabel": "ZIL-131 KUNG", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + }, + "ZIL-4331": { + "name": "ZIL-4331", + "label": "ZIL-4331", + "shortLabel": "ZIL-4331", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Unarmed" + ], + "code": "", + "name": "Default" + } + ], + "filename": "" + } + } + } +} + +export var groundUnitsDatabase = new GroundUnitsDatabase(); \ No newline at end of file diff --git a/client/src/units/helicopterdatabase.ts b/client/src/units/helicopterdatabase.ts new file mode 100644 index 00000000..d7fc9996 --- /dev/null +++ b/client/src/units/helicopterdatabase.ts @@ -0,0 +1,571 @@ +import { UnitDatabase } from "./unitdatabase" + +export class HelicopterDatabase extends UnitDatabase { + constructor() { + super(); + this.blueprints = { + "AH-64D_BLK_II": { + "name": "AH-64D_BLK_II", + "label": "AH-64D Apache", + "shortLabel": "AH64", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AGM-114k Hellfire", + "quantity": 8 + }, + { + "name": "M151 Rocket Pod", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "2 * M261: M151 (6PD), 2 * Hellfire station: 4*AGM-114K", + "name": "Gun / ATGM / Rocket" + }, + { + "fuel": 1, + "items": [ + { + "name": "AGM-114K Hellfire", + "quantity": 16 + } + ], + "roles": [ + "CAS" + ], + "code": "4 * Hellfire station: 4*AGM-114K", + "name": "Gun / ATGM" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "ah-64.png" + }, + "Ka-50_3": { + "name": "Ka-50_3", + "label": "Ka-50 Hokum A", + "shortLabel": "K50", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Igla", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "4xIgla", + "name": "Gun / Fox 2" + }, + { + "fuel": 1, + "items": [ + { + "name": "Igla", + "quantity": 4 + }, + { + "name": "S-13", + "quantity": 10 + }, + { + "name": "Kh-25ML", + "quantity": 2 + } + ], + "roles": [ + "Anti-Ship" + ], + "code": "2xKh-25ML, 10xS-13, 4xIgla", + "name": "Gun / ASM / Rockets / Fox 2" + }, + { + "fuel": 1, + "items": [ + { + "name": "Igla", + "quantity": 4 + }, + { + "name": "S-80FP", + "quantity": 40 + }, + { + "name": "Vikhr-M", + "quantity": 12 + } + ], + "roles": [ + "CAS" + ], + "code": "12x9A4172, 40xS-8OFP, 4xIgla", + "name": "Gun / ATGM / Rockets / Fox 2" + }, + { + "fuel": 1, + "items": [ + { + "name": "Igla", + "quantity": 4 + }, + { + "name": "S-80FP", + "quantity": 40 + }, + { + "name": "Vikhr-M", + "quantity": 12 + } + ], + "roles": [ + "CAS" + ], + "code": "12x9A4172, 40xS-8OFP, 4xIgla", + "name": "Gun / ATGM" + }, + { + "fuel": 1, + "items": [ + { + "name": "Igla", + "quantity": 4 + }, + { + "name": "FAB-500", + "quantity": 2 + }, + { + "name": "S-13", + "quantity": 10 + } + ], + "roles": [ + "Strike" + ], + "code": "10xS-13, 2xFAB-500, 4xIgla", + "name": "Gun / Bombs / Rockets / Fox 2" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "ka-50.png" + }, + "Mi-24P": { + "name": "Mi-24P", + "label": "Mi-24P Hind", + "shortLabel": "Mi24", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "S-8KOM", + "quantity": 40 + }, + { + "name": "9M114 ATGM", + "quantity": 8 + } + ], + "roles": [ + "CAS" + ], + "code": "2xB8V20 (S-8KOM)+8xATGM 9M114", + "name": "Gun / ATGM / Rockets" + }, + { + "fuel": 1, + "items": [ + { + "name": "S-24B", + "quantity": 4 + }, + { + "name": "9M114 ATGM", + "quantity": 4 + } + ], + "roles": [ + "Strike" + ], + "code": "4xS-24B+4xATGM 9M114", + "name": "Gun / ATGM / Rockets" + }, + { + "fuel": 1, + "items": [ + { + "name": "GUV-1 Grenade Launcher", + "quantity": 4 + }, + { + "name": "9M114 ATGM", + "quantity": 4 + } + ], + "roles": [ + "CAS" + ], + "code": "4xGUV-1 AP30+4xATGM 9M114", + "name": "Gun / ATGM / Grenade Launcher" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mi-24.png" + }, + "SA342L": { + "name": "SA342L", + "label": "SA342L Gazelle", + "shortLabel": "342", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "20mm Cannon", + "quantity": 1 + }, + { + "name": "SNEB68", + "quantity": 8 + } + ], + "roles": [ + "Recon" + ], + "code": "M621, 8xSNEB68 EAP", + "name": "Gun / ATGM / Rockets" + } + ], + "filename": "sa-342.png" + }, + "SA342M": { + "name": "SA342M", + "label": "SA342M Gazelle", + "shortLabel": "342", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "HOT3", + "quantity": 4 + } + ], + "roles": [ + "CAS" + ], + "code": "HOT3x4", + "name": "ATGM" + } + ], + "filename": "sa-342.png" + }, + "SA342Mistral": { + "name": "SA342Mistral", + "label": "SA342Mistral Gazelle", + "shortLabel": "342", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "Mistral", + "quantity": 4 + } + ], + "roles": [ + "CAP" + ], + "code": "Mistral x 4", + "name": "Fox 2" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "sa-342.png" + }, + "AH-1W": { + "name": "AH-1W", + "label": "AH-1W Cobra", + "shortLabel": "AH1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "BGM-71 TOW", + "quantity": 8 + }, + { + "name": "Hydra-70 WP", + "quantity": 38 + } + ], + "roles": [ + "CAS" + ], + "code": "8xBGM-71, 38xHYDRA-70 WP", + "name": "TOW / Hydra" + }, + { + "fuel": 1, + "items": [ + { + "name": "Hydra-70", + "quantity": 76 + } + ], + "roles": [ + "CAS" + ], + "code": "76xHYDRA-70", + "name": "Hydra" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "ah-1.png" + }, + "Mi-26": { + "name": "Mi-26", + "label": "Mi-26 Halo", + "shortLabel": "M26", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mi-26.png" + }, + "Mi-28N": { + "name": "Mi-28N", + "label": "Mi-28N Havoc", + "shortLabel": "M28", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "9M114 Shturm", + "quantity": 16 + }, + { + "name": "S-8", + "quantity": 40 + } + ], + "roles": [ + "CAS" + ], + "code": "16x9M114, 40xS-8", + "name": "ATGM / S-8" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mi-28.png" + }, + "Mi-8MT": { + "name": "Mi-8MT", + "label": "Mi-8MT Hip", + "shortLabel": "Mi8", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "UPK", + "quantity": 2 + }, + { + "name": "B8", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "2 x UPK +2 x B8", + "name": "Rockets / Gunpods" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "mi-8.png" + }, + "SH-60B": { + "name": "SH-60B", + "label": "SH-60B Blackhawk", + "shortLabel": "S60", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "AGM-119 ASM", + "quantity": 1 + } + ], + "roles": [ + "CAS" + ], + "code": "AGM-119", + "name": "ASM" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "uh-60.png" + }, + "UH-60A": { + "name": "UH-60A", + "label": "UH-60A Blackhawk", + "shortLabel": "U60", + "loadouts": [ + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "uh-60.png" + }, + "UH-1H": { + "name": "UH-1H", + "label": "UH-1H Huey", + "shortLabel": "UH1", + "loadouts": [ + { + "fuel": 1, + "items": [ + { + "name": "M134 Minigun", + "quantity": 2 + }, + { + "name": "XM-158", + "quantity": 2 + } + ], + "roles": [ + "CAS" + ], + "code": "M134 Minigun*2, XM158*2", + "name": "Miniguns / XM158" + }, + { + "fuel": 1, + "items": [ + + ], + "roles": [ + "Transport" + ], + "code": "", + "name": "Empty Loadout" + } + ], + "filename": "uh-1.png" + } + } + } +} + +export var helicopterDatabase = new HelicopterDatabase(); + diff --git a/client/src/units/payloadNames.ts b/client/src/units/payloadNames.ts deleted file mode 100644 index 1523661f..00000000 --- a/client/src/units/payloadNames.ts +++ /dev/null @@ -1 +0,0 @@ -export var payloadNames: any = { "A-10A": ["MK-84*2 , LAU-68*2 , AGM-65K*2", "LAU-68-MK5*6", "AGM-65K*2,Mk20*6,AIM-9*2,ECM", "Mk-82*6,AIM-9*2,ECM", "Mk20*6,AIM-9*2,ECM", "AGM-65D*4,AIM-9*2,ECM", "AGM-65K, AGM-65D", "ECM", "Mk-82*6, Mk-84*2", "Mk20*8", "AGM-65K*2,Mk-84*2,AIM-9*2,ECM", "AGM-65K*2,Mk-82*6,AIM-9*2,ECM", "Mk-84*2,AIM-9*2,ECM", "AGM-65K*2,Mk84*2,Mk82*4,AIM-9M*2,ECM", "AGM-65H*6,Mk82*10,AIM-9M*2,ECM"], "A-10C": ["LAU-68 42 rkt M156 WP, AIM-9*2, ECM", "AGM-65D*4, CBU-97*2, CBU-87*2, TGP, ECM, AIM-9*2", "LAU-131 98 rkt M156 WP, AIM-9*2,ECM", "SUU-25*9,AIM-9*2,ECM", "AGM-65D*4, CBU-97*4,TGP, ECM, AIM-9*2", "Mk-82AIR*8,AIM-9*2,ECM", "MK-84*2,LAU-68*2,AGM-65K*2", "BDU-33*6, TGM-65H, TGM-65D, TGP, BDU-50LGB*2, CAP-9*1", "Mk-82*6,Mk-84*2,AIM-9*2,ECM", "Mk-84*4,AIM-9*2,ECM", "Mk-82*8,AIM-9*2,ECM", "BDU-33*12, TGP, CAP-9*1", "AGM-65D*4,Mk-82AIR*2,CBU-87*2,AIM-9M*2,ECM,TGP", "AGM-65D*4,GBU-12*2,GBU-38,Mk-82,AIM-9,TGP,ECM", "AGM-65D*2, AGM-65H*2, CBU-97*2, CBU-87*2, TGP, ECM, AIM-9*2", "BDU-50HD*6,Mk1*7,TGP, CAP-9*1", "AGM-65H*4, CBU-97*4,TGP, ECM, AIM-9*2", "AGM-65D*2,AGM-65H*2,Mk-82AIR*2,CBU-87*2,AIM-9M*2,ECM,TGP", "AGM-65K*2,GBU-38*4,AIM-9*2,TGP,ECM", "BDU-33*6, TGP, CAP-9*1", "AGM-65D*2,AGM-65H*2,GBU-12*2,GBU-38*2,AIM-9*2,TGP,ECM,MK151*7", "TGP", "BDU-33*6, TGP, CAP-9*1, BDU-50LD*2", "GBU-12*6,GBU-10*2,TGP, AIM-9*2", "TGP, CBU-87*3, M151*28, AIM-9*2, ECM", "AGM-65D*4,Mk-82*6,CBU-87*2,TGP,AIM-9*2,Mk151*7", "PGM- GBU-10*2,GBU-12*4,AIM-9*2,TGP,ECM", "AGM-65D*4,TGP, ECM, AIM-9*2", "TGP, CAP-9*1, CATM-65K*1, TGM-65G*1", "AGM-65G*2,GBU-31*2,AIM-9*2,TGP,ECM", "TGP, M151*14, Mk-82*2, Mk-82AIR*2, AIM-9*2, ECM", "PGM- GBU-10*4, AGM-65K*2,AIM-9*2,TGP,ECM", "AGM-65D*2,AGM-65H*2,Mk-82AIR*6,CBU-87*2,Mk151*7,AIM-9*2,TGP,ECM", "GBU-31*2,GBU-38*2, AGM-65H*2, AIM-9*2,TGP, ECM", "CBU-103*4, M151*14, AIM-9*2, ECM", "CBU-87*4, M151*42, AIM-9*2, ECM", "AGM-65D*6, CBU-97*4,TGP, ECM, AIM-9*2", "CBU-87*2, M151*14, MK-82AIR*6, AIM-9*2,ECM", "AGM-65D*4, CBU-105*4,TGP, ECM, AIM-9*2", "BDU-50HD*2,BDU-50LGB*2,TGP, CAP-9*1", "CBU-87*4, M151*28, AIM-9*2,ECM", "M151*98, Mk-82*2,AIM-9*2,ECM", "AGM-65D*2,AGM-65H*2,GBU-12,GBU-38,MK82*3,MK82AIR*3,MK5*7,TGP,AM-9*2", "TGP, M151*42, Mk-82*6, Mk-82AIR*6, AIM-9*2, ECM", "TGP, M151*84, Mk-82*2,AIM-9*2, ECM", "BDU-50LD*2, BDU-50HD*2,CATM-65K, TGM-65G, TGP, CAP-9*1", "TGP, M151*49, Mk-82*2, CBU-87*2, AIM-9*2, ECM", "TGP, CAP-9*1, BDU-50LGB*4", "GBU-12*14,TGP, AIM-9*2", "AGM-65D*3, AGM-65H*3, CBU-97*4,TGP, ECM, AIM-9*2", "AGM-65D*2,AGM-65H*2,Mk-82AIR*2,CBU-97*2,AIM-9M*2,TGP,ECM", "AGM-65D*4, CBU-105*2,CBU-97*2, TGP, ECM, AIM-9*2", "AGM-65D*2,Mk-82*6,AIM-9*2,ECM", "AGM-65D*2,AGM-65H*2,TGP, ECM, AIM-9*2", "GBU-38*4,GBU-31*2,TGP, AIM-9*2", "AGM-65D*4,GBU-12*2,GBU-38*2,AIM-9*2,TGP,ECM,MK5*7", "AGM-65G,AGM-65K,GBU-10*2,AIM-9*2,TGP,ECM", "AGM-65G,AGM-65D,Mk-82*7,AIM-9*2,ECM", "GBU-31*2,GBU-38*4,AIM-9*2,TGP,ECM, AIM-9*2", "AGM-65K*2,GBU-12*8,AIM-9M*2.ECM,TGP", "AGM-65D*6,GBU-12*4,AIM-9M*2,ECM,TGP"], "A-10C_2": ["LAU-68 42 rkt M156 WP, AIM-9*2, ECM", "AGM-65D*4, CBU-97*2, CBU-87*2, TGP, ECM, AIM-9*2", "LAU-131 98 rkt M156 WP, AIM-9*2,ECM", "SUU-25*9,AIM-9*2,ECM", "AGM-65D*4, CBU-97*4,TGP, ECM, AIM-9*2", "Mk-82AIR*8,AIM-9*2,ECM", "MK-84*2,LAU-68*2,AGM-65K*2", "BDU-33*6, TGM-65H, TGM-65D, TGP, BDU-50LGB*2, CAP-9*1", "Mk-82*6,Mk-84*2,AIM-9*2,ECM", "Mk-84*4,AIM-9*2,ECM", "Mk-82*8,AIM-9*2,ECM", "BDU-33*12, TGP, CAP-9*1", "AGM-65D*4,Mk-82AIR*2,CBU-87*2,AIM-9M*2,ECM,TGP", "AGM-65D*4,GBU-12*2,GBU-38,Mk-82,AIM-9,TGP,ECM", "AGM-65D*2, AGM-65H*2, CBU-97*2, CBU-87*2, TGP, ECM, AIM-9*2", "BDU-50HD*6,Mk1*7,TGP, CAP-9*1", "AGM-65H*4, CBU-97*4,TGP, ECM, AIM-9*2", "AGM-65D*2,AGM-65H*2,Mk-82AIR*2,CBU-87*2,AIM-9M*2,ECM,TGP", "AGM-65K*2,GBU-38*4,AIM-9*2,TGP,ECM", "BDU-33*6, TGP, CAP-9*1", "AGM-65D*2,AGM-65H*2,GBU-12*2,GBU-38*2,AIM-9*2,TGP,ECM,MK151*7", "TGP", "BDU-33*6, TGP, CAP-9*1, BDU-50LD*2", "GBU-12*6,GBU-10*2,TGP, AIM-9*2", "TGP, CBU-87*3, M151*28, AIM-9*2, ECM", "AGM-65D*4,Mk-82*6,CBU-87*2,TGP,AIM-9*2,Mk151*7", "PGM- GBU-10*2,GBU-12*4,AIM-9*2,TGP,ECM", "AGM-65D*4,TGP, ECM, AIM-9*2", "TGP, CAP-9*1, CATM-65K*1, TGM-65G*1", "AGM-65G*2,GBU-31*2,AIM-9*2,TGP,ECM", "TGP, M151*14, Mk-82*2, Mk-82AIR*2, AIM-9*2, ECM", "PGM- GBU-10*4, AGM-65K*2,AIM-9*2,TGP,ECM", "AGM-65D*2,AGM-65H*2,Mk-82AIR*6,CBU-87*2,Mk151*7,AIM-9*2,TGP,ECM", "GBU-31*2,GBU-38*2, AGM-65H*2, AIM-9*2,TGP, ECM", "CBU-103*4, M151*14, AIM-9*2, ECM", "CBU-87*4, M151*42, AIM-9*2, ECM", "AGM-65D*6, CBU-97*4,TGP, ECM, AIM-9*2", "CBU-87*2, M151*14, MK-82AIR*6, AIM-9*2,ECM", "AGM-65D*4, CBU-105*4,TGP, ECM, AIM-9*2", "BDU-50HD*2,BDU-50LGB*2,TGP, CAP-9*1", "CBU-87*4, M151*28, AIM-9*2,ECM", "M151*98, Mk-82*2,AIM-9*2,ECM", "AGM-65D*2,AGM-65H*2,GBU-12,GBU-38,MK82*3,MK82AIR*3,MK5*7,TGP,AM-9*2", "TGP, M151*42, Mk-82*6, Mk-82AIR*6, AIM-9*2, ECM", "TGP, M151*84, Mk-82*2,AIM-9*2, ECM", "BDU-50LD*2, BDU-50HD*2,CATM-65K, TGM-65G, TGP, CAP-9*1", "TGP, M151*49, Mk-82*2, CBU-87*2, AIM-9*2, ECM", "TGP, CAP-9*1, BDU-50LGB*4", "GBU-12*14,TGP, AIM-9*2", "AGM-65D*3, AGM-65H*3, CBU-97*4,TGP, ECM, AIM-9*2", "AGM-65D*2,AGM-65H*2,Mk-82AIR*2,CBU-97*2,AIM-9M*2,TGP,ECM", "AGM-65D*4, CBU-105*2,CBU-97*2, TGP, ECM, AIM-9*2", "AGM-65D*2,Mk-82*6,AIM-9*2,ECM", "AGM-65D*2,AGM-65H*2,TGP, ECM, AIM-9*2", "GBU-38*4,GBU-31*2,TGP, AIM-9*2", "AGM-65D*4,GBU-12*2,GBU-38*2,AIM-9*2,TGP,ECM,MK5*7", "AGM-65G,AGM-65K,GBU-10*2,AIM-9*2,TGP,ECM", "AGM-65G,AGM-65D,Mk-82*7,AIM-9*2,ECM", "GBU-31*2,GBU-38*4,AIM-9*2,TGP,ECM, AIM-9*2", "AGM-65K*2,GBU-12*8,AIM-9M*2.ECM,TGP", "AGM-65D*6,GBU-12*4,AIM-9M*2,ECM,TGP", "AGM-65E*2,Mk-82AIR*2,CBU-97*2,AIM-9M*2,ECM,TGP", "AGM-65E*2,CBU-97*4,AIM-9M*2,ECM,TGP", "AGM-65E*2,CBU-97*4,AIM-9M*2,ECM,M151 APKWS*7,TGP", "AGM-65E*2,CBU-105*4,AIM-9M*2,ECM,M151 APKWS*7,TGP", "Mk-82*4,Mk-8AIR*4,AIM-9*2,ECM", "Mk-82*20,AIM-9*2,ECM", "Mk-82*6,AIM-9*2,TGP,ECM", "Mk-84*6,AIM-9*2,TGP,ECM", "Mk-82AIR*6,Mk-8AIR*4,M151*1,TGP,AIM-9*2,ECM", "GBU-38*4,M151 APKWS*7,AGM-65D*1,AGM-65H*1,TGP,AIM-9*2,ECM", "GBU-38*4,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-12*4,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-12*2,GBU-38*2,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-10*2,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-31*2,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-54*4,M151 APKWS*7,AGM-65E*2,TGP,AIM-9*2,ECM", "GBU-54*4,M151 APKWS*7,AGM-65D*4,TGP,AIM-9*2,ECM"], "AH-64D_BLK_II": ["4 * Fuel Tank 230 gal", "2 * M261: M151 (6PD), 2 * Hellfire station: 4*AGM-114K", "4 * Hellfire station: 4*AGM-114K", "4 * M261: M151 (6PD)", "2 * M261: M151 (6PD), 2 * Fuel Tank 230 gal", "2 * Fuel Tank 230 gal, 2 * Hellfire station: 4*AGM-114K", "2 * M261: A/B - M151 (6PD), E - M274 (6SK), 2 * Hellfire station: 4*AGM-114K", "2 * M261: A/B - M151 (6PD), E - M257 (6IL), 2 * Hellfire station: 4*AGM-114K", "2 * M261: C - M257 (6IL), D/E - M151 (6PD), 2 * Hellfire station: 4*AGM-114K", "2 * M261: C - M274 (6SK), D/E - M151 (6PD), 2 * Hellfire station: 4*AGM-114K"], "AJS37": ["Battlefield Air Interdiction: RB-75*4, RB-24J*2, XT", "Anti-ship: RB-04E*2, RB-74*2, XT", "Anti-ship (Heavy Mav): RB-75T*4, XT", "Hard Target (Countermeasures): RB-05, XT, KB, U22", "Hard Target (MAV): RB-75T*2, RB-74*2, XT", "Ferry Flight: XT", "CAS (75 GUN): RB-75*2, AKAN", "CAP: RB-74*4, XT", "Countermeasures Escort: U/22A, KB", "Strike: BK90 (MJ1)*2, RB-74*2, XT", "CAS: AKAN, RB-05A", "CAP (6 AAM): RB-74*4, RB-24J*2, XT", "Rocket Half Load HE: ARAK HE*2, RB-74*2, XT", "CAP / Intecept: RB-05A*2, RB-74*2, XT", "Bombs Low-drag: SB71LD*16, RB-24J*2, XT", "SEAD: RB-75T*2, U22/A, KB, XT", "Anti-Ship (Modern): RB-15F*2, RB-74*2, XT", "New Payload", "CAP (AJ37): RB-24J*2", "ECM Escort Anti-ship: RB-04E, KB, RB-74*2, XT", "Bombs High-drag: SB71HD*16, XT, RB-24J", "Anti-ship (Light Mav): RB-75*4, XT", "Rocket Full Load HE: ARAK HE*4, RB-24J, XT", "Illumination: LYSB*8, XT", "Anti-ship (RB05): RB-05A*2, RB-74*2, XT", "CAP (Gun): AKAN*2, RB-74*2, XT", "Hard Target: RB-05A*2, RB-74*2, XT", "RB-05*2, XT", "CAS: ARAK M70 HE*4, XT", "Runway Strike: SB71HD*16, RB-24J, XT"], "AV8BNA": ["H-L-H 2", "AFAC 1", "AS 1", "H-M-H 3", "Interdiction 1 (H-H-H-H)", "Interdiction 2 (H-H-H-H)", "AFAC 3", "Interdiction 3 (H-L-L-H)", "H-M-H 1", "H-L-H 1", "PGM 2 (H-H-H-H)", "L-L-L 1", "AFAC 2", "H-M-H 2", "Area Suppression", "Rockets 1", "AS 2", "Helo Escort 1", "PGM 1 (H-H-H-H)", "Helo Escort 2", "PGM 3 (H-H-H-H)", "Anti Armor", "RA 1 (H-M-M-H)", "Stand Off 1", "Stand Off 2", "Stand Off 3", "Iron Hand 1", "Iron Hand 2"], "C-101CC": ["2*AIM-9P, DEFA 553 CANNON (I)", "2*AIM-9M, DEFA 553 CANNON (I)", "2*AIM-9P, DEFA 533 CANNON (II)", "2*AIM-9P, AN-M3 CANNON (IV)", "2*R.550 MAGIC, DEFA 553 CANNON", "2*AIM-9M, AN-M3 CANNON (III)", "2*AIM-9P, DEFA 553 CANNON", "2*R.550 MAGIC, DEFA 553 CANNON (III)", "2*AIM-9P, 2*BELOUGA, DEFA 553 CANNON", "2*AIM9-P, 2*SEA EAGLE, DEFA-553 CANNON", "2*AIM-9M 2*SEA EAGLE, AN-M3 CANNON", "2*AIM-9M, AN-M3 CANNON", "2*BELOUGA,2*BDU-33, DEFA-553 CANNON", "2* SEA EAGLE, DEFA-553 CANNON", "2*AIM-9P, 2*BR-250,2*MK-82, DEFA 553 CANNON", "2*R.550 MAGIC, 2*SEA EAGLE , DEFA-553 CANNON", "2*R.550 MAGIC, DEFA 553 CANNON (IV)", "2*BELOUGA, 2*BR-500, DEFA 553 CANNON", "2*AIM-9M, DEFA 553 CANNON (IV)", "2*R.550 MAGIC, AN-M3 CANNON (II)", "2*R550 Magic, DEFA 553 CANNON (I)", "2*AIM-9M ,2*BELOUGA,2*BIN-200, AN-M3 CANNON", "2*AIM-9M, 2*LAU 68, 2*MK-82, DEFA 553 CANNON", "2*AIM-9P, AN-M3 CANNON (III)", "2*AIM-9M, DEFA 533 CANNON (II)", "2*R.550 MAGIC, 2*BR-250, 2*BDU-33, DEFA 553 CANNON"], "C-101EB": ["Smoke System: White Smoke", "Smoke System: White Smoke+Red Colorant", "Smoke System: White Smoke+Yellow Colorant"], "H-6J": ["YJ-12 x 2", "YJ-12 x 4", "YJ-83K x 6", "250-2 HD Bomb x 12 in Bay", "250-2 HD Bomb x 24 in Bay", "250-3 LD Bomb x 36", "KD-63 x 4", "KD-20 x 6", "KD-20 x 4", "KD-63 x 2, KD-20 x 4", "KD-63 x 2, KD-20 x 2"], "J-11A": ["FAB-100x36,R-73x2,ECM", "FAB-250x8,R-73x2,ECM", "FAB-500x8,R-73x2,ECM", "S-8KOMx80,FAB-250x4,R-73x2,ECM", "S-13x20,FAB-250x4,R-73x2,ECM", "S-25x4,FAB-500x4,R-73x2,ECM", "R-27ERx4,R-27ETx2,R-73x2,ECM", "R-77x6,R-73x2,ECM", "R-27ERx6,R-73x2,ECM", "R-77x4,R-27ETx2,R-73x2,ECM", "R-77x4,R-27ERx2,R-73x2,ECM", "BetAB-500ShPx6,R-73x2,ECM", "R-73x4,ECM", "R-77x2,R-27ETx2,R-73x2,ECM", "R-77x6,R-73x4", "R-77x2,R-27ETx2,R-27ERx2,R-73x2,ECM", "R-27ETx2,R-27ERx4,R-73x2,ECM", "S-8TsMx80,FAB-250x4,R-73x2,ECM", "S-8OFP2x80,FAB-250x4,R-73x2,ECM", "FAB-250x18,R-73x2,ECM", "2*S8-KOMx2, R-73x2, ECM", "2*S8-OFP2x2, R-73x2, ECM", "FAB-250x4, 2*FAB-500x2, R-73x2", "FAB-250x4, 2*FAB-250x2, R-73x2", "RBK-250 HEAT/APx2, RBK-250 HE/Fragx2, R-73x2"], "JF-17": ["PL-5Ex2, C802AKx2, 800L Tank", "PL-5Ex2, C-701 IRx2, 1100L Tank, 800L Tank", "PL-5Ex2, SD-10x2, 1100L Tankx2, WMD7", "PL-5Ex2, LD-10x2, 1100L Tankx2, WMD7", "PL-5Ex2, 800L Tank, WMD7", "PL-5Ex2, GBU-10x2, WMD7", "PL-5Ex2, 2*GBU-12x2, 800L Tank, WMD7", "PL-5Ex2, 2*Mk-82x2, Mk-83x2, 800L Tank", "PL-5Ex2, 1100L Tankx2, 800L Tank", "PL-5Ex2, WMD7, CM802AKGx2, 800L Tank, DL", "PL-5Ex2, C-701 CCDx2, 1100L Tank, 800L Tank", "PL-5Ex2, GBU-12x2, 1100L Tank, WMD7", "PL-5Ex2, 2*GBU-12x2, GBU-16x2, WMD7", "PL-5Ex2, 1100L Tankx2, WMD7", "PL-5Ex2, WMD7, 800L Tankx2, SPJ, 2*LD-10", "PL-5Ex2, LS-6x2, 1100L Tankx2, WMD7", "PL-5Ex2, C-701 IRx2, 1100L Tankx2, WMD7", "PL-5Ex2, GBU-12x2, 1100L Tankx2, WMD7", "PL-5Ex2, 2*LD-10x2, 1100L Tankx2, SPJ", "PL-5Ex2, LD-10x2, 1100L Tankx2, SPJ", "PL-5Ex2, 2*LD-10x2, LS-6x2, SPJ", "PL-5Ex2, 2*LD-10x2, GB-6-HEx2, SPJ", "PL-5Ex2, C-701 IRx2, 800L Tankx2, WMD7", "PL-5Ex2, C-701 CCDx2, 1100L Tankx2, WMD7", "PL-5Ex2, C-701 CCDx2, 800L Tankx2, WMD7", "PL-5Ex2, 2*GBU-12x2, 1100L Tank, WMD7", "PL-5Ex2, C-701 IRx2, 1100L Tank, WMD7", "PL-5Ex2, C-701 IRx2, 800L Tank, WMD7", "PL-5Ex2, C-701 CCDx2, 1100L Tank, WMD7", "PL-5Ex2, C-701 CCDx2, 800L Tank, WMD7", "PL-5Ex2, C-701 IRx2, LS-6x2, 800L Tank", "PL-5Ex2, C-701 IR/CCD, GB-6-HEx2, 800L Tank", "PL-5Ex2, C-701 IR/CCD, GB-6-SFWx2, 800L Tank", "PL-5Ex2, WMD7, GB-6-SFWx2, 800L Tank, BRM1", "PL-5Ex2, WMD7, GB-6-SFWx2, 800L Tank, GBU-12", "PL-5Ex2, 2*Mk-82SEx2, Mk-83x3", "PL-5Ex2, Mk-84x3", "PL-5Ex2, 2*Mk5x2, 800L Tank", "PL-5Ex2, Unguided 90mmx2, 800L Tank", "PL-5Ex2, 2*Mk5x2, Mk-83x3", "PL-5Ex2, BRM1x2, 1100L Tank, WMD7", "PL-5Ex2, 2x1100L Tank", "PL-5Ex2, SD-10x2, 2x1100L Tank", "PL-5Ex2, 2*SD-10x2, 2x1100L Tank", "PL-5Ex2, 800L Tank", "PL-5Ex2, SD-10x2, 800L Tank", "PL-5Ex2, 2*SD-10x2, 800L Tank", "PL-5Ex2, SD-10x2, SPJ", "PL-5Ex2, SPJ", "PL-5Ex2, 2*SD-10x2, SPJ", "PL-5Ex2", "PL-5Ex2, SD-10x2", "PL-5Ex2, 2*SD-10", "PL-5Ex2, SD-10x2, SPJ, 1100L Tankx2", "PL-5Ex2, 2*SD-10x2, 1100L Tankx2, 800L Tank", "PL-5Ex2, SD-10x2, 1100L Tankx2, 800L Tank", "PL-5Ex2, GBU-16x2, BRM1x2, WMD7", "PL-5Ex2, WMD7", "PL-5Ex2, 2*LD-10, GB-6x2, 2*SD-10, SPJ", "PL-5Ex2, C-701 CCDx2, SPJ", "PL-5Ex2, 2*LD-10, CM802AKGx2, 2*SD-10, DL", "PL-5Ex2, 2*MK-82x2, MK-83x2, MK-84", "PL-5Ex2, LS-6x2, GB-6x2, 800L Tank", "PL-5Ex2, 2*GBU-12x2, LS-6x2, WMD7", "PL-5Ex2, 2*GBU-12x2, GB-6x2, WMD7", "PL-5Ex2, 2*Type-200Ax2", "PL-5Ex2, Type-200Ax2", "PL-5Ex2, 2*LS6-250x2, 800L Tankx2, WMD7", "PL-5Ex2, 2*LS6-250x2, 800L Tank, 1100L Tankx2", "PL-5Ex2, 2*LS6-100x2, 1100L Tankx2, WMD7", "PL-5Ex2, 2*LS6-100x2, 800L Tankx2, WMD7"], "WingLoong-I": ["AKD-10 x 2"], "Christen Eagle II": ["Smoke - white"], "F-16C_50": ["AIM-120B*2, AIM-9M*4, FUEL*3", "AIM-120B*4, AIM-9M*2, FUEL*3", "AIM-120B*6, FUEL*3", "AIM-120C*2, AIM-9X*4, FUEL*2", "AIM-120C*4, AIM-9X*2, FUEL*3", "AIM-120C*4, AIM-9X*2, FUEL*3, TGP", "AIM-120C*4, AIM-9X*2, FUEL*2", "AIM-120C*6, FUEL*3", "AIM-120C*4, AIM-9X*2, FUEL*2, ECM", "AIM-120C*4, AIM-9X*2, FUEL*2, ECM, TGP", "AIM-120C*6, FUEL*2, ECM", "AIM-120C*6, FUEL*2, ECM, TGP", "AIM-120C*6, FUEL*2", "AIM-120C*6, FUEL*3, TGP", "AIM-120C*2, AIM-9X*2, AGM-65D*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65H*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65H*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65D*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, CBU-97*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-82*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, CBU-87*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-82HD*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, CBU-103*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, CBU-105*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-82*6, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-82HD*6, FUEL*2, ECM, TGP", "AIM-120*2, AIM-9X*2, MK-82SE*4, FUEL*2, ECM, TGP", "AIM-120*2, AIM-9X*2, MK-82SE*6, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-84*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, MK-82P*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-12*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-12*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-10*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-24*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-31-1B*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-31-3B*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-38*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, GBU-38*4, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65K*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65G*2, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-65G, AGM-65K, FUEL*2, ECM, TGP", "AIM-120C*2, AIM-9X*2, AGM-88C*2, FUEL*3, TGP, HTS", "AIM-120C*2, AIM-9X*2, AGM-88C*2, FUEL*2, ECM, TGP, HTS", "AIM-120C*2, AIM-9X*2, AGM-88C*4, ECM, TGP, HTS", "AIM-120C*4, AGM-88C*2, FUEL*3, TGP, HTS", "AIM-120C*4, AGM-88C*2, FUEL*2, ECM, TGP, HTS", "AIM-120C*4, AGM-88C*4, ECM, TGP, HTS", "AIM-120C*2, AIM-9X*2, MK-61*2, FUEL*2, ECM, TGP"], "F-5E-3": ["Mk-82LD*4,AIM-9P*2,Fuel 275", "AIM-9P*2, Fuel 275*3", "AIM-9P5*2, Fuel 275*3", "AIM-9P*2, Fuel 150*3", "AIM-9P5*2, Fuel 150*3", "Mk-82SE*4,AIM-9P*2,Fuel 275", "CBU-52B*4,AIM-9P*2,Fuel 275", "LAU-3 HE*4,AIM-9P*2,Fuel 275", "LAU-3 HEAT*4,AIM-9P*2,Fuel 275", "LAU-68 HE*4,AIM-9P*2,Fuel 275", "LAU-68 HEAT*4,AIM-9P*2,Fuel 275", "M-117*4,AIM-9P*2,Fuel 275", "GBU-12*4,AIM-9P*2,Fuel 275", "CBU-52B*5,AIM-9*2", "Mk-82LD*5,AIM-9*2", "Mk-82SE*5,AIM-9*2", "Mk-82LD*7,AIM-9P*2, Fuel 275*2", "Mk-82SE*7,AIM-9P*2, Fuel 275*2", "LAU-3 HE*2,Mk-82LD,AIM-9P*2,Fuel 275*2", "LAU-68 HE*2,Mk-82LD,AIM-9P*2,Fuel 275*2", "M-117*5,AIM-9*2", "AIM-9P*2, Fuel 275", "AIM-9P*2, Fuel 150", "AIM-9P5*2, Fuel 275", "AIM-9P5*2, Fuel 150", "AIM-9B*2, Fuel 275", "AIM-9B*2, Fuel 150", "AIM-9B*2, Fuel 275*3", "AIM-9B*2, Fuel 150*3", "AN/ASQ-T50, AIM-9P, Fuel 150", "AIM-9B*2", "AIM-9P*2", "AIM-9P5*2", "Antiship Mk82"], "F-5E": ["Mk-82LD*4,AIM-9P*2,Fuel 275", "AIM-9P*2, Fuel 275*3", "AIM-9P5*2, Fuel 275*3", "AIM-9P*2, Fuel 150*3", "AIM-9P5*2, Fuel 150*3", "Mk-82SE*4,AIM-9P*2,Fuel 275", "CBU-52B*4,AIM-9P*2,Fuel 275", "LAU-3 HE*4,AIM-9P*2,Fuel 275", "LAU-3 HEAT*4,AIM-9P*2,Fuel 275", "LAU-68 HE*4,AIM-9P*2,Fuel 275", "LAU-68 HEAT*4,AIM-9P*2,Fuel 275", "M-117*4,AIM-9P*2,Fuel 275", "GBU-12*4,AIM-9P*2,Fuel 275", "CBU-52B*5,AIM-9*2", "Mk-82LD*5,AIM-9*2", "Mk-82SE*5,AIM-9*2", "Mk-82LD*7,AIM-9P*2, Fuel 275*2", "Mk-82SE*7,AIM-9P*2, Fuel 275*2", "LAU-3 HE*2,Mk-82LD,AIM-9P*2,Fuel 275*2", "LAU-68 HE*2,Mk-82LD,AIM-9P*2,Fuel 275*2", "M-117*5,AIM-9*2", "AIM-9P*2, Fuel 275", "AIM-9P*2, Fuel 150", "AIM-9P5*2, Fuel 275", "AIM-9P5*2, Fuel 150", "AIM-9B*2, Fuel 275", "AIM-9B*2, Fuel 150", "AIM-9B*2, Fuel 275*3", "AIM-9B*2, Fuel 150*3", "AN/ASQ-T50, AIM-9P, Fuel 150", "AIM-9B*2", "AIM-9P*2", "AIM-9P5*2"], "F-86F Sabre": ["120gal Fuel*2", "200gal Fuel*2", "120gal Fuel*2, 200gal Fuel*2", "GAR-8*2", "120gal Fuel*2, GAR-8*2", "HVAR*16", "200gal Fuel*2, HVARx2*4", "AN-M64*2", "200gal Fuel*2, AN-M64*2", "M117*2"], "F-14A-135-GR": ["XT*2", "AIM-54A-MK47*6, AIM-9L*2, XT*2", "AIM-7F*6, AIM-9L*2, XT*2", "AIM-54A-MK47*4, AIM-7F*2, AIM-9L*2, XT*2", "AIM-54A-MK47*2, AIM-7F*1, AIM-9L*4, XT*2", "AIM-54A-MK47*4, AIM-9L*4, XT*2", "AIM-54A-MK47*4, AIM-9M*4, XT*2", "AIM-54A-MK60*4, AIM-9M*4, XT*2", "AIM-7F*4, AIM-9L*4, XT*2", "AIM-7F*4, AIM-9L*4, XT*2", "BDU-33*14", "BDU-33*12", "GBU-10*2", "GBU-12*4", "GBU-16*4", "GBU-24*2", "Mk-84*4", "Mk-83*4", "Mk-82*4", "Mk-82*14", "Mk-81*14", "Mk-20*4", "Mk-82AIR*4", "Zuni*12", "Zuni*28", "LUU-2*24", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, XT*2, Mk-82*2", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, XT*2, Mk-82*1", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, XT*2, Mk-20*2", "AIM-7M*1, AIM-9M*2, XT*2, GBU-12*2, LANTIRN", "AIM-7M*1, AIM-9M*2, XT*2, GBU-24*1, LANTIRN", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-82*2, LANTIRN", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-20*2, LANTIRN"], "F-14A-95-GR": ["AIM-54A-MK47*6, AIM-9L*2", "AIM-54A-MK47*6, AIM-9L*2", "AIM-54A-MK60*6, AIM-9L*2", "AIM-7F*6, AIM-9L*2", "AIM-7F*6, AIM-9L*2", "AIM-54A-MK47*4, AIM-7F*2, AIM-9L*2", "AIM-54A-MK47*4, AIM-7F*2, AIM-9L*2", "AIM-54A-MK60*4, AIM-7F*2, AIM-9L*2", "AIM-54A-MK47*2, AIM-7F*1, AIM-9L*4", "AIM-54A-MK47*2, AIM-7F*1, AIM-9L*4", "AIM-54A-MK60*2, AIM-7F*1, AIM-9L*4", "AIM-54A-MK47*4, AIM-9L*4", "AIM-54A-MK47*4, AIM-9L*4", "AIM-54A-MK60*4, AIM-9L*4", "AIM-7F*4, AIM-9L*4", "AIM-7F*4, AIM-9L*4", "AIM-54A-MK47*2, AIM-7F*3, AIM-9L*2", "AIM-54A-MK47*2, AIM-7F*3, AIM-9L*2", "AIM-54A-MK60*2, AIM-7F*3, AIM-9L*2", "BDU-33*14", "BDU-33*12", "GBU-10*2", "GBU-12*4", "GBU-16*4", "GBU-24*2", "Mk-84*4", "Mk-83*4", "Mk-82*4", "Mk-82*14", "Mk-81*14", "Mk-20*4", "Mk-82AIR*4", "Zuni*12", "Zuni*28", "LUU-2*24", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, Mk-82*2", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, Mk-82*1", "AIM-54A-MK60*1, AIM-7F*1, AIM-9L*2, Mk-20*2"], "F-14B": ["XT*2", "AIM-54A-MK47*6, AIM-9M*2, XT*2", "AIM-54A-MK47*6, AIM-9L*2, XT*2", "AIM-54A-MK60*6, AIM-9M*2, XT*2", "AIM-54C-MK47*6, AIM-9M*2, XT*2", "AIM-7M*6, AIM-9M*2, XT*2", "AIM-7M*6, AIM-9L*2, XT*2", "AIM-54A-MK47*4, AIM-7M*2, AIM-9M*2, XT*2", "AIM-54A-MK47*4, AIM-7M*2, AIM-9L*2, XT*2", "AIM-54A-MK60*4, AIM-7M*2, AIM-9M*2, XT*2", "AIM-54C-MK47*4, AIM-7M*2, AIM-9M*2, XT*2", "AIM-54A-MK47*2, AIM-7M*1, AIM-9M*2, AIM-9L*2, XT*2", "AIM-54A-MK47*2, AIM-7M*1, AIM-9M*4, XT*2", "AIM-54A-MK60*2, AIM-7M*1, AIM-9M*4, XT*2", "AIM-54C-MK47*2, AIM-7M*1, AIM-9M*4, XT*2", "AIM-54A-MK47*4, AIM-9M*2, AIM-9L*2, XT*2", "AIM-54A-MK47*4, AIM-9M*4, XT*2", "AIM-54A-MK60*4, AIM-9M*4, XT*2", "AIM-54C-MK47*4, AIM-9M*4, XT*2", "AIM-7M*4, AIM-9M*2, AIM-9L*2, XT*2", "AIM-7M*4, AIM-9L*4, XT*2", "AIM-54A-MK47*2, AIM-7M*3, AIM-9M*2, XT*2", "AIM-54A-MK47*2, AIM-7M*3, AIM-9M*2, XT*2", "AIM-54A-MK60*2, AIM-7M*3, AIM-9M*2, XT*2", "AIM-54C-MK47*2, AIM-7M*3, AIM-9M*2, XT*2", "BDU-33*14", "BDU-33*12", "GBU-10*2", "GBU-12*4", "GBU-16*4", "GBU-24*2", "Mk-84*4", "Mk-83*4", "Mk-82*4", "Mk-82*14", "Mk-81*14", "Mk-20*4", "Mk-82AIR*4", "Zuni*12", "Zuni*28", "LUU-2*24", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-82*2", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-82*1", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-20*2", "AIM-7M*1, AIM-9M*2, XT*2, GBU-12*2, LANTIRN", "AIM-7M*1, AIM-9M*2, XT*2, GBU-24*1, LANTIRN", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-82*2, LANTIRN", "AIM-54A-MK60*1, AIM-7M*1, AIM-9M*2, XT*2, Mk-20*2, LANTIRN"], "FA-18C_hornet": ["AIM-9M*6, AIM-7M*2, FUEL*3", "AIM-9M*6, AIM-7M*2, FUEL*2", "AIM-9M*2, MK-84*2, FUEL*2", "AIM-9M*2, MK-83*4, FUEL*2", "Carrier Landing", "AIM-9M*2, AIM-7M*4, FUEL*3", "AIM-9M*2, CBU-99*4, FUEL*2", "AIM-9M*2, MK-82SE*4, FUEL*2", "AIM-9M*2, MK-20*4, FUEL*2", "AIM-9M*2, MK-82*4, FUEL*2", "AIM-9M*2, AIM-7M*2, FUEL*2", "AIM-9M*2, MK-83*2, FUEL*2", "AIM-9M*2, ZUNI*4, FUEL*2", "AIM-9M*2, LAU-61*4, FUEL*2", "AIM-9M*2, LAU-68*4, FUEL*2", "AIM-9M*2, AIM-7M*2, FUEL*1", "AIM-9X*2, AIM-120C-5*1, GBU-31*4, ATFLIR, FUEL", "AIM-9X*2, AIM-120C-5*6, FUEL*3", "AIM-9X*2, AIM-120C-5*1, AGM-65D*4, ATFLIR, FUEL", "AIM-9X*2, AIM-120C-5*2, AGM-88C*2, FUEL", "AIM-9X*2, AIM-120C-5*1, GBU-38*4, GBU-12*4, ATFLIR, FUEL", "AIM-9X*2, AIM-120C-5*1, AGM-84E*2, DATALINK, ATFLIR, FUEL*2", "AIM-9M*2, AIM-120C-5*1, AGM-84D*4, ATFLIR, FUEL", "AIM-9M*2, ATFLIR, FUEL", "AIM-9M*2, ATFLIR, FUEL*2"], "I-16": ["6xRS-82", "2xFAB-100", "6xRS-82, 2xFAB-100", "6xRS-82, 2xDropTank-93L", "2xDropTank-93L"], "Ka-50": ["4xFAB-500", "2xFuel tank, 2xKMGU AP", "80xS-8 TsM", "2xFuel tank, 40xS-8", "80xS-8", "4xKMGU AT", "2xKMGU AP", "2xFuel tank, 12x9A4172", "2xFuel tank, 2xUPK-23", "12x9A4172, 40xS-8", "12x9A4172, 2xKMGU AT", "4xKMGU AP", "2xFAB-250, 2xFuel tank", "12x9A4172, 2xFAB-500", "20xS-13", "40xS-8", "12x9A4172, 2xUPK-23", "2xKMGU AT", "6x9A4172", "2xFuel tank, 2xKMGU AT", "4xFuel tank", "2xKMGU AP, 12x9A4172", "12x9A4172", "2xFuel tank", "40xS-8 TsM, 12x9A4172", "2xFAB-500", "40xS-8 TsM, 2xFuel tank", "4xUPK-23", "10xS-13", "40xS-8 TsM", "12x9A4172, 10xS-13", "2xFuel tank, 2xFAB-500", "2xFAB-250", "4xFAB-250", "2xFuel tank, 10xS-13", "2xFAB-250, 12x9A4172", "2xUPK-23"], "Ka-50_3": ["4xIgla", "2xKh-25ML, 10xS-13, 4xIgla", "12x9A4172, 40xS-8KOM, 4xIgla", "12x9A4172, 40xS-8OFP, 4xIgla", "12x9A4172, 40xS-13, 4xIgla", "80xS-8KOM, 4xIgla", "80xS-8OFP, 4xIgla", "20xS-20, 4xIgla", "4xUPK-23, 4xIgla", "10xS-13, 2xFAB-500, 4xIgla", "10xS-13, 2xFAB-250, 4xIgla", "80xS-8OM, 4xIgla", "80xS-8TsM, 4xIgla", "40xS-8OFP, 2xFuel, 4xIgla", "12x9A4172, 2xFuel, 4xIgla"], "L-39C": ["SAB-100x2", "R-3Sx2", "Smokes", "Central Smoke", "FAB-100x2", "S-5KOx32"], "L-39ZA": ["S-5KOx32", "S-5KOx64", "S-5KOx32, PTB-150x2", "S-5KOx32, PTB-350x2", "S-5KOx32, FAB-100x2", "OFAB-100 Jupiter x4, FAB-100x2", "FAB-100x2", "FAB-100x4", "OFAB-100 Jupiter x8", "FAB-100x2, PTB-150x2", "FAB-100x2, PTB-350x2", "PK-3x4", "PK-3x2, PTB-150x2", "R-60Mx2", "SAB-100x4", "R-3Sx2", "R-3Sx2, PK-3x2", "R-60Mx2, PK-3x2"], "M-2000C": ["Fox", "Fox / Magic (QRA)", "Alpha / S530D", "Fox / S530D / Magic", "Fox / S530D / Magic / Eclair", "Bravo", "Bravo / Magic", "Kilo", "Kilo / Magic", "Bravo / 4xMk-82 / Magic", "Bravo / GBU-12 / Magic", "Bravo / 2xGBU-12 / Magic", "Bravo / GBU-16 / Magic", "Bravo / GBU-24 / Magic", "Bravo / BAP-100 / Magic", "Bravo / 4xSnakeEye / Magic", "Fox / 4xMk-82 / Magic", "Kilo / 4xMk-82 / Magic"], "MB-339A": ["A - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*Mk.83 + 2*Mk.81 ", "A - 2*320L TipTanks [Clean]", "Recon", "Training", "AA - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*LAU-10(Zuni Rockets) [ARMADA]", "AM - 2*320L TipTanks + 2*AN/M3 GunPods + 2*330L Tanks + 2*LAU-3 (Hydra rockets)", "A - 2*500L TipTanks + 2*330L Tanks + Luggage Container [Ferry Long Range]", "A - 2*500L TipTanks + 4*Mk.82HD + 2*LR-25 (API Rockets)", "A - 2*320L TipTanks + 2*330L Tanks [Ferry Medium Range]", "A - 2*500L TipTanks + 2*AN/M3 GunPods + 2*Matra 155 + 2* Belouga", "Runway Interdiction", "A - 2*500L TipTanks + 2*DEFA-553 GunPods + 2*Mk.82LD + 2*LR-25 (API Rockets)", "A - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*Mk.82LD Bombs + 2*LR-25(API Rockets)", "A - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*Mk.82LD + 2*LR-25 (HEI Rockets)", "A - 2*320L TipTanks + 6*Mk.82LD", "Runway Interdiction (36*BAP-100)", "Anti - Light Armoured Vehicle (36*BAT-120 ABL)", "AP - 2*320L TipTanks + 2*DEFA-553 GunPods + 2*330L Tanks + 2*Matra 155 (SNEB rockets)"], "MB-339APAN": ["PAN - Smoke White", "PAN - Smoke White and Color Red", "PAN - Fuel External [RED SMOKE]", "PAN - Full Payload [RED SMOKE] + 2*320L TipTanks", "PAN - Full Payload [GREEN SMOKE] + 2*320L TipTanks", "PAN - Fuel External [GREEN SMOKE]", "PAN - Full Payload [RED SMOKE] + 2*500L TipTanks", "PAN - Smoke White and Color Green", "PAN - Full Payload [GREEN SMOKE] + 2*500L TipTanks"], "Mi-24P": ["2xB8V20 (S-8KOM)+8xATGM 9M114", "2xB8V20 ( S-8KOM)+4xATGM 9M114", "4xB8V20 (S-8KOM)+4xATGM 9M114", "2xB8V20 (S-8KOM)+2xBombs-250+4xATGM 9M114", "2xB8V20 (S-8OFP2)+4xATGM 9M114", "4xUB-32A (S-5KO)+4xATGM 9M114", "4xGUV-1 AP30+4xATGM 9M114", "2xGUV-1 AP30+4xATGM 9M114", "2xGUV-1 (GUN 12.7+2x7.62) +4xATGM 9M114", "2xKMGU (96 AO 2.5RT)+8xATGM 9M114", "2xB-13L+4xATGM 9M114", "2xS-24B+4xATGM 9M114", "4xS-24B+4xATGM 9M114", "2xBombs-500+4xATGM 9M114", "4xBombs-250+4ATGM 9M114", "2xRBK-500 (PTAB-1M)+4xATGM 9M114", "2xRBK-500U (OAB 2.5RT)+4xATGM 9M114", "4xRBK-250 (42 PTAB 2.5M) +4ATGM 9M114", "4xRBK-250-275 (150 AO-1SCh)+4ATGM 9M114", "4xPTB-450 Fuel tank"], "MiG-19P": ["PTB-760 x 2", "K-13A x 2, PTB-760 x 2", "K-13A x 2", "K-13A x 2, ORO-57K x 2, PTB-760 x 2", "ORO-57K x 2, PTB-760 x 2", "ORO-57K x 4", "ORO-57K x 2", "FAB-100M x 2, ORO-57K x 2", "FAB-250 x 2, ORO-57K x 2", "FAB-100M x 2", "FAB-250 x 2"], "MiG-21Bis": ["Patrol, long range", "Patrol, medium range", "Patrol, short range", "Hard targets, BOMBS", "Unknown or mixed targets, BOMBS + ROCKETS", "Soft targets, CLUSTERS + ROCKETS", "Soft targets, CLUSTERS", "Soft targets, scattered", "Few big targets, GROM + BOMBS", "Very hard target, PENETRATION", "Aerial attack, hard targets, CLUSTERS", "Hard targets, ROCKETS, PENETRATION", "Soft targets, ROCKETS, BLAST-FRAGMENTS", "Long range, MIX", "Long range, RADAR GUIDED MISSILES", "Long range, INFRA RED MISSILES", "Escort", "Escort, JAMMER", "Night, ILLUMINATOR", "Long range, JAMMER", "Soft targets, UPK + ROCKETS", "Soft targets, UPK + CLUSTERS", "Patrol, JAMMER", "NUCLEAR A", "NUCLEAR B", "Short range", "AEROBATIC"], "Mirage-F1B": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1BD": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1BE": ["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*AIM9-JULI, R530IR", "2*R550 Magic I, R530EM", "2*AIM9-JULI, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*AIM9-J, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 LD", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9JULI, 8*SAMP 250 HD", "2*AIM-9JULI, 8*SAMP 400 LD", "2*AIM-9JULI, 8*BLU107 Durandal"], "Mirage-F1BQ": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1C-200": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1C": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CE": ["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*AIM9-JULI, R530IR", "2*R550 Magic I, R530EM", "2*AIM9-JULI, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*AIM9-J, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 LD", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9JULI, 8*SAMP 250 HD", "2*AIM-9JULI, 8*SAMP 400 LD", "2*AIM-9JULI, 8*BLU107 Durandal"], "Mirage-F1CG": ["2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank", "4*AIM-9 JULI, 2*R530IR, 1*Fuel Tank", "2*AIM-9 JULI, 2*R530EM, 1*Fuel Tank", "2*AIM-9 JULI, R530IR", "2*AIM-9 JULI, 1*R530IR, 2*Fuel Tank", "2*AIM-9 JULI, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*AIM-9 JULI, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*AIM-9 JULI, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*AIM-9 JULI, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9 JULI, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9 JULI, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CH": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CJ": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CK": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CR": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CT": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1CZ": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1DDA": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1ED": ["2*R550 Magic II, 2*S530, 1*Fuel Tank", "2*R550 Magic II", "2*R550 Magic II, 2*Fuel Tank", "2*R550 Magic II, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic II, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic II, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic II, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic II, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic II, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1EDA": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1EE": ["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank", "2*AIM9-JULI, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*AIM9-JULI, R530EM", "2*R550 Magic I, R530IR", "2*AIM9-JULI, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*AIM9-J, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 LD", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9JULI, 8*SAMP 250 HD", "2*AIM-9JULI, 8*SAMP 400 LD", "2*AIM-9JULI, 8*BLU107 Durandal"], "Mirage-F1EH": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1EQ": ["2*R550 Magic I, 2*S530, 1*Fuel Tank", "2*R550 Magic I", "2*R550 Magic I, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*S530, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1JA": ["2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530EM, 1*Fuel Tank", "2*R550 Magic I, R530IR", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 LD", "2*R550_Magic_1, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550_Magic_1, 8*SAMP 250 HD", "2*R550_Magic_1, 8*SAMP 400 LD", "2*R550_Magic_1, 8*BLU107 Durandal"], "Mirage-F1M-CE": ["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*AIM9-JULI, R530IR", "2*R550 Magic I, R530IR", "2*AIM9-JULI, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*AIM9-J, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 LD", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9JULI, 8*SAMP 250 HD", "2*AIM-9JULI, 8*SAMP 400 LD", "2*AIM-9JULI, 8*BLU107 Durandal"], "Mirage-F1M-EE": ["2*AIM9-JULI, 2*R530IR, 1*Fuel Tank", "2*R550 Magic I, 2*R530IR, 1*Fuel Tank", "2*AIM9-JULI, R530IR", "2*R550 Magic I, R530IR", "2*AIM9-JULI, 1*R530IR, 2*Fuel Tank", "2*R550 Magic I, 1*R530IR, 2*Fuel Tank", "2*AIM9-J, 2*MATRA F4 SNEB251 (HE), 2*R530IR, 1*Fuel Tank", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 LD", "2*AIM-9J, 2*Fuel Tank, 4*SAMP 400 HD", "2*R550 Magic I, 4*MATRA F1 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB253 (Shaped Charge), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 4*MATRA F4 SNEB256 (AP), 1*Fuel Tank", "2*R550 Magic I, 2*SAMP 250 HD, 2 MATRA F1 SNEB256 (AP), 1*Fuel Tank", "2*AIM-9JULI, 8*SAMP 250 HD", "2*AIM-9JULI, 8*SAMP 400 LD", "2*AIM-9JULI, 8*BLU107 Durandal"], "SA342L": ["M621, 8xSNEB68 EAP", "M621, 8xSNEB68 EAP, IR Deflector", "M621, 8xSNEB68 EAP, IR Deflector, Sand Filter"], "SA342M": ["HOT3x4", "IR Deflector", "Hot3x4, FAS, IR Deflector", "HOT3x2", "Hot3x4, IR Deflector", "Hot3x2, IR Deflector"], "SA342Minigun": ["IR Deflector", "IR Deflector, Sand Filter"], "SA342Mistral": ["Mistral x 4", "Mistral x 4, IR Deflector", "Mistral x 4, IR Deflector, Sand Filter"], "A-20G": ["500 lb GP bomb LD*4"], "Bf-109K-4": ["Fuel Tank", "SC250", "SC500"], "FW-190A8": ["Without pylon", "SC 50 * 4", "AB 250 (w/ SD 10A)", "AB 250 (w/ SD 2)", "AB 500 (w/ SD 10A)", "SC 250 L2", "SC 250 J", "SC 500 J", "SC 500 L2", "SD 250 Stg", "SD 500 A", "Fuel Tank 300 liters", "BR 21"], "FW-190D9": ["SC500", "Fuel Tank", "R4M", "BR 21"], "MosquitoFBMkVI": ["250 lb S.A.P*2; 500 lb S.A.P.*2", "500 lb GP Mk.V*2, 500 lb GP Short tail*2", "100 gal Drop tank*2, 500 lb MC Short tail*2", "RP-3 60lb SAP No2 Mk.I*8, 250 lb A.A.P.*2", "100 gal. Drop tank*2, 250 lb MC Mk.II, RP-3 60lb F No1 Mk.I*4", "500 lb GP Short tail*4"], "P-47D-30": ["AN-M65*2", "Fuel150*2", "AN-M64*2, Fuel110", "AN-M57*3"], "P-47D-30bl1": ["AN-M57*2", "AN-M64*2, Fuel110"], "P-47D-40": ["AN-M65*2", "Fuel150*2", "AN-M57*3", "AN-M64*2, Fuel110", "M8*6, AN-M57*2, Fuel110", "HVAR*10, Fuel110"], "P-51D-30-NA": ["Fuel75*2", "HVAR*6,Fuel75*2", "HVAR*6,M64*2", "HVAR*6", "M64*2", "HVAR*10", "Smokes"], "P-51D": ["Fuel75*2", "HVAR*6,Fuel75*2", "HVAR*6,M64*2", "HVAR*6", "M64*2", "HVAR*10", "Smokes"], "A-50": [], "AH-1W": ["14xHYDRA-70 WP", "38xHYDRA-70 WP", "8xBGM-71, 14xHYDRA-70", "8xBGM-71, 14xHYDRA-70 WP", "8xBGM-71, 38xHYDRA-70 WP", "14xHYDRA-70", "38xHYDRA-70", "8xAGM-114", "28xHYDRA-70", "8xBGM-71, 38xHYDRA-70", "8xAGM-114, 38xHYDRA-70 WP", "8xBGM-71", "8xAGM-114, 14xHYDRA-70 WP", "76xHYDRA-70", "8xAGM-114, 38xHYDRA-70", "8xAGM-114, 14xHYDRA-70"], "AH-64A": ["8xAGM-114", "38xHYDRA-70 WP", "76xHYDRA-70", "8xAGM-114, 38xHYDRA-70 WP", "38xHYDRA-70", "8xAGM-114, 38xHYDRA-70", "AGM-114K*16"], "AH-64D": ["76xHYDRA-70", "38xHYDRA-70", "38xHYDRA-70 WP", "8xAGM-114", "8xAGM-114, 38xHYDRA-70 WP", "8xAGM-114, 38xHYDRA-70", "AGM-114K*16"], "An-26B": [], "An-30M": [], "B-1B": ["Mk-82*84", "AGM-154*12", "GBU-38*48", "CBU-87*30", "CBU-97*30", "GBU-38*16, CBU-97*20", "Mk-84*24", "GBU-31*24", "GBU-31(V)3/B*24", "GBU-31*8, GBU-38*32"], "B-52H": ["Mk-84*18", "Mk 82*51", "Mk20*18", "AGM-86C*20", "AGM-84A*8"], "C-130": [], "C-17A": [], "CH-47D": [], "CH-53E": [], "E-2C": [], "E-3A": [], "F-117A": ["GBU-12*2", "GBU-10*2", "GBU-27*2"], "F-14A": ["AIM-9*2", "AIM-54C*6,AIM-9*2", "AIM-54C*4,AIM-9*2,AIM-7*2"], "F-15C": ["AIM-120B*4, AIM-7M*2, AIM-9M*2, Fuel*3", "AIM-9*2,AIM-120*6,Fuel", "AIM-9*4,AIM-120*4,Fuel*3", "AIM-9*4,AIM-120*4,Fuel", "AIM-9*2,AIM-120*2,AIM-7*4,Fuel*3", "AIM-9*2,AIM-120*6,Fuel*3", "AIM-9*4,AIM-7*4,Fuel", "AIM-120*8,Fuel", "AIM-9*4,AIM-7*4,Fuel*3", "AIM-120*8,Fuel*3", "AIM-9*2,AIM-120*2,AIM-7*4,Fuel"], "F-15E": ["AIM-120B*2,AIM-9M*2,FUEL*3,CBU-87*6,Mk-82AIR*6", "AIM-120B*2,AIM-9M*2,FUEL*3,CBU-97*12", "AIM-120B*2,AIM-9M*2,FUEL*3,Mk-82*12", "AIM-120B*4,AIM-120C*4,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-65H,AGM-65D", "AIM-120B*2,AIM-9M*2,FUEL,CBU-103*2,GBU-12,GBU-38,AGM-154C*2", "AIM-120B*4,AIM-9M*4,FUEL*3", "AIM-120B*2,AIM-9M*2,AIM-120C*2,AIM-7M*2,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-154C*2", "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-154C*2", "AIM-120B*2,AIM-9M*2,AIM-7M*4,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL*3,Mk-84*8", "AIM-120B*2,AIM-9M*2,FUEL,AGM-154C*2", "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65D*2", "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65K*2", "AIM-120C*2,AIM-9M*4,AIM-7M*2,FUEL*3", "AIM-9M*4,AIM-7M*4,FUEL*3", "AIM-120C*2,AIM-9M*2,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL,Mk-84*8,AGM-154C*2", "AIM-120B*2,AIM-9M*2,FUEL*3,Mk-82AIR*12", "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*2,GBU-38*2,AGM-154C*2", "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*2,GBU-38*2,AGM-65H,AGM-65D", "AIM-120B*2,AIM-120C*4,AIM-9M*2,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL*3,Mk-82*6,Mk-82AIR*6", "AIM-120B*2,AIM-9M*2,FUEL,CBU-103*2,GBU-12,GBU-38,AGM-65H*2", "AIM-120B*2,AIM-9M*2,FUEL,AGM-65D*2", "AIM-120B*2,AIM-9M*2,FUEL*2,SUU-25*2,GBU-12,GBU-38", "AIM-120B*6,AIM-9M*2,FUEL*3", "AIM-120B*2,AIM-9M*2,FUEL,Mk-82AIR*12,AGM-154C*2", "AIM-120C*6,AIM-9M*2,FUEL*3", "GBU-31(V)3/B*5, AIM-120C*2, AIM-9M*2"], "F-16A MLU": ["Fuel*3", "AGM-88*2, AGM-65D*2, AIM-120B*2, ECM", "Mk-82*6,AIM-120*2,ECM,Fuel*2", "Mk-82*2,AIM-120*2,AIM-9*2,ECM,Fuel*2", "AGM88*2,AGM-65D*6,AIM-120*2,AIM-9*2,ECM", "AIM-120*2,GBU-10*2,ECM,Lantirn ,Fuel*2", "AIM-9*4,ECM", "Mk20*4,AIM-9*2,ECM,Fuel*2", "AIM-120*6,ECM", "AIM-120*4,AIM-9*2,ECM", "AGM88, AGM-65D, AIM-120*2,Fuel*2,ECM", "AGM-65D*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "AIM-9*4,ECM,Fuel*2", "AIM-120*4,AIM-9*2,ECM,Fuel*2", "AGM-88*2,AIM-120*2,AIM-9*2,ECM,Fuel*2", "Mk-84*2,AIM-9*2,ECM,Fuel*2", "AIM-120*6,ECM,Fuel*2", "AGM-154*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "AGM-88*2,AGM-65D*2,AIM-120B*2,AIM-9M*2,ECM", "Mk20*2,AIM-120*2,ECM,Fuel*2", "AIM-120*2,AIM-9*2,GBU-12*2,ECM,Lantirn ,Fuel*2", "AGM-65K*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "AIM-120B*2_AIM-9M*2_AGM-119*4_ALQ-131"], "F-16A": ["AGM-88*2,AIM-120*2,AIM-9*2,ECM,Fuel*2", "Mk20*4,AIM-9*2,ECM,Fuel*2", "AIM-9*4,ECM,Fuel*2", "AGM-65D*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "AIM-120*4,AIM-9*2,ECM,Fuel*2", "AGM88*2_AGM-65D*6_AIM-120*2_AIM-9*2_ECM", "AGM-65K*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "AIM-120*2,AIM-9*2,GBU-12*2,ECM,Lantirn ,Fuel*2", "AIM-120*2,GBU-10*2,ECM,Lantirn ,Fuel*2", "AIM-120*6,ECM,Fuel*2", "AIM-120*6,ECM", "AIM-9*4,ECM", "Fuel*3", "Mk-84*2,AIM-9*2,ECM,Fuel*2", "AIM-120*4,AIM-9*2,ECM", "Mk-82*6,AIM-120*2,ECM,Fuel*2", "Mk-82*2,AIM-120*2,AIM-9*2,ECM,Fuel*2", "AGM-154*2,AIM-120*2,ECM,Lantirn ,Fuel*2", "Mk20*2,AIM-120*2,ECM,Fuel*2", "AGM-119*2,AIM-120B*2,AIM-9M*2,ALQ-184,Fuel*2"], "F-16C bl.50": ["AIM-120B*4, AIM-9M*2, Fuel", "AIM-120C*4, AIM-9M*2, Fuel", "AGM-65G*2, AIM-120C*2, AIM-9M*2, Fuel, ECM", "AGM-65D*6, AIM-120B*2, AIM-9M*2, ECM, Fuel", "Mk86*6, AIM-9M*4, ECM, Fuel", "CBU-97*4, AIM-9M*4, Fuel", "CBU-87*4, AIM-9M*4, Fuel", "AGM-65D*2, CBU-87*2, AIM-9M*4, Fuel", "AGM-65D*2, CBU-97*2, AIM-9M*4, Fuel", "Mk84*2, AIM-9M*4, ECM, Fuel", "GBU-31v1*2, AIM-9M*4, ECM, Fuel", "GBU-31v3*2, AIM-9M*4, ECM, Fuel", "GBU-31v1*2, Mk82*6, AIM-9M*4, Fuel", "Mk84*4, AIM-9M*4, Fuel", "Mk82*12, AIM-9M*4, Fuel", "AGM-154C*2, AIM-9M*4, ECM, Fuel"], "F-16C bl.52d": ["AGM-88*2, AGM-65D*2, AIM-120B*2, ECM,LIGHTNING", "Mk-82*6,AIM-120*2,ECM,Fuel*2,LIGHTNING", "AGM88*2,AGM-65D*6,AIM-120*2,AIM-9*2,ECM,LIGHTNING", "AGM-65D*4,AIM-120*2,ECM,Fuel*2,LIGHTNING", "AIM-120*2,GBU-31*2,ECM,Fuel*2,LIGHTNING", "AIM-120*2,GBU-31(v)*2,ECM,Fuel*2,LIGHTNING", "AGM-88*2,AIM-120*2,AIM-9*2,ECM,Fuel*2,LIGHTNING", "Copy AIM-120*2,GBU-38*2,ECM,Fuel*2,LIGHTNING", "AIM-120*2,GBU-10*2,ECM,Fuel*2,LIGHTNING", "Mk-84*2,AIM-120*2,ECM,Fuel*2,LIGHTNING", "AGM-154*2,AIM-120*2,ECM,Fuel*2,LIGHTNING", "Mk-82AIR*6,AIM-120*2,ECM,Fuel*2,LITENING", "CBU97*4,AIM120*2,ECM,Fuel*2,LITENING", "AGM-88*2,AGM-65D*2,AIM-120B*2,AIM-9M*2,ECM,LITENING", "CBU87*2,AIM-120*2,ECM,Fuel*2,LITENING", "AGM88, AGM-65D, AIM-120*2,Fuel*2,ECM,LITENING", "AIM-120*2,GBU-12*2,ECM,Fuel*2,LITENING", "AGM-65K*2,AIM-120*2,ECM,Lantirn ,Fuel*2,LITENING", "AGM-65G*4,AIM-120C*2,AIM-9M*2,ECM", "AIM-120C*4,AIM-9M*2,ECM,Fuel*2", "AIM-120B*4,AIM-9M*2,ECM,Fuel*2", "AIM-9M*4,Fuel*2", "AIM-120C*2,AIM-120B*2,AIM-9M*2,Fuel*3", "AIM-9P*4", "AIM-9M*2,ECM"], "F-4E": ["AIM-9*4,AIM-7*4", "AGM45*2_AGM-65D*4_AIM7*2_ECM", "AGM-45*2,AIM-7*2,Fuel*2,ECM", "Mk-82*18,AIM-7*2,ECM", "GBU-12*2,AIM-7*2,Fuel*2,ECM", "Mk20*12,AIM-7*2,ECM", "Mk-82*6,AIM-7*2,Fuel*2,ECM", "GBU-10*2,AIM-7*2,Fuel*2,ECM", "Mk20*6,AIM-7*2,Fuel*2,ECM", "AGM-45*4,AIM-7*2,ECM", "AGM-65K*4,AIM-7*2,Fuel*2,ECM", "Fuel*3", "AIM-9*4,AIM-7*4,Fuel*2", "Mk-84*2,AIM-7*2,ECM", "AGM-65K*4,AIM-7M*4,Fuel*3"], "F/A-18A": ["GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-9*6,Fuel", "Mk-84*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-65D*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM88*4_AIM9*2_AIM7_FLIR Pod_Fuel", "AIM-9*4,Fuel*3", "LAU-10*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-88*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "MK-82*8,AIM-9*2,AIM-7,FLIR Pod,Fuel", "MK-82*4,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-9*4,AIM-7*2,Fuel*3", "AGM-65K*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "Fuel*3", "AGM88*2_AGM65D*2_AIM9*2_AIM7_FLIR Pod_Fuel", "GBU-12*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-9*4,AIM-7*4,Fuel", "Mk20*4,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "GBU-10*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-84A*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3"], "F/A-18C": ["AGM-62*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "GBU-10*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "GBU-12*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "Fuel*3", "Mk-84*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-154*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-120*4,AIM-9*2,Fuel*3", "AGM-65D*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM88*2_AGM65D*2_AIM9*2_AIM7_FLIR Pod_Fuel", "AGM88*4_AIM9*2_AIM7_FLIR Pod_Fuel", "AIM-9*2,AIM-7*6,Fuel", "Mk20*4,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-120*6,AIM-9*2,Fuel", "LAU-10*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-9*2,AIM-7*4,Fuel*3", "MK-82*8,AIM-9*2,AIM-7,FLIR Pod,Fuel", "AIM-120*2,AIM-9*2,AIM-7*2,Fuel*3", "AGM-88*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-84E*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "MK-82*4,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AGM-84A*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3", "AIM-9M*2,AGM-65D*2,Mk-82*4,FLIR Pod,Fuel", "AIM-120*4,AIM-9*2,AIM-7*2,Fuel", "AGM-65K*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3"], "IL-76MD": [], "IL-78M": [], "Ka-27": [], "Ka-52": ["APU-6 Vikhr-M*2, Kh-25ML*2", "APU-6 Vikhr-M*2", "B-8*4", "KMGU-2 (AO-2.5RT)*4", "B-8*2, APU-6 Vikhr-M*2", "FAB-500*2", "UB-13*2", "FAB-250*4", "Kh-25ML*2, R-73*2"], "KC-10A": [], "KC-135": [], "Mi-24V": ["2xFAB-500", "10xS-13", "2xFAB-250", "4x9M114, 2xFuel tank", "128xS-5", "80xS-8 TsM", "4x9M114, 40xS-8 TsM", "64xS-5", "8x9M114", "4x9M114, 80xS-8", "4x9M114, 128xS-5", "2xKMGU AP", "4xFuel tank", "4xUPK-23", "4x9M114, 10xS-13", "4x9M114, 80xS-8 TsM", "4x9M114", "80xS-8", "40xS-8 TsM", "8x9M114, 40xS-8 TsM", "8x9M114, 10xS-13", "2xFuel tank", "4x9M114, 4xUPK-23", "2xUPK-23", "8x9M114, 64xS-5", "8x9M114, 40xS-8", "20xS-13", "40xS-8", "8x9M114, 2xUPK-23"], "Mi-26": [], "Mi-28N": ["2xFAB-250", "4xFuel tank", "80xS-8", "4xKMGU AP", "4xUPK-23", "16x9M114, 10xS-13", "4xFAB-500", "16x9M114, 2xFAB-500", "40xS-8", "40xS-8 TsM", "2xKMGU AP", "2xUPK-23", "16x9M114, 2xUPK-23", "2xFAB-500", "16x9M114, 40xS-8", "16x9M114", "20xS-13", "16x9M114, 2xKMGU AP", "4xFAB-250", "4xKMGU AT", "16x9M114, 40xS-8 TsM", "80xS-8 TsM", "2xKMGU AT", "9x9M114", "2xFuel tank", "10xS-13", "2xFAB-250, 16x9M114", "16x9M114, 2xKMGU AT"], "Mi-8MT": ["4 x B8", "4 x B8 + 2GUV_AP-30 (GrL 30mm)", "2 x UPK +2 x B8", "2 xB8 + 2GUV_YaKB (MG-12.7+7.62)+ 2GUV_AP-30 (GrL 30mm)", "6 x FAB-100", "2 x B8 + 2 x UPK-23-250", "2 x UPK--23-250"], "MiG-15bis": ["2*FAB-50", "2*FAB-100M", "2*300L", "2*400L", "2*600L", "Fuel tank 300", "Fuel tank 400"], "MiG-23MLD": ["R-60M*4", "B-8*2,R-60M*2,Fuel-800", "UB-32*2,R-60M*2,Fuel-800", "R-24R*2,R-60M*4,Fuel-800", "R-24R,R-24T,R-60M*4,Fuel-800", "R-60M*4,Fuel-800", "FAB-500*2,R-60M*2,Fuel-800", "R-24R*2,R-60M*4", "FAB-250*2,R-60M*2,Fuel-800", "RBK-250*2,R-60M*2,Fuel-800", "RBK-500*2,R-60M*2,Fuel-800", "R-24R,R-24T,R-60M*4"], "MiG-25PD": ["R-40R*2,R-40T*2", "R-40R*4", "R-40R*2,R-60M*2"], "MiG-25RBT": ["FAB-500x2_60x2", "R-60M*2"], "MiG-27K": ["FAB-250*6,R-60M*2,Fuel", "BetAB-500ShP*2,FAB-250*2,R-60*2", "Kh-25MR*2,R-60M*2,Fuel", "Kh-29L*2,R-60M*2,Fuel", "B-8*4", "BetAB-500*2,FAB-500*2,R-60*2", "Kh-25MPU*2,R-60M*2,Fuel", "Kh-29T*2,R-60M*2,Fuel", "FAB-500*2,FAB-250*2,R-60M*2,Fuel", "Kh-25ML*2,R-60M*2,Fuel", "KAB-500*2,R-60M*2,Fuel", "RBK-500AO*2,RBK-250*2,R-60M*2", "UB-32*4", "Kh-29L*2,R-60*2,Fuel"], "MiG-29A": ["Fuel-1150*2,Fuel-1500", "RBK-500AO*4,R-73*2,Fuel", "FAB-250*4,R-73*2,Fuel", "B-8*4,R-73*2,Fuel", "R-60M*4,R-27R*2", "R-73*4,R-27R*2,Fuel-1500", "R-73*6,Fuel-1500", "R-60M*6,Fuel-1500", "S-24*4,R-73*2,Fuel", "FAB-500*4,R-73*2,Fuel", "R-60M*6", "BetAB-500*4,R-73*2,Fuel", "R-73*6", "R-73*2,R-60M*2,R-27R*2,Fuel-1500", "R-60M*4,R-27R*2,Fuel-1500", "RBK-250*4,R-73*2,Fuel", "R-73*4,R-27R*2", "R-73*2,R-60M*2,R-27R*2", "S-24*2,FAB-500*2,R-73*2,Fuel"], "MiG-29G": ["R-73*6,Fuel-1500", "R-73*4,R-27R*2,Fuel-1500", "R-73*2,R-60M*2,R-27R*2", "R-60M*4,R-27R*2,Fuel-1500", "R-73*6", "R-60M*4,R-27R*2", "R-73*4,R-27R*2", "Fuel-1150*2,Fuel-1500", "R-60M*6", "R-60M*6,Fuel-1500", "R-73*2,R-60M*2,R-27R*2,Fuel-1500"], "MiG-29S": ["R-73*2,R-60M*2,R-27R*2", "R-73*4,R-27R*2,Fuel-1500", "R-73*6,Fuel-1500", "R-60M*6,Fuel-1500", "S-24*4,R-73*2,Fuel", "FAB-500*4,R-73*2,Fuel", "BetAB-500*4,R-73*2,Fuel", "RBK-500AO*4,R-73*2,Fuel", "R-73*2,R-60M*2,R-27R*2,Fuel-1500", "R-77*2,R-73*2,Fuel-1500,Fuel-1150*2", "B-8*4,R-73*2,Fuel", "RBK-250*4,R-73*2,Fuel", "R-73*6", "Fuel-1150*2,Fuel-1500", "R-60M*6", "R-60M*4,R-27R*2", "R-73*4,R-27R*2", "R-77*4,R-73*2", "FAB-250*4,R-73*2,Fuel", "R-60M*4,R-27R*2,Fuel-1500", "R-77*4,R-73*2,Fuel-1500", "S-24*2,FAB-500*2,R-73*2,Fuel"], "MiG-31": ["R-40T*2,R-33*4", "R-40T,R-33*4,R-40R", "R-40R*2,R-33*4", "R-60M*4,R-33*4"], "Mirage 2000-5": ["R 550*2,MICA IR*4", "R 550*2,MICA IR*2,MICA AR*2,Fuel*3", "R 550*2,MICA AR*4,Fuel*3", "R 550*2,SUPER 530F*2,Fuel", "Fuel*3", "R 550*2,MICA IR*4,Fuel*3", "R 550*2,MICA AR*4", "R 550*2,SUPER 530F*2"], "MQ-9 Reaper": ["GBU-12*4", "GBU-38*4", "AGM-114K*8,GBU-38*2", "AGM-114K*12"], "OH-58D": ["2xAGM-114, 7xHYDRA-70", "4xAGM-114", "M-3, 7xHYDRA-70", "2xAGM-114, M-3", "14xHYDRA-70", "14xHYDRA-70 WP"], "RQ-1A Predator": ["AGM-114K*2"], "S-3B": ["AGM-84A*2, Mk-82*2", "AGM-84E*2", "AGM-65D, AGM-65K, Mk20*4", "AGM-65D, AGM-65K, Mk82*4", "Mk82*10", "Mk84*2, Mk82*4", "ZUNI Mk71*8, Mk20*4"], "SH-3W": [], "SH-60B": ["AGM-119"], "Su-17M4": ["UB-32*4,R-60M*2,FAB-250*4", "FAB-100*24,R-60M*2", "UB-32*4,R-60M*2,Fuel*2", "B-8*4,R-60M*2,FAB-250*4", "Kh-29L*2,R-60M*2,Fuel*2", "B-8*4,R-60M*2,Fuel*2", "Kh-29T*2,R-60M*2,Fuel*2", "BetAB-500*6,R-60M*2", "Kh-25MR*4,R-60M*2,Fuel*2", "S-24*4,R-60M*2,Fuel*2", "Kh25MPU*2_Kh25ML*2_,R60M*2_Fuel*2", "Kh58*2_Kh25MPU*2_R60M*2_Fuel*2", "FAB-250*16,R-60M*2", "Kh-25ML*4,R-60M*2,Fuel*2", "RBK-500AO*4,SPPU-22*2,R-60M*2", "S-24*4,R-60M*2,FAB-250*4", "Fuel*4", "FAB-500*6,R-60M*2", "Kh-25ML*2,Kh-29L*2,R-60*2"], "Su-24M": ["UB-13*4,FAB-500*2", "Kh-31A*2,R-60M*2,Fuel", "UB-13*4", "KAB-500*4,R-60M*2", "S-25*2,Fuel*3", "Kh31P*2_Kh25ML*2_L-081", "B-8*2,Fuel*3", "FAB-1500*2,R-60M*2", "S-24*4", "BetAB-500*4,R-60M*2", "Kh-25ML*4", "Kh-25MR*4", "FAB-100*24", "Kh-31A*2,R-60M*2", "UB-13*2,Fuel*3", "B-8*2,Fuel*2", "Kh58*2_Kh25ML*2_L-081", "RBK-250*8", "UB-32*4", "Kh-29L*2,R-60M*2", "S-24*2,Fuel*3", "Kh25MPU*2_Kh25ML*2_L-081", "FAB-500*4,R-60M*2", "FAB-250*8", "Fuel*3", "RBK-500AO*4,R-60M*2", "KAB-1500*2,R-60M*2,Fuel", "UB-32*4,FAB-250*4", "Kh-29T*2,R-60M*2", "UB-32*2,Fuel*3", "Kh-59M*2,R-60M*2,Fuel", "S-25*4", "B-8*6"], "Su-24MR": ["SHPIL,ETHER,R-60M*2", "Fuel*2", "TANGAZH,ETHER,R-60M*2,Fuel*2", "TANGAZH,ETHER,R-60M*2", "SHPIL,ETHER,R-60M*2,Fuel*2"], "Su-25": ["RBK-250*2,S-8KOM*80,R-60M*2,Fuel*2", "FAB-250*4,UB-13*2,R-60M*2,SPPU-22*2", "S-25L*6,UB-13*2,R-60M*2", "S-25*6,SPPU-22*2,R-60M*2", "2-25L*2, KH-25ML*2, RBK-500*2, B-8MI*2, R-60M*2", "S-8KOM*120,R-60M*2,Fuel*2", "FAB-250*4,S-25*2,R-60M*2,Fuel*2", "RBK-500AO*4,S-8KOM*40,R-60M*2,Fuel*2", "FAB-250*2,SPPU-22*2,SAB-100*4,R-60M*2", "RBK-500AO*6,R-60M*2,Fuel*2", "RBK-250*8,R-60M*2", "Kh-29L*2,Kh-25ML*4,R-60M*2", "RBK-250*4,S-8KOM*80,R-60M*2", "FAB-250*4,UB-13*2,R-60M*2,Fuel*2", "S-8TsM*160,R-60*2", "Kh-25ML*4,R-60M*2,Fuel*2", "BetAB-500ShP*8,R-60M*2", "SAB-100*8,R-60*2", "Kh-29L*2,Kh-25ML*4,S-25L*2,R-60M*2", "FAB-500*6,R-60M*2,Fuel*2", "Kh-29L*2,Kh-25ML*2,R-60M*2,Fuel*2", "Kh-29L*2,R-60M*2,Fuel*2", "FAB-100*32,R-60M*2", "FAB-100*16,R-60M*2,Fuel*2", "FAB-250*6,R-60M*2,Fuel*2", "BetAB-500*6,R-60M*2,Fuel*2", "S-25*6,R-60M*2,Fuel*2", "UB-13*6,R-60M*2,Fuel*2", "Kh-25*4,Kh-29T*2,R-60*2", "S-25L*6,R-60*2,Fuel*2"], "Su-25T": ["FAB-250*4,SPPU-22*2,SAB-100*2,R-60M*2", "Kh-29L*2,Kh-25ML*4,R-73*2,Mercury LLTV Pod,MPS-410", "KAB-500Kr*2,Kh-25ML*2,R-73*2,MPS-410,Fuel*2", "RBK-500AO*4,UB-32*2,R-60M*2,Fuel*2", "BetAB-500ShP*8,R-60M*2", "UB-13*6,R-60M*2,Fuel*2", "Kh-29T*2,R-73*2,Fuel*2,MPS-410", "Kh58*2_Kh25ML*4_R73*2_L-081_MPS-410", "FAB-250*4,UB-13*2,R-60M*2,Fuel*2", "KH-29T*2, VIKHR*2, ECM", "Kh-29T*2,Kh-25ML*4,R-73*2,MPS-410", "FAB-250*4,UB-13*2,R-60M*2,SPPU-22*2", "FAB-500*6,R-60M*2,Fuel*2", "Fuel*4", "APU-8 Vikhr-M*2,Kh-25ML,R-73*2,SPPU-22*2,Mercury LLTV Pod,MPS-410", "S-8KOM*120,R-60M*2,Fuel*2", "KMGU-2 (PTAB-2.5KO)*8,R-60M*2", "FAB-250*6,R-60M*2,Fuel*2", "Kh-29L*2,R-73*2,Fuel*2,Mercury LLTV Pod,MPS-410", "FAB-100*32,R-60M*2", "RBK-250*8,R-60M*2", "S-25L*6,UB-13*2,R-60M*2", "FAB-250*4,S-25*2,R-60M*2,Fuel*2", "S-25*2,SPPU-22*4,R-60M*2,R-73*2", "KMGU-2 (AO-2.5RT)*8,R-60M*2", "APU-8 Vikhr-M*2,S-25L*2,R-73*2,SPPU-22*2,Mercury LLTV Pod,MPS-410", "S-25*6,R-60M*2,Fuel*2", "RBK-500AO*6,R-60M*2,Fuel*2", "Kh58*2_Kh25MPU*2_Kh25ML*2_R73*2_L-081_MPS-410", "RBK-250*4,UB-32*4,R-60M*2", "Kh25MPU*4_R73*2_Fuel*2_L-081_MPS-410", "BetAB-500*6,R-60M*2,Fuel*2", "RBK-250*2,UB-32*4,R-60M*2,Fuel*2", "FAB-100*16,R-60M*2,Fuel*2", "Kh-29L*2,Kh-25ML*4,R-73*2,ECM"], "Su-25TM": ["BetAB-500ShP*6,R-73*2,ECM", "APU-8 Vikhr-M*2,R-60M*2,R-73*2,SPPU-22*2,Mercury LLTV Pod", "Kh-29T*2,Kh-25ML*2,R-73*2,Fuel*2,ECM", "Kh-58*2_Kh-25MPU*4_R-73*2_L-081_MPS410", "S-25L*6,UB-13*2,R-60M*2", "Kh-35*2_R-73*2_Fuel*2_MPS410_Kopyo-25", "Kh-31A*2_R-73*2_Fuel*2_MPS410_Kopyo-25", "Fuel*4", "BetAB-500*6,R-60M*2,Fuel*2", "KAB-500Kr*2,Kh-25ML*2,R-73*2,Fuel*2,ECM", "Kh-29L*2,R-73*2,Fuel*2,Mercury LLTV Pod,ECM", "UB-13*6,R-60M*2,Fuel*2", "S-25*2,SPPU-22*4,R-60M*2,R-73*2", "Kh-31P*2_Kh-25ML*4_R-73*2_L-081_MPS410", "UB-32*6,R-60M*2,Fuel*2", "FAB-100*16,R-60M*2,Fuel*2", "FAB-250*6,R-60M*2,Fuel*2", "FAB-500*6,R-60M*2,Fuel*2", "Kh-25MPU*4_R-73*2_Fuel*2_L-081_MPS410", "S-25*6,R-60M*2,Fuel*2", "RBK-500AO*4,UB-32*2,R-60M*2,Fuel*2", "FAB-250*4,UB-13*2,R-60M*2,SPPU-22*2", "Kh-31P*2_Kh-25MPU*4_R-73*2_L-081_MPS410", "RBK-500AO*6,R-60M*2,Fuel*2", "FAB-100*32,R-60M*2", "RBK-250*2,UB-32*4,R-60M*2,Fuel*2", "RBK-250*8,R-60M*2", "APU-8 Vikhr-M*2,R-73*2,SPPU-22*2,Mercury LLTV Pod,ECM", "RBK-250*4,UB-32*4,R-60M*2", "FAB-250*4,SPPU-22*2,SAB-100*2,R-60M*2", "FAB-250*4,S-25*2,R-60M*2,Fuel*2", "FAB-250*4,UB-13*2,R-60M*2,Fuel*2"], "Su-27": ["R-73*4,R-27ER*4,R-27ET*2", "KMGU-2 (AO-2.5RT)*5,R-73*2,ECM", "BetAB-500ShP*6,R-73*2,ECM", "KMGU-2 (PTAB-2.5KO)*5,R-73*2,ECM", "R-73*2,R-27ER*6,ECM", "R-73*6", "S-13*10,RBK-500AO*2,FAB-500*2,R-73*2,ECM", "R-73*4,R-27ER*6", "R-73*2,R-27ER*4,R-27ET*2,ECM", "R-73*4,ECM", "ECM", "FAB-500*6,R-73*2,ECM", "S-25*2,FAB-500*4,R-73*4", "S-25*4, FAB-500*4, R-73*2, ECM", "CAS S-8KOM Rockets + RBK-500 PTAB1", "CAS S-8OFP Rockets + FAB-500 Bombs", "CAS S-8OFP Rockets", "CAS S-8OFP Rockets + FAB-100 Bombs", "CAS S-8KOM Rockets + FAB-100 Bombs", "CAS S-13 Rockets", "CAS S-8KOM Rockets + FAB-250 Bombs", "CAS S-8KOM Rockets + RBK-250 PTAB2.5", "CAS S-8KOM Rockets", "CAS S-8KOM Rockets + FAB-500 Bombs", "CAS S-8KOM Rockets + RBK-500 PTAB10", "CAS S-8KOM Rockets + KMGU PTAB", " CAS S-25 Rockets", "CAS S-25 Rockets + FAB-500 Bombs"], "Su-30": ["R-73*2,R-77*6,ECM", "R-73*2,R-27T*2,R-27R*4", "RBK-500AO*6,R-73*2,ECM", "Kh-31P*2,Kh-31A*2,R-73*2,R-77*2,ECM", "R-73*4,R-27T*2,R-27R*4", "R-73*2,R-77*2,Kh-35*2,ECM", "Kh-35*2,Kh-31P*2,R-73*2,R-77*2,ECM", "FAB-250*4,B-8*2,R-73*2,ECM", "ECM", "KAB-1500*2,R-73*2,R-77*2,ECM", "RBK-250*6,R-73*2,ECM", "R-73*4,R-77*6", "FAB-250*4,S-25*2,R-73*2,ECM", "R-73*2,R-27R*2,R-27ER*4,ECM", "R-73*2,R-27T*2,R-27ER*2,R-77*2,ECM", "FAB-1500*2,R-73*2,R-77*2,ECM", "R-73*4,R-27T*2,R-27ER*2,R-77*2", "Kh-59M*2,R-73*2,R-77*2,ECM", "FAB-500*6,R-73*2,ECM", "R-73*4,R-27R*2,R-27ER*4", "Kh-29L*4,R-73*2,R-77*2,ECM", "BetAB-500*6,R-73*2,ECM", "R-73*4", "FAB-250*4,UB-13*2,R-73*2,ECM", "R-73*2,R-77*4,R-27ER*2,ECM", "KAB-500*4,R-73*2,R-77*2,ECM", "FAB-250*6,R-73*2,ECM", "R-73*4,R-77*4,R-27ER*2", "Kh-29T*4,R-73*2,R-77*2,ECM", "Kh-31P*2,Kh-31A*2,R-73*2,R-77*2,ECM", "Kh-31P*4,R-73*2,R-77*2,ECM"], "Su-33": ["RBK-250*6,R-73*2,R-27R*2,ECM", "R-73*4", "R-73*4,R-27R*2,R-27ER*6", "R-73*2,R-27ET*2,R-27ER*6,ECM", "R-73*4,R-27ET*2,R-27ER*6", "FAB-250*6,R-73*2,R-27R*2,ECM", "R-73*2,R-27R*2,R-27ER*6,ECM", "ECM", "BetAB-500*6,R-73*2,R-27R*2,ECM", "RBK-500AO*6,R-73*2,R-27R*2,ECM", "UB-13*4,FAB-250*4,R-73*2,ECM", "S-25*4,FAB-250*4,R-73*2,ECM", "FAB-500*6,R-73*2,R-27R*2,ECM", "B-8*4,FAB-250*4,R-73*2,ECM", "S-25*4,FAB-500*4,R-73*4", "CAS S-8KOM rockets + FAB500", "CAS S-8OFP rockets + FAB500", "CAS S-13 Rockets + FAB500", "CAS S-13 Rockets + FAB100", "CAS S-8KOM rockets + FAB250", "CAS S-25 Rockets + FAB500", "CAS S-8KOM rockets + RBK500 PTAB10", "CAS S-8KOM rockets + RBK500 PTAB1"], "Su-34": ["UB-13*4,FAB-250*4,R-73*2,ECM", "FAB-100*28,R-73*2,ECM", "BetAB-500*8,R-73*2,ECM", "Kh-29L*4,R-73*2,R-77*2,ECM", "KAB-500*4,R-73*2,R-77*2,ECM", "RBK-250 PTAB-2.5M*8,R-73*2,ECM", "FAB-250*8,R-73*2,ECM", "ECM", "Kh-29T*4,R-73*2,R-77*2,ECM", "RBK-500 PTAB-10-5*8,R-73*2,ECM", "FAB-1500*3,R-73*2,R-77*2,ECM", "Kh-59M*2,R-73*2,R-77*2,ECM", "B-8*6,R-73*2,R-27R*2,ECM", "FAB-500*8,R-73*2,ECM", "KAB-1500*2,R-73*2,R-77*2,ECM", "Kh-29T*4,R-73*2,R-27R*2,ECM", "Kh-31A*4,Kh-31P*2,R-73*2,R-27R*2,ECM", "Kh-31A*6,R-73*2,R-27R*2,ECM", "Kh-31P*4,R-73*2,R-27R*2,ECM", "Kh-29L*4,R-73*2,R-27R*2,ECM"], "Tornado GR4": ["AIM-9M*2, Fuel*2, ECM", "ALARM*4, Fuel*2, ECM", "GBU-16*2, AIM-9M*2, Fuel*2, ECM", "BL755*4, AIM-9M*2, Fuel*2, ECM", "Sea Eagle*2, AIM-9M*2, Fuel*2, ECM"], "Tornado IDS": ["Kormoran*2,AIM-9*2,Fuel*2", "GBU-16*2,AIM-9*2,Fuel*2", "Fuel*2", "AGM-88*4,AIM-9*2,ECM", "AGM-88*2,AIM-9*2,Fuel*2,ECM", "Kormoran*4,AIM-9*2", "Kormoran*2,AIM-9*2,AGM-88*2", "Mk-82*4,AIM-9*2,Fuel*2"], "Tu-142": ["Kh-35*6"], "Tu-160": ["Kh-65*12"], "Tu-22M3": ["Kh-22N", "Kh-22N*2", "FAB-250*69", "FAB-500*33", "FAB-500*33, FAB-250*36", "FAB-250*33"], "Tu-95MS": ["Kh-65*6"], "UH-1H": ["M134 Minigun*2, XM158*2"], "UH-60A": [] } \ No newline at end of file diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index 723aac23..54e9d37b 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,112 +1,176 @@ -import { Marker, LatLng, Polyline, Icon } from 'leaflet'; -import { ConvertDDToDMS } from '../other/utils'; -import { getMap, getUnitsManager, getVisibilitySettings } from '..'; -import { UnitMarker, MarkerOptions, AircraftMarker, HelicopterMarker, GroundUnitMarker, NavyUnitMarker, WeaponMarker } from './unitmarker'; -import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, landAt, setAltitude, setReactionToThreat, setROE, setSpeed } from '../dcs/dcs'; +import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; +import { getMap, getUnitsManager } from '..'; +import { mToFt, msToKnots, rad2deg } from '../other/utils'; +import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads, bombPoint, carpetBomb, bombBuilding, fireAtArea } from '../server/server'; +import { aircraftDatabase } from './aircraftdatabase'; +import { groundUnitsDatabase } from './groundunitsdatabase'; +import { CustomMarker } from '../map/custommarker'; +import { SVGInjector } from '@tanem/svg-injector'; +import { UnitDatabase } from './unitdatabase'; +import { BOMBING, CARPET_BOMBING, FIRE_AT_AREA, IDLE, MOVE_UNIT } from '../map/map'; +import { TargetMarker } from '../map/targetmarker'; var pathIcon = new Icon({ - iconUrl: 'images/marker-icon.png', - shadowUrl: 'images/marker-shadow.png', + iconUrl: '/resources/theme/images/markers/marker-icon.png', + shadowUrl: '/resources/theme/images/markers/marker-shadow.png', iconAnchor: [13, 41] }); -export class Unit { - ID: number = -1; - formation: string = ""; - name: string = ""; - unitName: string = ""; - groupName: string = ""; - latitude: number = 0; - longitude: number = 0; - altitude: number = 0; - heading: number = 0; - speed: number = 0; - coalitionID: number = -1; - alive: boolean = true; - currentTask: string = ""; - fuel: number = 0; - type: any = null; - flags: any = null; - activePath: any = null; - ammo: any = null; - targets: any = null; - hasTask: boolean = false; - isLeader: boolean = false; - isWingman: boolean = false; - leaderID: number = 0; - wingmen: Unit[] = []; - wingmenIDs: number[] = []; - targetSpeed: number = 0; - targetAltitude: number = 0; - ROE: string = ""; - reactionToThreat: string = ""; - +export class Unit extends CustomMarker { + ID: number; + + #data: UnitData = { + baseData: { + controlled: false, + name: "", + unitName: "", + groupName: "", + alive: true, + category: "", + }, + flightData: { + latitude: 0, + longitude: 0, + altitude: 0, + heading: 0, + speed: 0, + }, + missionData: { + fuel: 0, + flags: {}, + ammo: {}, + contacts: {}, + hasTask: false, + coalition: "", + }, + formationData: { + leaderID: 0 + }, + taskData: { + currentState: "NONE", + currentTask: "", + activePath: {}, + desiredSpeed: 0, + desiredSpeedType: "GS", + desiredAltitude: 0, + desiredAltitudeType: "AGL", + targetLocation: {}, + isTanker: false, + isAWACS: false, + onOff: true, + followRoads: false, + targetID: 0 + }, + optionsData: { + ROE: "", + reactionToThreat: "", + emissionsCountermeasures: "", + TACAN: { isOn: false, channel: 0, XY: "X", callsign: "" }, + radio: { frequency: 0, callsign: 1, callsignNumber: 1}, + generalSettings: { prohibitJettison: false, prohibitAA: false, prohibitAG: false, prohibitAfterburner: false, prohibitAirWpn: false} + } + }; + #selectable: boolean; #selected: boolean = false; + #hidden: boolean = false; + #highlighted: boolean = false; + #preventClick: boolean = false; + #pathMarkers: Marker[] = []; #pathPolyline: Polyline; - #targetsPolylines: Polyline[]; - #marker: UnitMarker; + #contactsPolylines: Polyline[]; + #miniMapMarker: CircleMarker | null = null; + #targetLocationMarker: TargetMarker; + #targetLocationPolyline: Polyline; + #timer: number = 0; - static getConstructor(name: string) { - if (name === "GroundUnit") return GroundUnit; - if (name === "Aircraft") return Aircraft; - if (name === "Helicopter") return Helicopter; - if (name === "Missile") return Missile; - if (name === "Bomb") return Bomb; - if (name === "NavyUnit") return NavyUnit; + #hotgroup: number | null = null; + + static getConstructor(type: string) { + if (type === "GroundUnit") return GroundUnit; + if (type === "Aircraft") return Aircraft; + if (type === "Helicopter") return Helicopter; + if (type === "Missile") return Missile; + if (type === "Bomb") return Bomb; + if (type === "NavyUnit") return NavyUnit; } - constructor(ID: number, marker: UnitMarker) { + constructor(ID: number, data: UpdateData) { + super(new LatLng(0, 0), { riseOnHover: true, keyboard: false }); + this.ID = ID; this.#selectable = true; - /* The marker is set by the inherited class */ - this.#marker = marker; - this.#marker.on('click', (e) => this.#onClick(e)); - this.#marker.on('dblclick', (e) => this.#onDoubleClick(e)); - this.#marker.on('contextmenu', (e) => this.#onContextMenu(e)); + this.on('click', (e) => this.#onClick(e)); + this.on('dblclick', (e) => this.#onDoubleClick(e)); + this.on('contextmenu', (e) => this.#onContextMenu(e)); + this.on('mouseover', () => { this.setHighlighted(true); }) + this.on('mouseout', () => { this.setHighlighted(false); }) this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 }); this.#pathPolyline.addTo(getMap()); - this.#targetsPolylines = []; + this.#contactsPolylines = []; + + this.#targetLocationMarker = new TargetMarker(new LatLng(0, 0)); + this.#targetLocationPolyline = new Polyline([], { color: '#FF0000', weight: 3, opacity: 0.5, smoothFactor: 1 }); + + /* Deselect units if they are hidden */ + document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { + window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); + }); + + document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { + window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); + }); + + /* Set the unit data */ + this.setData(data); } - update(response: any) { - for (let entry in response) { - // @ts-ignore TODO handle better - this[entry] = response[entry]; + getMarkerCategory() { + // Overloaded by child classes + return ""; + } + + getDatabase(): UnitDatabase | null { + // Overloaded by child classes + return null; + } + + getIconOptions(): UnitIconOptions { + // Default values, overloaded by child classes if needed + return { + showState: false, + showVvi: false, + showHotgroup: false, + showUnitIcon: true, + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: false, + rotateToHeading: false } - - // TODO handle better - if (response['activePath'] == undefined) - this.activePath = null - - /* Dead units can't be selected */ - this.setSelected(this.getSelected() && this.alive) - - this.#updateMarker(); - - this.#clearTargets(); - if (this.getSelected() && this.activePath != null) - { - this.#drawPath(); - this.#drawTargets(); - } - else - this.#clearPath(); } setSelected(selected: boolean) { /* Only alive units can be selected. Some units are not selectable (weapons) */ - if ((this.alive || !selected) && this.#selectable && this.#selected != selected) { + if ((this.getBaseData().alive || !selected) && this.getSelectable() && this.getSelected() != selected) { this.#selected = selected; - this.#marker.setSelected(selected); - getUnitsManager().onUnitSelection(); - + this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected); + if (selected) { + document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); + this.#updateMarker(); + } + else { + document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this })); + this.#clearDetectedUnits(); + this.#clearPath(); + this.#clearTarget(); + } } } @@ -122,58 +186,400 @@ export class Unit { return this.#selectable; } - addDestination(latlng: L.LatLng) { - var path: any = {}; - if (this.activePath != null) { - path = this.activePath; - path[(Object.keys(path).length + 1).toString()] = latlng; - } - else { - path = { "1": latlng }; - } - addDestination(this.ID, path); + setHotgroup(hotgroup: number | null) { + this.#hotgroup = hotgroup; + this.#updateMarker(); } - clearDestinations() { - this.activePath = null; + getHotgroup() { + return this.#hotgroup; + } + + setHighlighted(highlighted: boolean) { + if (this.getSelectable() && this.#highlighted != highlighted) { + this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted); + this.#highlighted = highlighted; + this.getGroupMembers().forEach((unit: Unit) => unit.setHighlighted(highlighted)); + } + } + + getHighlighted() { + return this.#highlighted; + } + + getGroupMembers() { + return Object.values(getUnitsManager().getUnits()).filter((unit: Unit) => {return unit != this && unit.getBaseData().groupName === this.getBaseData().groupName;}); + } + + /********************** Unit data *************************/ + setData(data: UpdateData) { + /* Check if data has changed comparing new values to old values */ + const positionChanged = (data.flightData != undefined && data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude)); + const headingChanged = (data.flightData != undefined && data.flightData.heading != undefined && this.getFlightData().heading != data.flightData.heading); + const aliveChanged = (data.baseData != undefined && data.baseData.alive != undefined && this.getBaseData().alive != data.baseData.alive); + const stateChanged = (data.taskData != undefined && data.taskData.currentState != undefined && this.getTaskData().currentState != data.taskData.currentState); + const controlledChanged = (data.baseData != undefined && data.baseData.controlled != undefined && this.getBaseData().controlled != data.baseData.controlled); + var updateMarker = (positionChanged || headingChanged || aliveChanged || stateChanged || controlledChanged || !getMap().hasLayer(this)); + + /* Load the data from the received json */ + Object.keys(this.#data).forEach((key1: string) => { + Object.keys(this.#data[key1 as keyof(UnitData)]).forEach((key2: string) => { + if (key1 in data && key2 in data[key1]) { + var value1 = this.#data[key1 as keyof(UnitData)]; + var value2 = value1[key2 as keyof typeof value1]; + if (typeof data[key1][key2] === typeof value2 || typeof value2 === "undefined") + //@ts-ignore + this.#data[key1 as keyof(UnitData)][key2 as keyof typeof struct] = data[key1][key2]; + } + }); + }); + + /* Fire an event when a unit dies */ + if (aliveChanged && this.getBaseData().alive == false) + document.dispatchEvent(new CustomEvent("unitDeath", { detail: this })); + + /* Dead units can't be selected */ + this.setSelected(this.getSelected() && this.getBaseData().alive && !this.getHidden()) + + if (updateMarker) + this.#updateMarker(); + + this.#clearDetectedUnits(); + if (this.getSelected()) { + this.#drawPath(); + this.#drawDetectedUnits(); + this.#drawTarget(); + } + else { + this.#clearPath(); + this.#clearTarget(); + } + + + document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); + } + + getData() { + return this.#data; + } + + getBaseData() { + return this.getData().baseData; + } + + getFlightData() { + return this.getData().flightData; + } + + getTaskData() { + return this.getData().taskData; + } + + getMissionData() { + return this.getData().missionData; + } + + getFormationData() { + return this.getData().formationData; + } + + getOptionsData() { + return this.getData().optionsData; + } + + /********************** Icon *************************/ + createIcon(): void { + /* Set the icon */ + var icon = new DivIcon({ + className: 'leaflet-unit-icon', + iconAnchor: [25, 25], + iconSize: [50, 50], + }); + this.setIcon(icon); + + var el = document.createElement("div"); + el.classList.add("unit"); + el.setAttribute("data-object", `unit-${this.getMarkerCategory()}`); + el.setAttribute("data-coalition", this.getMissionData().coalition); + + // Generate and append elements depending on active options + // Velocity vector + if (this.getIconOptions().showVvi) { + var vvi = document.createElement("div"); + vvi.classList.add("unit-vvi"); + vvi.toggleAttribute("data-rotate-to-heading"); + el.append(vvi); + } + + // Hotgroup indicator + if (this.getIconOptions().showHotgroup) { + var hotgroup = document.createElement("div"); + hotgroup.classList.add("unit-hotgroup"); + var hotgroupId = document.createElement("div"); + hotgroupId.classList.add("unit-hotgroup-id"); + hotgroup.appendChild(hotgroupId); + el.append(hotgroup); + } + + // Main icon + if (this.getIconOptions().showUnitIcon) { + var unitIcon = document.createElement("div"); + unitIcon.classList.add("unit-icon"); + var img = document.createElement("img"); + img.src = `/resources/theme/images/units/${this.getMarkerCategory()}.svg`; + img.onload = () => SVGInjector(img); + unitIcon.appendChild(img); + unitIcon.toggleAttribute("data-rotate-to-heading", this.getIconOptions().rotateToHeading); + el.append(unitIcon); + } + + // State icon + if (this.getIconOptions().showState){ + var state = document.createElement("div"); + state.classList.add("unit-state"); + el.appendChild(state); + } + + // Short label + if (this.getIconOptions().showShortLabel) { + var shortLabel = document.createElement("div"); + shortLabel.classList.add("unit-short-label"); + shortLabel.innerText = this.getDatabase()?.getByName(this.getBaseData().name)?.shortLabel || ""; + el.append(shortLabel); + } + + // Fuel indicator + if (this.getIconOptions().showFuel) { + var fuelIndicator = document.createElement("div"); + fuelIndicator.classList.add("unit-fuel"); + var fuelLevel = document.createElement("div"); + fuelLevel.classList.add("unit-fuel-level"); + fuelIndicator.appendChild(fuelLevel); + el.append(fuelIndicator); + } + + // Ammo indicator + if (this.getIconOptions().showAmmo){ + var ammoIndicator = document.createElement("div"); + ammoIndicator.classList.add("unit-ammo"); + for (let i = 0; i <= 3; i++) + ammoIndicator.appendChild(document.createElement("div")); + el.append(ammoIndicator); + } + + // Unit summary + if (this.getIconOptions().showSummary) { + var summary = document.createElement("div"); + summary.classList.add("unit-summary"); + var callsign = document.createElement("div"); + callsign.classList.add("unit-callsign"); + callsign.innerText = this.getBaseData().unitName; + var altitude = document.createElement("div"); + altitude.classList.add("unit-altitude"); + var speed = document.createElement("div"); + speed.classList.add("unit-speed"); + summary.appendChild(callsign); + summary.appendChild(altitude); + summary.appendChild(speed); + el.appendChild(summary); + } + + this.getElement()?.appendChild(el); + } + + /********************** Visibility *************************/ + updateVisibility() { + var hidden = false; + const hiddenUnits = getUnitsManager().getHiddenTypes(); + if (this.getMissionData().flags.Human && hiddenUnits.includes("human")) + hidden = true; + else if (this.getBaseData().controlled == false && hiddenUnits.includes("dcs")) + hidden = true; + else if (hiddenUnits.includes(this.getMarkerCategory())) + hidden = true; + else if (hiddenUnits.includes(this.getMissionData().coalition)) + hidden = true; + this.setHidden(hidden || !this.getBaseData().alive); + } + + setHidden(hidden: boolean) { + this.#hidden = hidden; + + /* Add the marker if not present */ + if (!getMap().hasLayer(this) && !this.getHidden()) { + this.addTo(getMap()); + } + + /* Hide the marker if necessary*/ + if (getMap().hasLayer(this) && this.getHidden()) { + getMap().removeLayer(this); + } } getHidden() { - return false; + return this.#hidden; } getLeader() { - return getUnitsManager().getUnitByID(this.leaderID); + return getUnitsManager().getUnitByID(this.getFormationData().leaderID); } - getFormation() { - return [this].concat(this.getWingmen()) + canRole(roles: string | string[]) { + if (typeof(roles) === "string") + roles = [roles]; + + return this.getDatabase()?.getByName(this.getBaseData().name)?.loadouts.some((loadout: LoadoutBlueprint) => { + return (roles as string[]).some((role: string) => {return loadout.roles.includes(role)}); + }); } - getWingmen() { - var wingmen: Unit[] = []; - if (this.wingmenIDs != null) - { - for (let ID of this.wingmenIDs) - { - var unit = getUnitsManager().getUnitByID(ID) - if (unit) - wingmen.push(unit); + /********************** Unit commands *************************/ + addDestination(latlng: L.LatLng) { + if (!this.getMissionData().flags.Human) { + var path: any = {}; + if (this.getTaskData().activePath != undefined) { + path = this.getTaskData().activePath; + path[(Object.keys(path).length + 1).toString()] = latlng; + } + else { + path = { "1": latlng }; + } + addDestination(this.ID, path); + } + } + + clearDestinations() { + if (!this.getMissionData().flags.Human) + this.getTaskData().activePath = undefined; + } + + attackUnit(targetID: number) { + /* Units can't attack themselves */ + if (!this.getMissionData().flags.Human) + if (this.ID != targetID) + attackUnit(this.ID, targetID); + } + + followUnit(targetID: number, offset: { "x": number, "y": number, "z": number }) { + /* Units can't follow themselves */ + if (!this.getMissionData().flags.Human) + if (this.ID != targetID) + followUnit(this.ID, targetID, offset); + } + + landAt(latlng: LatLng) { + if (!this.getMissionData().flags.Human) + landAt(this.ID, latlng); + } + + changeSpeed(speedChange: string) { + if (!this.getMissionData().flags.Human) + changeSpeed(this.ID, speedChange); + } + + changeAltitude(altitudeChange: string) { + if (!this.getMissionData().flags.Human) + changeAltitude(this.ID, altitudeChange); + } + + setSpeed(speed: number) { + if (!this.getMissionData().flags.Human) + setSpeed(this.ID, speed); + } + + setSpeedType(speedType: string) { + if (!this.getMissionData().flags.Human) + setSpeedType(this.ID, speedType); + } + + setAltitude(altitude: number) { + if (!this.getMissionData().flags.Human) + setAltitude(this.ID, altitude); + } + + setAltitudeType(altitudeType: string) { + if (!this.getMissionData().flags.Human) + setAltitudeType(this.ID, altitudeType); + } + + setROE(ROE: string) { + if (!this.getMissionData().flags.Human) + setROE(this.ID, ROE); + } + + setReactionToThreat(reactionToThreat: string) { + if (!this.getMissionData().flags.Human) + setReactionToThreat(this.ID, reactionToThreat); + } + + setEmissionsCountermeasures(emissionCountermeasure: string) { + if (!this.getMissionData().flags.Human) + setEmissionsCountermeasures(this.ID, emissionCountermeasure); + } + + setLeader(isLeader: boolean, wingmenIDs: number[] = []) { + if (!this.getMissionData().flags.Human) + setLeader(this.ID, isLeader, wingmenIDs); + } + + setOnOff(onOff: boolean) { + if (!this.getMissionData().flags.Human) + setOnOff(this.ID, onOff); + } + + setFollowRoads(followRoads: boolean) { + if (!this.getMissionData().flags.Human) + setFollowRoads(this.ID, followRoads); + } + + delete(explosion: boolean) { + deleteUnit(this.ID, explosion); + } + + refuel() { + if (!this.getMissionData().flags.Human) + refuel(this.ID); + } + + setAdvancedOptions(isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { + if (!this.getMissionData().flags.Human) + setAdvacedOptions(this.ID, isTanker, isAWACS, TACAN, radio, generalSettings); + } + + bombPoint(latlng: LatLng) { + bombPoint(this.ID, latlng); + } + + carpetBomb(latlng: LatLng) { + carpetBomb(this.ID, latlng); + } + + bombBuilding(latlng: LatLng) { + bombBuilding(this.ID, latlng); + } + + fireAtArea(latlng: LatLng) { + fireAtArea(this.ID, latlng); + } + + /***********************************************/ + onAdd(map: Map): this { + super.onAdd(map); + getMap().removeTemporaryMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + return this; + } + + /***********************************************/ + #onClick(e: any) { + if (!this.#preventClick) { + if (getMap().getState() === IDLE || getMap().getState() === MOVE_UNIT || e.originalEvent.ctrlKey) { + if (!e.originalEvent.ctrlKey) + getUnitsManager().deselectAllUnits(); + this.setSelected(!this.getSelected()); } } - return wingmen; - } - #onClick(e: any) { - this.#timer = setTimeout(() => { - if (!this.#preventClick) { - if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { - if (!e.originalEvent.ctrlKey) { - getUnitsManager().deselectAllUnits(); - } - this.setSelected(true); - } - } + this.#timer = window.setTimeout(() => { this.#preventClick = false; }, 200); } @@ -184,66 +590,233 @@ export class Unit { } #onContextMenu(e: any) { - var options = [ - 'Attack', - 'Follow' - ] + var options: {[key: string]: {text: string, tooltip: string}} = {}; + const selectedUnits = getUnitsManager().getSelectedUnits(); + const selectedUnitTypes = getUnitsManager().getSelectedUnitsTypes(); - getMap().showSelectionScroll(e.originalEvent, "Action: " + this.unitName, options, (action: string) => this.#executeAction(action)); - } + options["center-map"] = {text: "Center map", tooltip: "Center the map on the unit and follow it"}; - #executeAction(action: string) { - getMap().hideSelectionScroll(); - if (action === "Attack") - getUnitsManager().selectedUnitsAttackUnit(this.ID); - } - - #updateMarker() { - /* Add the marker if not present */ - if (!getMap().hasLayer(this.#marker) && !this.getHidden()) { - this.#marker.addTo(getMap()); + if (selectedUnits.length > 0 && !(selectedUnits.length == 1 && (selectedUnits.includes(this)))) { + options["attack"] = {text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons"}; + if (getUnitsManager().getSelectedUnitsTypes().length == 1 && getUnitsManager().getSelectedUnitsTypes()[0] === "Aircraft") + options["follow"] = {text: "Follow", tooltip: "Follow the unit at a user defined distance and position"};; + } + else if ((selectedUnits.length > 0 && (selectedUnits.includes(this))) || selectedUnits.length == 0) { + if (this.getBaseData().category == "Aircraft") { + options["refuel"] = {text: "Air to air refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR + } } - /* Hide the marker if necessary*/ - if (getMap().hasLayer(this.#marker) && this.getHidden()) { - getMap().removeLayer(this.#marker); - } - else + if ((selectedUnits.length === 0 && this.getBaseData().category == "Aircraft") || (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0]))) { - this.#marker.setLatLng(new LatLng(this.latitude, this.longitude)); - this.#marker.draw({ - heading: this.heading, - speed: this.speed, - altitude: this.altitude, - alive: this.alive + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["CAS", "Strike"])})) { + options["bomb"] = {text: "Precision bombing", tooltip: "Precision bombing of a specific point"}; + options["carpet-bomb"] = {text: "Carpet bombing", tooltip: "Carpet bombing close to a point"}; + } + } + + if ((selectedUnits.length === 0 && this.getBaseData().category == "GroundUnit") || selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"])})) + options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at a large area"}; + } + + if (Object.keys(options).length > 0) { + getMap().showUnitContextMenu(e); + getMap().getUnitContextMenu().setOptions(options, (option: string) => { + getMap().hideUnitContextMenu(); + this.#executeAction(e, option); }); } } + #executeAction(e: any, action: string) { + if (action === "center-map") + getMap().centerOnUnit(this.ID); + if (action === "attack") + getUnitsManager().selectedUnitsAttackUnit(this.ID); + else if (action === "refuel") + getUnitsManager().selectedUnitsRefuel(); + else if (action === "follow") + this.#showFollowOptions(e); + else if (action === "bomb") + getMap().setState(BOMBING); + else if (action === "carpet-bomb") + getMap().setState(CARPET_BOMBING); + else if (action === "fire-at-area") + getMap().setState(FIRE_AT_AREA); + } + + #showFollowOptions(e: any) { + var options: {[key: string]: {text: string, tooltip: string}} = {}; + + options = { + 'trail': {text: "Trail", tooltip: "Follow unit in trail formation"}, + 'echelon-lh': {text: "Echelon (LH)", tooltip: "Follow unit in echelon left formation"}, + 'echelon-rh': {text: "Echelon (RH)", tooltip: "Follow unit in echelon right formation"}, + 'line-abreast-lh': {text: "Line abreast (LH)", tooltip: "Follow unit in line abreast left formation"}, + 'line-abreast-rh': {text: "Line abreast (RH)", tooltip: "Follow unit in line abreast right formation"}, + 'front': {text: "Front", tooltip: "Fly in front of unit"}, + 'diamond': {text: "Diamond", tooltip: "Follow unit in diamond formation"}, + 'custom': {text: "Custom", tooltip: "Set a custom formation position"}, + } + + getMap().getUnitContextMenu().setOptions(options, (option: string) => { + getMap().hideUnitContextMenu(); + this.#applyFollowOptions(option); + }); + getMap().showUnitContextMenu(e); + } + + #applyFollowOptions(action: string) { + if (action === "custom") { + document.getElementById("custom-formation-dialog")?.classList.remove("hide"); + getMap().getUnitContextMenu().setCustomFormationCallback((offset: { x: number, y: number, z: number }) => { + getUnitsManager().selectedUnitsFollowUnit(this.ID, offset); + }) + } + else { + getUnitsManager().selectedUnitsFollowUnit(this.ID, undefined, action); + } + } + + #updateMarker() { + this.updateVisibility(); + + /* Draw the minimap marker */ + if (this.getBaseData().alive) { + if (this.#miniMapMarker == null) { + this.#miniMapMarker = new CircleMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude), { radius: 0.5 }); + if (this.getMissionData().coalition == "neutral") + this.#miniMapMarker.setStyle({ color: "#CFD9E8" }); + else if (this.getMissionData().coalition == "red") + this.#miniMapMarker.setStyle({ color: "#ff5858" }); + else + this.#miniMapMarker.setStyle({ color: "#247be2" }); + this.#miniMapMarker.addTo(getMap().getMiniMapLayerGroup()); + this.#miniMapMarker.bringToBack(); + } + else { + this.#miniMapMarker.setLatLng(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + this.#miniMapMarker.bringToBack(); + } + } + else { + if (this.#miniMapMarker != null && getMap().getMiniMapLayerGroup().hasLayer(this.#miniMapMarker)) { + getMap().getMiniMapLayerGroup().removeLayer(this.#miniMapMarker); + this.#miniMapMarker = null; + } + } + + /* Draw the marker */ + if (!this.getHidden()) { + this.setLatLng(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + + var element = this.getElement(); + if (element != null) { + /* Draw the velocity vector */ + element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.getFlightData().speed / 5}px;`); + + /* Set fuel data */ + element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.getMissionData().fuel}%`); + element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.getMissionData().fuel < 20); + + /* Set dead/alive flag */ + element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.getBaseData().alive); + + /* Set current unit state */ + if (this.getMissionData().flags.Human) // Unit is human + element.querySelector(".unit")?.setAttribute("data-state", "human"); + else if (!this.getBaseData().controlled) // Unit is under DCS control (not Olympus) + element.querySelector(".unit")?.setAttribute("data-state", "dcs"); + else if ((this.getBaseData().category == "Aircraft" || this.getBaseData().category == "Helicopter") && !this.getMissionData().hasTask) + element.querySelector(".unit")?.setAttribute("data-state", "no-task"); + else // Unit is under Olympus control + element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase()); + + /* Set altitude and speed */ + if (element.querySelector(".unit-altitude")) + (element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(mToFt(this.getFlightData().altitude) / 100)); + if (element.querySelector(".unit-speed")) + (element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.getFlightData().speed))) + "GS"; + + /* Rotate elements according to heading */ + element.querySelectorAll("[data-rotate-to-heading]").forEach(el => { + const headingDeg = rad2deg(this.getFlightData().heading); + let currentStyle = el.getAttribute("style") || ""; + el.setAttribute("style", currentStyle + `transform:rotate(${headingDeg}deg);`); + }); + + /* Turn on ammo indicators */ + var hasFox1 = element.querySelector(".unit")?.hasAttribute("data-has-fox-1"); + var hasFox2 = element.querySelector(".unit")?.hasAttribute("data-has-fox-2"); + var hasFox3 = element.querySelector(".unit")?.hasAttribute("data-has-fox-3"); + var hasOtherAmmo = element.querySelector(".unit")?.hasAttribute("data-has-other-ammo"); + + var newHasFox1 = false; + var newHasFox2 = false; + var newHasFox3 = false; + var newHasOtherAmmo = false; + Object.values(this.getMissionData().ammo).forEach((ammo: any) => { + if (ammo.desc.category == 1 && ammo.desc.missileCategory == 1) { + if (ammo.desc.guidance == 4 || ammo.desc.guidance == 5) + newHasFox1 = true; + else if (ammo.desc.guidance == 2) + newHasFox2 = true; + else if (ammo.desc.guidance == 3) + newHasFox3 = true; + } + else + newHasOtherAmmo = true; + }); + + if (hasFox1 != newHasFox1) element.querySelector(".unit")?.toggleAttribute("data-has-fox-1", newHasFox1); + if (hasFox2 != newHasFox2) element.querySelector(".unit")?.toggleAttribute("data-has-fox-2", newHasFox2); + if (hasFox3 != newHasFox3) element.querySelector(".unit")?.toggleAttribute("data-has-fox-3", newHasFox3); + if (hasOtherAmmo != newHasOtherAmmo) element.querySelector(".unit")?.toggleAttribute("data-has-other-ammo", newHasOtherAmmo); + + /* Draw the hotgroup element */ + element.querySelector(".unit")?.toggleAttribute("data-is-in-hotgroup", this.#hotgroup != null); + if (this.#hotgroup) { + const hotgroupEl = element.querySelector(".unit-hotgroup-id") as HTMLElement; + if (hotgroupEl) + hotgroupEl.innerText = String(this.#hotgroup); + } + + } + + /* Set vertical offset for altitude stacking */ + var pos = getMap().latLngToLayerPoint(this.getLatLng()).round(); + this.setZIndexOffset(1000 + Math.floor(this.getFlightData().altitude) - pos.y + (this.#highlighted || this.#selected ? 5000 : 0)); + } + } + #drawPath() { - if (this.activePath != null) { + if (this.getTaskData().activePath != undefined) { var points = []; - points.push(new LatLng(this.latitude, this.longitude)); + points.push(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); /* Add markers if missing */ - while (this.#pathMarkers.length < Object.keys(this.activePath).length) { + while (this.#pathMarkers.length < Object.keys(this.getTaskData().activePath).length) { var marker = new Marker([0, 0], { icon: pathIcon }).addTo(getMap()); this.#pathMarkers.push(marker); } /* Remove markers if too many */ - while (this.#pathMarkers.length > Object.keys(this.activePath).length) { + while (this.#pathMarkers.length > Object.keys(this.getTaskData().activePath).length) { getMap().removeLayer(this.#pathMarkers[this.#pathMarkers.length - 1]); this.#pathMarkers.splice(this.#pathMarkers.length - 1, 1) } /* Update the position of the existing markers (to avoid creating markers uselessly) */ - for (let WP in this.activePath) { - var destination = this.activePath[WP]; + for (let WP in this.getTaskData().activePath) { + var destination = this.getTaskData().activePath[WP]; this.#pathMarkers[parseInt(WP) - 1].setLatLng([destination.lat, destination.lng]); points.push(new LatLng(destination.lat, destination.lng)); this.#pathPolyline.setLatLngs(points); } + + if (points.length == 1) + this.#clearPath(); } } @@ -255,221 +828,208 @@ export class Unit { this.#pathPolyline.setLatLngs([]); } - #drawTargets() - { - for (let typeIndex in this.targets) - { - for (let index in this.targets[typeIndex]) - { - var targetData = this.targets[typeIndex][index]; + #drawDetectedUnits() { + for (let index in this.getMissionData().contacts) { + var targetData = this.getMissionData().contacts[index]; + if (targetData.object != undefined){ var target = getUnitsManager().getUnitByID(targetData.object["id_"]) - if (target != null){ - var startLatLng = new LatLng(this.latitude, this.longitude) - var endLatLng = new LatLng(target.latitude, target.longitude) - + if (target != null) { + var startLatLng = new LatLng(this.getFlightData().latitude, this.getFlightData().longitude) + var endLatLng = new LatLng(target.getFlightData().latitude, target.getFlightData().longitude) + var color; - if (typeIndex === "radar") - { + if (targetData.detectionMethod === "RADAR") color = "#FFFF00"; - } - else if (typeIndex === "visual") - { + else if (targetData.detectionMethod === "VISUAL") color = "#FF00FF"; - } - else if (typeIndex === "rwr") - { + else if (targetData.detectionMethod === "RWR") color = "#00FF00"; - } else - { color = "#FFFFFF"; - } - var targetPolyline = new Polyline([startLatLng, endLatLng], {color: color, weight: 3, opacity: 1, smoothFactor: 1}); + var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1, dashArray: "4, 8" }); targetPolyline.addTo(getMap()); - this.#targetsPolylines.push(targetPolyline) + this.#contactsPolylines.push(targetPolyline) } } } } - #clearTargets() - { - for (let index in this.#targetsPolylines) - { - getMap().removeLayer(this.#targetsPolylines[index]) - } - } - - attackUnit(targetID: number) { - /* Call DCS attackUnit function */ - if (this.ID != targetID) { - attackUnit(this.ID, targetID); - } - else { - // TODO: show a message + #clearDetectedUnits() { + for (let index in this.#contactsPolylines) { + getMap().removeLayer(this.#contactsPolylines[index]) } } - landAt(latlng: LatLng) - { - landAt(this.ID, latlng); + #drawTarget() { + const targetLocation = this.getTaskData().targetLocation; + + if (targetLocation.latitude && targetLocation.longitude && targetLocation.latitude != 0 && targetLocation.longitude != 0) { + const lat = targetLocation.latitude; + const lng = targetLocation.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else if (this.getTaskData().targetID != 0 && getUnitsManager().getUnitByID(this.getTaskData().targetID)) { + const flightData = getUnitsManager().getUnitByID(this.getTaskData().targetID)?.getFlightData(); + const lat = flightData?.latitude; + const lng = flightData?.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else + this.#clearTarget(); } - changeSpeed(speedChange: string) - { - changeSpeed(this.ID, speedChange); + #drawTargetLocation(targetLocation: LatLng) { + if (!getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.addTo(getMap()); + if (!getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.addTo(getMap()); + this.#targetLocationMarker.setLatLng(new LatLng(targetLocation.lat, targetLocation.lng)); + this.#targetLocationPolyline.setLatLngs([new LatLng(this.getFlightData().latitude, this.getFlightData().longitude), new LatLng(targetLocation.lat, targetLocation.lng)]) + } + + #clearTarget() { + if (getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.removeFrom(getMap()); + + if (getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.removeFrom(getMap()); } - - changeAltitude(altitudeChange: string) - { - changeAltitude(this.ID, altitudeChange); - } - - setSpeed(speed: number) - { - setSpeed(this.ID, speed); - } - - setAltitude(altitude: number) - { - setAltitude(this.ID, altitude); - } - - setROE(ROE: string) - { - setROE(this.ID, ROE); - } - - setReactionToThreat(reactionToThreat: string) - { - setReactionToThreat(this.ID, reactionToThreat); - } - - /* - 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(isLeader: boolean, wingmenIDs: number[] = []) - { - setLeader(this.ID, isLeader, wingmenIDs); - } - } export class AirUnit extends Unit { - getHidden() { - if (this.alive) - { - if (this.flags.user && getVisibilitySettings().user === "hidden") - return true - else if (!this.flags.user && getVisibilitySettings().ai === "hidden") - return true - } - else - return getVisibilitySettings().dead === "hidden" - return false; + getIconOptions() { + return { + showState: true, + showVvi: true, + showHotgroup: true, + showUnitIcon: true, + showShortLabel: true, + showFuel: true, + showAmmo: true, + showSummary: true, + rotateToHeading: false + }; } } export class Aircraft extends AirUnit { - constructor(ID: number, options: MarkerOptions) { - var marker = new AircraftMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); + } + + getMarkerCategory() { + return "aircraft"; + } + + getDatabase(): UnitDatabase | null { + return aircraftDatabase; } } export class Helicopter extends AirUnit { - constructor(ID: number, options: MarkerOptions) { - var marker = new HelicopterMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); + } + + getMarkerCategory() { + return "helicopter"; } } export class GroundUnit extends Unit { - constructor(ID: number, options: MarkerOptions) { - var marker = new GroundUnitMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); } - getHidden() { - if (this.alive) - { - if (this.flags.user && getVisibilitySettings().user === "hidden") - return true - else if (!this.flags.user && getVisibilitySettings().ai === "hidden") - return true - } - else - return getVisibilitySettings().dead === "hidden" - return false; + getIconOptions() { + return { + showState: true, + showVvi: false, + showHotgroup: true, + showUnitIcon: true, + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: false, + rotateToHeading: false + }; + } + + getMarkerCategory() { + // TODO this is very messy + var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0]; + var markerCategory = (role === "SAM") ? "groundunit-sam" : "groundunit-other"; + return markerCategory; + } + + getDatabase(): UnitDatabase | null { + return groundUnitsDatabase; } } export class NavyUnit extends Unit { - constructor(ID: number, options: MarkerOptions) { - var marker = new NavyUnitMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); } - getHidden() { - if (this.alive) - { - if (this.flags.user && getVisibilitySettings().user === "hidden") - return true - else if (!this.flags.user && getVisibilitySettings().ai === "hidden") - return true - } - else - return getVisibilitySettings().dead === "hidden" - return false; + getIconOptions() { + return { + showState: true, + showVvi: false, + showHotgroup: true, + showUnitIcon: true, + showShortLabel: true, + showFuel: false, + showAmmo: false, + showSummary: false, + rotateToHeading: false + }; + } + + getMarkerCategory() { + return "navyunit"; } } export class Weapon extends Unit { - constructor(ID: number, marker: UnitMarker) - { - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); this.setSelectable(false); } - getHidden() { - if (this.alive) - { - if (!this.flags.user && getVisibilitySettings().weapon === "hidden") - return true - } - else - return getVisibilitySettings().dead === "hidden" - return false; + getIconOptions() { + return { + showState: false, + showVvi: false, + showHotgroup: false, + showUnitIcon: true, + showShortLabel: false, + showFuel: false, + showAmmo: false, + showSummary: false, + rotateToHeading: true + }; } } export class Missile extends Weapon { - constructor(ID: number, options: MarkerOptions) { - var marker = new WeaponMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); + } + + getMarkerCategory() { + return "missile"; } } export class Bomb extends Weapon { - constructor(ID: number, options: MarkerOptions) { - var marker = new WeaponMarker(options); - super(ID, marker); + constructor(ID: number, data: UnitData) { + super(ID, data); + } + + getMarkerCategory() { + return "bomb"; } } diff --git a/client/src/units/unitTypes.ts b/client/src/units/unitTypes.ts deleted file mode 100644 index fb0da5aa..00000000 --- a/client/src/units/unitTypes.ts +++ /dev/null @@ -1,276 +0,0 @@ - - -export var unitTypes: any = {}; -/* NAVY */ -unitTypes.navy = {}; -unitTypes.navy.blue = [ - "VINSON", - "PERRY", - "TICONDEROG" -] - -unitTypes.navy.red = [ - "ALBATROS", - "KUZNECOW", - "MOLNIYA", - "MOSCOW", - "NEUSTRASH", - "PIOTR", - "REZKY" -] - -unitTypes.navy.civil = [ - "ELNYA", - "Dry-cargo ship-2", - "Dry-cargo ship-1", - "ZWEZDNY" -] - -unitTypes.navy.submarine = [ - "KILO", - "SOM" -] - -unitTypes.navy.speedboat = [ - "speedboat" -] - -/* VEHICLES (GROUND) */ -unitTypes.vehicles = [] -unitTypes.vehicles.Howitzers = [ - "2B11 mortar", - "SAU Gvozdika", - "SAU Msta", - "SAU Akatsia", - "SAU 2-C9", - "M-109" -] - -unitTypes.vehicles.IFV = [ - "AAV7", - "BMD-1", - "BMP-1", - "BMP-2", - "BMP-3", - "Boman", - "BRDM-2", - "BTR-80", - "BTR_D", - "Bunker", - "Cobra", - "LAV-25", - "M1043 HMMWV Armament", - "M1045 HMMWV TOW", - "M1126 Stryker ICV", - "M-113", - "M1134 Stryker ATGM", - "M-2 Bradley", - "Marder", - "MCV-80", - "MTLB", - "Paratrooper RPG-16", - "Paratrooper AKS-74", - "Sandbox", - "Soldier AK", - "Infantry AK", - "Soldier M249", - "Soldier M4", - "Soldier M4 GRG", - "Soldier RPG", - "TPZ" -] - -unitTypes.vehicles.MLRS = [ - "Grad-URAL", - "Uragan_BM-27", - "Smerch", - "MLRS" -] - -unitTypes.vehicles.SAM = [ - "2S6 Tunguska", - "Kub 2P25 ln", - "5p73 s-125 ln", - "S-300PS 5P85C ln", - "S-300PS 5P85D ln", - "SA-11 Buk LN 9A310M1", - "Osa 9A33 ln", - "Tor 9A331", - "Strela-10M3", - "Strela-1 9P31", - "SA-11 Buk CC 9S470M1", - "SA-8 Osa LD 9T217", - "Patriot AMG", - "Patriot ECS", - "Gepard", - "Hawk pcp", - "SA-18 Igla manpad", - "SA-18 Igla comm", - "Igla manpad INS", - "SA-18 Igla-S manpad", - "SA-18 Igla-S comm", - "Vulcan", - "Hawk ln", - "M48 Chaparral", - "M6 Linebacker", - "Patriot ln", - "M1097 Avenger", - "Patriot EPP", - "Patriot cp", - "Roland ADS", - "S-300PS 54K6 cp", - "Stinger manpad GRG", - "Stinger manpad dsr", - "Stinger comm dsr", - "Stinger manpad", - "Stinger comm", - "ZSU-23-4 Shilka", - "ZU-23 Emplacement Closed", - "ZU-23 Emplacement", - "ZU-23 Closed Insurgent", - "Ural-375 ZU-23 Insurgent", - "ZU-23 Insurgent", - "Ural-375 ZU-23" -] - -unitTypes.vehicles.Radar = [ - "1L13 EWR", - "Kub 1S91 str", - "S-300PS 40B6M tr", - "S-300PS 40B6MD sr", - "55G6 EWR", - "S-300PS 64H6E sr", - "SA-11 Buk SR 9S18M1", - "Dog Ear radar", - "Hawk tr", - "Hawk sr", - "Patriot str", - "Hawk cwar", - "p-19 s-125 sr", - "Roland Radar", - "snr s-125 tr" -] - -unitTypes.vehicles.Structures = [ - "house1arm", - "house2arm", - "outpost_road", - "outpost", - "houseA_arm" -] - -unitTypes.vehicles.Tanks = [ - "Challenger2", - "Leclerc", - "Leopard1A3", - "Leopard-2", - "M-60", - "M1128 Stryker MGS", - "M-1 Abrams", - "T-55", - "T-72B", - "T-80UD", - "T-90" -] - -unitTypes.vehicles.Unarmed = [ - "Ural-4320 APA-5D", - "ATMZ-5", - "ATZ-10", - "GAZ-3307", - "GAZ-3308", - "GAZ-66", - "M978 HEMTT Tanker", - "HEMTT TFFT", - "IKARUS Bus", - "KAMAZ Truck", - "LAZ Bus", - "Hummer", - "M 818", - "MAZ-6303", - "Predator GCS", - "Predator TrojanSpirit", - "Suidae", - "Tigr_233036", - "Trolley bus", - "UAZ-469", - "Ural ATsP-6", - "Ural-375 PBU", - "Ural-375", - "Ural-4320-31", - "Ural-4320T", - "VAZ Car", - "ZiL-131 APA-80", - "SKP-11", - "ZIL-131 KUNG", - "ZIL-4331" -] - -/* AIRPLANES */ -unitTypes.air = {} - -unitTypes.air.CAP = [ - "F-4E", - "F/A-18C", - "MiG-29S", - "F-14A", - "Su-27", - "MiG-23MLD", - "Su-33", - "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", - "F-15E", -] - -unitTypes.air.strike = [ - "Tu-22M3", - "B-52H", - "F-111F", - "Tu-95MS", - "Su-24M", - "Tu-160", - "F-117A", - "B-1B", - "Tu-142", -] - -unitTypes.air.tanker = [ - "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", - "IL-76MD", -] \ No newline at end of file diff --git a/client/src/units/unitdatabase.ts b/client/src/units/unitdatabase.ts new file mode 100644 index 00000000..90d87863 --- /dev/null +++ b/client/src/units/unitdatabase.ts @@ -0,0 +1,71 @@ +export class UnitDatabase { + blueprints: { [key: string]: UnitBlueprint } = {}; + + constructor() { + + } + + /* Returns a list of all possible roles in a database */ + getRoles() { + var roles: string[] = []; + for (let unit in this.blueprints) { + for (let loadout of this.blueprints[unit].loadouts) { + for (let role of loadout.roles) { + if (role !== "" && !roles.includes(role)) + roles.push(role); + } + } + } + return roles; + } + + /* Gets a specific blueprint by name */ + getByName(name: string) { + if (name in this.blueprints) + return this.blueprints[name]; + return null; + } + + /* Gets a specific blueprint by label */ + getByLabel(label: string) { + for (let unit in this.blueprints) { + if (this.blueprints[unit].label === label) + return this.blueprints[unit]; + } + return null; + } + + /* Get all blueprints by role */ + getByRole(role: string) { + var units = []; + for (let unit in this.blueprints) { + for (let loadout of this.blueprints[unit].loadouts) { + if (loadout.roles.includes(role) || loadout.roles.includes(role.toLowerCase())) { + units.push(this.blueprints[unit]) + break; + } + } + } + return units; + } + + /* Get the names of all the loadouts for a specific unit and for a specific role */ + getLoadoutNamesByRole(name: string, role: string) { + var loadouts = []; + for (let loadout of this.blueprints[name].loadouts) { + if (loadout.roles.includes(role) || loadout.roles.includes("")) { + loadouts.push(loadout.name) + } + } + return loadouts; + } + + /* Get the loadout content from the unit name and loadout name */ + getLoadoutByName(name: string, loadoutName: string) { + for (let loadout of this.blueprints[name].loadouts) { + if (loadout.name === loadoutName) + return loadout; + } + return null; + } +} \ No newline at end of file diff --git a/client/src/units/unitmarker.ts b/client/src/units/unitmarker.ts deleted file mode 100644 index f2b27fad..00000000 --- a/client/src/units/unitmarker.ts +++ /dev/null @@ -1,276 +0,0 @@ -import * as L from 'leaflet' -import { Symbol } from 'milsymbol' -import { getVisibilitySettings } from '..' - -export interface MarkerOptions { - unitName: string - name: string - human: boolean - coalitionID: number - type: any -} - -export interface MarkerData { - heading: number - speed: number - altitude: number - alive: boolean -} - -export class UnitMarker extends L.Marker { - #unitName: string - #name: string - #human: boolean - #alive: boolean = true - #selected: boolean = false - - constructor(options: MarkerOptions) { - super(new L.LatLng(0, 0), { riseOnHover: true }); - this.#unitName = options.unitName; - this.#name = options.name; - this.#human = options.human; - - var symbol = new Symbol(this.#computeMarkerCode(options), { size: 25 }); - var img = symbol.asCanvas().toDataURL('image/png'); - - var coalition = ""; - if (options.coalitionID == 1) - coalition = "red" - else if (options.coalitionID == 2) - coalition = "blue" - else - coalition = "neutral" - - var icon = new L.DivIcon({ - html: ` - - - -
-
-
-
-
${this.#unitName}
-
-
-
${this.#name}
-
`, - className: 'unit-marker' - }); - this.setIcon(icon); - } - - onAdd(map: L.Map): this { - super.onAdd(map); - this.addEventListener('mouseover', function (e: any) { e.target?.setHovered(true); }); - this.addEventListener('mouseout', function (e: any) { e.target?.setHovered(false); }); - return this - } - - draw(data: MarkerData) { - this.#alive = data.alive; - var element = this.getElement(); - if (element != null) { - var nameDiv = element.querySelector("#name"); - var unitNameDiv = element.querySelector("#unitName"); - var container = element.querySelector("#container"); - var icon = element.querySelector("#icon"); - var altitudeDiv = element.querySelector("#altitude"); - var speedDiv = element.querySelector("#speed"); - - /* If visibility is full show all labels */ - nameDiv.style.display = ''; - unitNameDiv.style.display = ''; - altitudeDiv.style.display = ''; - speedDiv.style.display = ''; - - /* If visibility is partial shown only icon and unit name. If none, shown only icon. */ - if (this.getVisibility() === "partial" || this.getVisibility() === "none") - { - unitNameDiv.style.display = 'none'; - altitudeDiv.style.display = 'none'; - speedDiv.style.display = 'none'; - } - if (this.getVisibility() === "none") - nameDiv.style.display = 'none'; - - nameDiv.style.left = (-(nameDiv.offsetWidth - container.offsetWidth) / 2) + "px"; - unitNameDiv.style.left = (-(unitNameDiv.offsetWidth - container.offsetWidth) / 2) + "px"; - - icon.style.transform = "rotate(" + data.heading + "rad)"; - altitudeDiv.innerHTML = String(Math.round(data.altitude / 0.3048 / 100) / 10); - speedDiv.innerHTML = String(Math.round(data.speed * 1.94384)); - - if (!this.#alive) - { - this.getElement()?.querySelector("#icon")?.classList.add("unit-marker-dead"); - } - } - } - - setSelected(selected: boolean) { - this.#selected = selected; - this.getElement()?.querySelector("#icon")?.classList.remove("unit-marker-hovered"); - this.getElement()?.querySelector("#ring")?.classList.toggle("unit-marker-selected", selected); - this.getElement()?.querySelector("#background")?.classList.toggle("unit-marker-selected", selected); - } - - getSelected() { - return this.#selected; - } - - setHovered(hovered: boolean) { - this.getElement()?.querySelector("#icon")?.classList.toggle("unit-marker-hovered", hovered && this.#alive); - } - - getHuman() { - return this.#human; - } - - getAlive() { - return this.#alive; - } - - getVisibility() { - return "full"; - } - - #computeMarkerCode(options: MarkerOptions) { - var identity = "00"; - var set = "00"; - var entity = "00"; - var entityType = "00"; - var entitySubtype = "00"; - var sectorOneModifier = "00"; - var sectorTwoModifier = "00"; - - /* Identity */ - if (options.coalitionID == 1) - identity = "06" /* Hostile */ - else if (options.coalitionID == 2) - identity = "03" /* Friendly */ - else - identity = "04" /* Neutral */ - - /* Air */ - if (options.type.level1 == 1) { - set = "01" - entity = "11" - if (options.type.level2 == 1) - entityType = "01" - else if (options.type.level2 == 1) - entityType = "02" - - if (options.type.level3 == 1) - entitySubtype = "04"; - else if (options.type.level3 == 2) - entitySubtype = "05"; - else if (options.type.level3 == 3) - entitySubtype = "04"; - else if (options.type.level3 == 4) - entitySubtype = "02"; - else if (options.type.level3 == 5) - entitySubtype = "00"; - else if (options.type.level3 == 6) - entitySubtype = "00"; - } - - /* Ground */ - else if (options.type.level1 == 2) - { - set = "10" - entity = "12" - entityType = "05" - } - /* Navy */ - else if (options.type.level1 == 3) - set = "30" - /* Weapon */ - else if (options.type.level1 == 4) - { - set = "02" - entity = "11" - if (options.type.level3 == 7) - { - sectorOneModifier = "01" - sectorTwoModifier = "01" - } - else if (options.type.level3 == 8) - { - sectorOneModifier = "01" - sectorTwoModifier = "02" - } - else if (options.type.level3 == 34) - { - sectorOneModifier = "02" - sectorTwoModifier = "01" - } - else if (options.type.level3 == 11) - { - sectorOneModifier = "02" - sectorTwoModifier = "02" - } - } - - return `10${identity}${set}0000${entity}${entityType}${entitySubtype}${sectorOneModifier}${sectorTwoModifier}` - } -} - -export class AirUnitMarker extends UnitMarker { - getVisibility() { - if (this.getSelected()) - return "full"; - - if (this.getHuman()) - return getVisibilitySettings().user; - else if (!this.getAlive()) - return "none"; - else - return getVisibilitySettings().ai; - } -} - -export class AircraftMarker extends AirUnitMarker { -} - -export class HelicopterMarker extends AirUnitMarker { -} - -export class GroundUnitMarker extends UnitMarker { - /* Are user driven units recognized as human? */ - getVisibility() { - if (this.getSelected()) - return "full"; - - if (this.getHuman()) - return getVisibilitySettings().user; - else if (!this.getAlive()) - return "none"; - else - return getVisibilitySettings().ai; - } -} - -export class NavyUnitMarker extends UnitMarker { - getVisibility() { - if (this.getSelected()) - return "full"; - - if (!this.getAlive()) - return "none"; - else - return getVisibilitySettings().ai; - } -} - -export class WeaponMarker extends UnitMarker { - getVisibility() { - if (this.getSelected()) - return "full"; - - if (!this.getAlive()) - return "none"; - else - return getVisibilitySettings().weapon; - } -} \ No newline at end of file diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 1149661b..299c2e0a 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -1,11 +1,16 @@ import { LatLng, LatLngBounds } from "leaflet"; -import { getMap, getUnitControlPanel, getUnitInfoPanel } from ".."; -import { Unit, GroundUnit } from "./unit"; -import { cloneUnit } from "../dcs/dcs"; +import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from ".."; +import { Unit } from "./unit"; +import { cloneUnit } from "../server/server"; +import { deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots } from "../other/utils"; +import { IDLE, MOVE_UNIT } from "../map/map"; export class UnitsManager { #units: { [ID: number]: Unit }; #copiedUnits: Unit[]; + #selectionEventDisabled: boolean = false; + #pasteDisabled: boolean = false; + #hiddenTypes: string[] = []; constructor() { this.#units = {}; @@ -13,38 +18,28 @@ export class UnitsManager { document.addEventListener('copy', () => this.copyUnits()); document.addEventListener('paste', () => this.pasteUnits()); + document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); + document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); + document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); + document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); + document.addEventListener('keyup', (event) => this.#onKeyUp(event)); } - #updateUnitControlPanel() { - /* Update the unit control panel */ - if (this.getSelectedUnits().length > 0) { - getUnitControlPanel().show(); - getUnitControlPanel().update(this.getSelectedLeaders().concat(this.getSelectedSingletons())); - } - else { - getUnitControlPanel().hide(); - } + getSelectableAircraft() { + const units = this.getUnits(); + return Object.keys(units).reduce((acc: { [key: number]: Unit }, unitId: any) => { + const baseData = units[unitId].getBaseData(); + if (baseData.category === "Aircraft" && baseData.alive === true) { + acc[unitId] = units[unitId]; + } + return acc; + }, {}); } getUnits() { return this.#units; } - addUnit(ID: number, data: any) { - /* The name of the unit category is exactly the same as the constructor name */ - var constructor = Unit.getConstructor(data.category); - if (constructor != undefined) { - var options = { - unitName: data.unitName, - name: data.name, - human: data.human, - coalitionID: data.coalitionID, - type: data.type - } - this.#units[ID] = new constructor(ID, options); - } - } - getUnitByID(ID: number) { if (ID in this.#units) return this.#units[ID]; @@ -52,282 +47,496 @@ export class UnitsManager { return null; } + getUnitsByHotgroup(hotgroup: number) { + return Object.values(this.#units).filter((unit: Unit) => { return unit.getBaseData().alive && unit.getHotgroup() == hotgroup }); + } + + addUnit(ID: number, data: UnitData) { + if (data.baseData && data.baseData.category){ + /* The name of the unit category is exactly the same as the constructor name */ + var constructor = Unit.getConstructor(data.baseData.category); + if (constructor != undefined) { + this.#units[ID] = new constructor(ID, data); + } + } + } + removeUnit(ID: number) { } + update(data: UnitsData) { + var updatedUnits: Unit[] = []; + Object.keys(data.units) + .filter((ID: string) => !(ID in this.#units)) + .reduce((timeout: number, ID: string) => { + window.setTimeout(() => { + if (!(ID in this.#units)) + this.addUnit(parseInt(ID), data.units[ID]); + this.#units[parseInt(ID)]?.setData(data.units[ID]); + }, timeout); + return timeout + 10; + }, 10); + + Object.keys(data.units) + .filter((ID: string) => ID in this.#units) + .forEach((ID: string) => { + updatedUnits.push(this.#units[parseInt(ID)]); + this.#units[parseInt(ID)]?.setData(data.units[ID]) + }); + + this.getSelectedUnits().forEach((unit: Unit) => { + if (!updatedUnits.includes(unit)) + unit.setData({}) + }); + } + + setHiddenType(key: string, value: boolean) { + if (value) { + if (this.#hiddenTypes.includes(key)) + delete this.#hiddenTypes[this.#hiddenTypes.indexOf(key)]; + } + else + this.#hiddenTypes.push(key); + Object.values(this.getUnits()).forEach((unit: Unit) => unit.updateVisibility()); + } + + getHiddenTypes() { + return this.#hiddenTypes; + } + + selectUnit(ID: number, deselectAllUnits: boolean = true) { + if (deselectAllUnits) + this.getSelectedUnits().filter((unit: Unit) => unit.ID !== ID).forEach((unit: Unit) => unit.setSelected(false)); + this.#units[ID]?.setSelected(true); + } + + selectFromBounds(bounds: LatLngBounds) { + this.deselectAllUnits(); + for (let ID in this.#units) { + if (this.#units[ID].getHidden() == false) { + var latlng = new LatLng(this.#units[ID].getFlightData().latitude, this.#units[ID].getFlightData().longitude); + if (bounds.contains(latlng)) { + this.#units[ID].setSelected(true); + } + } + } + } + + getSelectedUnits(options?: { excludeHumans?: boolean, onlyOnePerGroup?: boolean }) { + var selectedUnits = []; + for (let ID in this.#units) { + if (this.#units[ID].getSelected()) { + selectedUnits.push(this.#units[ID]); + } + } + if (options) { + if (options.excludeHumans) + selectedUnits = selectedUnits.filter((unit: Unit) => { return !unit.getMissionData().flags.Human }); + if (options.onlyOnePerGroup) { + var temp: Unit[] = []; + for (let unit of selectedUnits) { + if (!temp.some((otherUnit: Unit) => unit.getBaseData().groupName == otherUnit.getBaseData().groupName)) + temp.push(unit); + } + selectedUnits = temp; + } + } + return selectedUnits; + } + deselectAllUnits() { for (let ID in this.#units) { this.#units[ID].setSelected(false); } } - selectUnit(ID: number, deselectAllUnits: boolean = true) - { - if (deselectAllUnits) - this.deselectAllUnits(); - this.#units[ID]?.setSelected(true); - } - - update(data: any) { - for (let ID in data["units"]) { - /* Create the unit if missing from the local array, then update the data. Drawing is handled by leaflet. */ - if (!(ID in this.#units)) { - this.addUnit(parseInt(ID), data["units"][ID]); - } - this.#units[parseInt(ID)].update(data["units"][ID]); - } - - /* Update the unit info panel */ - if (this.getSelectedUnits().length == 1) { - getUnitInfoPanel().show(); - getUnitInfoPanel().update(this.getSelectedUnits()[0]); - } - else { - getUnitInfoPanel().hide(); - } - } - - onUnitSelection() { - if (this.getSelectedUnits().length > 0) { - getMap().setState("MOVE_UNIT"); - //unitControlPanel.setEnabled(true); - } - else { - getMap().setState("IDLE"); - //unitControlPanel.setEnabled(false); - } - - this.#updateUnitControlPanel(); - } - - selectFromBounds(bounds: LatLngBounds) - { + selectUnitsByHotgroup(hotgroup: number) { this.deselectAllUnits(); - for (let ID in this.#units) - { - var latlng = new LatLng(this.#units[ID].latitude, this.#units[ID].longitude); - if (bounds.contains(latlng)) - { - this.#units[ID].setSelected(true); - } - } + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) } - getSelectedUnits() { - var selectedUnits = []; - for (let ID in this.#units) { - if (this.#units[ID].getSelected()) { - selectedUnits.push(this.#units[ID]); - } - } - return selectedUnits; - } + getSelectedUnitsTypes() { + if (this.getSelectedUnits().length == 0) + return []; + return this.getSelectedUnits().map((unit: Unit) => { + return unit.constructor.name + })?.filter((value: any, index: any, array: string[]) => { + return array.indexOf(value) === index; + }); + }; - getSelectedLeaders() { - var leaders: Unit[] = []; - for (let idx in this.getSelectedUnits()) - { - var unit = this.getSelectedUnits()[idx]; - if (unit.isLeader) - leaders.push(unit); - else if (unit.isWingman) - { - var leader = unit.getLeader(); - if (leader && !leaders.includes(leader)) - leaders.push(leader); - } - } - return leaders; - } + getSelectedUnitsVariable(variableGetter: CallableFunction) { + if (this.getSelectedUnits().length == 0) + return undefined; + return this.getSelectedUnits().map((unit: Unit) => { + return variableGetter(unit); + })?.reduce((a: any, b: any) => { + return a == b ? a : undefined + }); + }; - getSelectedSingletons() { - var singletons: Unit[] = []; - for (let idx in this.getSelectedUnits()) - { - var unit = this.getSelectedUnits()[idx]; - if (!unit.isLeader && !unit.isWingman) - singletons.push(unit); - } - return singletons; - } - selectedUnitsAddDestination(latlng: L.LatLng) { - var selectedUnits = this.getSelectedUnits(); + getSelectedUnitsCoalition() { + if (this.getSelectedUnits().length == 0) + return undefined; + return this.getSelectedUnits().map((unit: Unit) => { + return unit.getMissionData().coalition + })?.reduce((a: any, b: any) => { + return a == b ? a : undefined + }); + }; + + /*********************** Actions on selected units ************************/ + selectedUnitsAddDestination(latlng: L.LatLng, mantainRelativePosition: boolean, rotation: number) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + + /* Compute the destination for each unit. If mantainRelativePosition is true, compute the destination so to hold the relative distances */ + var unitDestinations: { [key: number]: LatLng } = {}; + if (mantainRelativePosition) + unitDestinations = this.selectedUnitsComputeGroupDestination(latlng, rotation); + else + selectedUnits.forEach((unit: Unit) => { unitDestinations[unit.ID] = latlng }); + for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - //if (selectedUnits[idx].wingman) - //{ - // commandedUnit = this.getLeader(selectedUnits[idx].ID); - //} - commandedUnit.addDestination(latlng); + const unit = selectedUnits[idx]; + /* If a unit is following another unit, and that unit is also selected, send the command to the followed unit */ + if (unit.getTaskData().currentState === "Follow") { + const leader = this.getUnitByID(unit.getFormationData().leaderID) + if (leader && leader.getSelected()) + leader.addDestination(latlng); + else + unit.addDestination(latlng); + } + else { + if (unit.ID in unitDestinations) + unit.addDestination(unitDestinations[unit.ID]); + } + } + this.#showActionMessage(selectedUnits, " new destination added"); } selectedUnitsClearDestinations() { - var selectedUnits = this.getSelectedUnits(); + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - //if (selectedUnits[idx].wingman) - //{ - // commandedUnit = this.getLeader(selectedUnits[idx].ID); - //} - commandedUnit.clearDestinations(); - } - } - - selectedUnitsLandAt(latlng: LatLng) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].landAt(latlng); - } - } - - selectedUnitsChangeSpeed(speedChange: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].changeSpeed(speedChange); - } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail - } - - selectedUnitsChangeAltitude(altitudeChange: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].changeAltitude(altitudeChange); - } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail - } - - selectedUnitsSetSpeed(speed: number) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].setSpeed(speed); - } - } - - selectedUnitsSetAltitude(altitude: number) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].setAltitude(altitude); - } - } - - selectedUnitsSetROE(ROE: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].setROE(ROE); - } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail - } - - selectedUnitsSetReactionToThreat(reactionToThreat: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { - selectedUnits[idx].setReactionToThreat(reactionToThreat); - } - - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail - } - - - copyUnits() - { - this.#copiedUnits = this.getSelectedUnits(); - } - - pasteUnits() - { - for (let idx in this.#copiedUnits) - { - var unit = this.#copiedUnits[idx]; - cloneUnit(unit.ID, getMap().getMouseCoordinates()); - } - } - - selectedUnitsAttackUnit(ID: number) { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) { - /* If a unit is a wingman, send the command to its leader */ - var commandedUnit = selectedUnits[idx]; - //if (selectedUnits[idx].wingman) - //{ - // commandedUnit = this.getLeader(selectedUnits[idx].ID); - //} - commandedUnit.attackUnit(ID); - } - } - - selectedUnitsCreateFormation(ID: number | null = null) - { - var selectedUnits = this.getSelectedUnits(); - if (selectedUnits.length >= 2) - { - if (ID == null) - ID = selectedUnits[0].ID - - var wingmenIDs = []; - for (let idx in selectedUnits) - { - if (selectedUnits[idx].isWingman) - { - console.log(selectedUnits[idx].unitName + " is already in a formation."); - return; - } - else if (selectedUnits[idx].isLeader) - { - console.log(selectedUnits[idx].unitName + " is already in a formation."); - return; - } + const unit = selectedUnits[idx]; + if (unit.getTaskData().currentState === "Follow") { + const leader = this.getUnitByID(unit.getFormationData().leaderID) + if (leader && leader.getSelected()) + leader.clearDestinations(); else - { - /* TODO - 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(true, wingmenIDs); + unit.clearDestinations(); } else - { - console.log("At least 2 units must be selected to create a formation."); - } + unit.clearDestinations(); } - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail } - selectedUnitsUndoFormation(ID: number | null = null) - { - for (let leader of this.getSelectedLeaders()) - { - leader.setLeader(false); + selectedUnitsLandAt(latlng: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].landAt(latlng); } - setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail + this.#showActionMessage(selectedUnits, " landing"); + } + + selectedUnitsChangeSpeed(speedChange: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].changeSpeed(speedChange); + } + } + + selectedUnitsChangeAltitude(altitudeChange: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].changeAltitude(altitudeChange); + } + } + + selectedUnitsSetSpeed(speed: number) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setSpeed(speed); + } + this.#showActionMessage(selectedUnits, `setting speed to ${msToKnots(speed)} kts`); + } + + selectedUnitsSetSpeedType(speedType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setSpeedType(speedType); + } + this.#showActionMessage(selectedUnits, `setting speed type to ${speedType}`); + } + + selectedUnitsSetAltitude(altitude: number) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setAltitude(altitude); + } + this.#showActionMessage(selectedUnits, `setting altitude to ${mToFt(altitude)} ft`); + } + + selectedUnitsSetAltitudeType(altitudeType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setAltitudeType(altitudeType); + } + this.#showActionMessage(selectedUnits, `setting altitude type to ${altitudeType}`); + } + + selectedUnitsSetROE(ROE: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setROE(ROE); + } + this.#showActionMessage(selectedUnits, `ROE set to ${ROE}`); + } + + selectedUnitsSetReactionToThreat(reactionToThreat: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setReactionToThreat(reactionToThreat); + } + this.#showActionMessage(selectedUnits, `reaction to threat set to ${reactionToThreat}`); + } + + selectedUnitsSetEmissionsCountermeasures(emissionCountermeasure: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure); + } + this.#showActionMessage(selectedUnits, `emissions & countermeasures set to ${emissionCountermeasure}`); + } + + selectedUnitsSetOnOff(onOff: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setOnOff(onOff); + } + this.#showActionMessage(selectedUnits, `unit active set to ${onOff}`); + } + + selectedUnitsSetFollowRoads(followRoads: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setFollowRoads(followRoads); + } + this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`); + } + + + selectedUnitsAttackUnit(ID: number) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].attackUnit(ID); + } + this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getBaseData().unitName}`); + } + + selectedUnitsDelete(explosion: boolean = false) { + var selectedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ + const selectionContainsAHuman = selectedUnits.some( ( unit:Unit ) => { + return unit.getMissionData().flags.Human === true; + }); + + if (selectionContainsAHuman && !confirm( "Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?" ) ) { + return; + } + + for (let idx in selectedUnits) { + selectedUnits[idx].delete(explosion); + } + this.#showActionMessage(selectedUnits, `deleted`); + } + + selectedUnitsRefuel() { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].refuel(); + } + this.#showActionMessage(selectedUnits, `sent to nearest tanker`); + } + + selectedUnitsFollowUnit(ID: number, offset?: { "x": number, "y": number, "z": number }, formation?: string) { + if (offset == undefined) { + /* Simple formations with fixed offsets */ + // X: front-rear, positive front + // Y: top-bottom, positive top + // Z: left-right, positive right + offset = { "x": 0, "y": 0, "z": 0 }; + if (formation === "trail") { offset.x = -50; offset.y = -30; offset.z = 0; } + else if (formation === "echelon-lh") { offset.x = -50; offset.y = -10; offset.z = -50; } + else if (formation === "echelon-rh") { offset.x = -50; offset.y = -10; offset.z = 50; } + else if (formation === "line-abreast-rh") { offset.x = 0; offset.y = 0; offset.z = 50; } + else if (formation === "line-abreast-lh") { offset.x = 0; offset.y = 0; offset.z = -50; } + else if (formation === "front") { offset.x = 100; offset.y = 0; offset.z = 0; } + else offset = undefined; + } + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + var count = 1; + var xr = 0; var yr = 1; var zr = -1; + var layer = 1; + for (let idx in selectedUnits) { + var unit = selectedUnits[idx]; + if (offset != undefined) + /* Offset is set, apply it */ + unit.followUnit(ID, { "x": offset.x * count, "y": offset.y * count, "z": offset.z * count }); + else { + /* More complex formations with variable offsets */ + if (formation === "diamond") { + var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4); + var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4); + unit.followUnit(ID, { "x": -yl * 50, "y": zr * 10, "z": xl * 50 }); + + if (yr == 0) { layer++; xr = 0; yr = layer; zr = -layer; } + else { + if (xr < layer) { xr++; zr--; } + else { yr--; zr++; } + } + } + } + count++; + } + this.#showActionMessage(selectedUnits, `following unit ${this.getUnitByID(ID)?.getBaseData().unitName}`); + } + + selectedUnitsSetHotgroup(hotgroup: number) { + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setHotgroup(null)); + this.selectedUnitsAddToHotgroup(hotgroup); + } + + selectedUnitsAddToHotgroup(hotgroup: number) { + var selectedUnits = this.getSelectedUnits(); + for (let idx in selectedUnits) { + selectedUnits[idx].setHotgroup(hotgroup); + } + this.#showActionMessage(selectedUnits, `added to hotgroup ${hotgroup}`); + getHotgroupPanel().refreshHotgroups(); + } + + selectedUnitsComputeGroupDestination(latlng: LatLng, rotation: number) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + /* Compute the center of the group */ + var center = { x: 0, y: 0 }; + selectedUnits.forEach((unit: Unit) => { + var mercator = latLngToMercator(unit.getFlightData().latitude, unit.getFlightData().longitude); + center.x += mercator.x / selectedUnits.length; + center.y += mercator.y / selectedUnits.length; + }); + + /* Compute the distances from the center of the group */ + var unitDestinations: { [key: number]: LatLng } = {}; + selectedUnits.forEach((unit: Unit) => { + var mercator = latLngToMercator(unit.getFlightData().latitude, unit.getFlightData().longitude); + var distancesFromCenter = { dx: mercator.x - center.x, dy: mercator.y - center.y }; + + /* Rotate the distance according to the group rotation */ + var rotatedDistancesFromCenter: { dx: number, dy: number } = { dx: 0, dy: 0 }; + rotatedDistancesFromCenter.dx = distancesFromCenter.dx * Math.cos(deg2rad(rotation)) - distancesFromCenter.dy * Math.sin(deg2rad(rotation)); + rotatedDistancesFromCenter.dy = distancesFromCenter.dx * Math.sin(deg2rad(rotation)) + distancesFromCenter.dy * Math.cos(deg2rad(rotation)); + + /* Compute the final position of the unit */ + var destMercator = latLngToMercator(latlng.lat, latlng.lng); // Convert destination point to mercator + var unitMercator = { x: destMercator.x + rotatedDistancesFromCenter.dx, y: destMercator.y + rotatedDistancesFromCenter.dy }; // Compute final position of this unit in mercator coordinates + var unitLatLng = mercatorToLatLng(unitMercator.x, unitMercator.y); + unitDestinations[unit.ID] = new LatLng(unitLatLng.lat, unitLatLng.lng); + }); + + return unitDestinations; + } + + selectedUnitsBombPoint(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombPoint(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsCarpetBomb(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].carpetBomb(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsBombBuilding(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombBuilding(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsFireAtArea(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true, onlyOnePerGroup: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].fireAtArea(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + /***********************************************/ + copyUnits() { + this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ + this.#showActionMessage(this.#copiedUnits, `copied`); + } + + pasteUnits() { + if (!this.#pasteDisabled) { + for (let idx in this.#copiedUnits) { + var unit = this.#copiedUnits[idx]; + getMap().addTemporaryMarker(getMap().getMouseCoordinates()); + cloneUnit(unit.ID, getMap().getMouseCoordinates()); + this.#showActionMessage(this.#copiedUnits, `pasted`); + } + this.#pasteDisabled = true; + window.setTimeout(() => this.#pasteDisabled = false, 250); + } + } + + /***********************************************/ + #onKeyUp(event: KeyboardEvent) { + if (!keyEventWasInInput(event) && event.key === "Delete" ) { + this.selectedUnitsDelete(); + } + } + + #onUnitSelection(unit: Unit) { + if (this.getSelectedUnits().length > 0) { + getMap().setState(MOVE_UNIT); + /* Disable the firing of the selection event for a certain amount of time. This avoids firing many events if many units are selected */ + if (!this.#selectionEventDisabled) { + window.setTimeout(() => { + document.dispatchEvent(new CustomEvent("unitsSelection", { detail: this.getSelectedUnits() })); + this.#selectionEventDisabled = false; + }, 100); + this.#selectionEventDisabled = true; + } + } + else { + getMap().setState(IDLE); + document.dispatchEvent(new CustomEvent("clearSelection")); + } + } + + #onUnitDeselection(unit: Unit) { + if (this.getSelectedUnits().length == 0) { + getMap().setState(IDLE); + document.dispatchEvent(new CustomEvent("clearSelection")); + } + else + document.dispatchEvent(new CustomEvent("unitsDeselection", { detail: this.getSelectedUnits() })); + } + + #showActionMessage(units: Unit[], message: string) { + if (units.length == 1) + getInfoPopup().setText(`${units[0].getBaseData().unitName} ${message}`); + else if (units.length > 1) + getInfoPopup().setText(`${units[0].getBaseData().unitName} and ${units.length - 1} other units ${message}`); } } \ No newline at end of file diff --git a/client/tsconfig.json b/client/tsconfig.json index 74c81cdf..c293f5f2 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "ES2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ @@ -33,14 +33,16 @@ ], /* Specify multiple folders that act like './node_modules/@types'. */ "types": [ "leaflet", - "geojson" + "geojson", + "node", + "formatcoords" ], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ diff --git a/client/views/aic/aic.ejs b/client/views/aic/aic.ejs new file mode 100644 index 00000000..229a5a82 --- /dev/null +++ b/client/views/aic/aic.ejs @@ -0,0 +1,52 @@ +
+
+
+
+ +
+
×
+
AIC Help
+
+

How to be a good AIC and get people to do stuff good, too.

+
+
[DCS with Volvo video]
+
+
+
+ +
+ +
+ +
+

My callsign

+
Magic
+
+ +
+ + +
+ +
+

Control

+
+ + +
+
+ + +
+
+ +
+ +

Formations

+ +
+ +
+ +
\ No newline at end of file diff --git a/client/views/atc/addflight.ejs b/client/views/atc/addflight.ejs new file mode 100644 index 00000000..97e1ae5b --- /dev/null +++ b/client/views/atc/addflight.ejs @@ -0,0 +1,5 @@ +
+
+ + +
\ No newline at end of file diff --git a/client/views/atc/atc.ejs b/client/views/atc/atc.ejs new file mode 100644 index 00000000..65984a3a --- /dev/null +++ b/client/views/atc/atc.ejs @@ -0,0 +1,11 @@ +<%- include('board.ejs', { + "boardId": "strip-board-tower", + "boardType": "tower", + "headers": [ "Flight", "a. Alt", "alt", "a. Speed", "Speed" ] +}) %> + +<%- include('board.ejs', { + "boardId": "strip-board-ground", + "boardType": "ground", + "headers": [ "Flight", "Status", "T/O Time", "TTG" ] +}) %> \ No newline at end of file diff --git a/client/views/atc/board.ejs b/client/views/atc/board.ejs new file mode 100644 index 00000000..58d3dfcc --- /dev/null +++ b/client/views/atc/board.ejs @@ -0,0 +1,22 @@ +
+ +
+ +
+

<%= boardType %>

+ <%- include('addflight.ejs') %> +
+
+ +
+
+
+ <% headers.forEach( header => { %> +
<%= header %>
+ <% }); %> +
+
+
+
+ +
\ No newline at end of file diff --git a/client/views/atc/unitdatatable.ejs b/client/views/atc/unitdatatable.ejs new file mode 100644 index 00000000..0c090652 --- /dev/null +++ b/client/views/atc/unitdatatable.ejs @@ -0,0 +1,13 @@ +
+ +
+ +
+

Unit list

+
+ +
+ +
+ +
\ No newline at end of file diff --git a/client/views/connectionstatuspanel.ejs b/client/views/connectionstatuspanel.ejs deleted file mode 100644 index 47f4636f..00000000 --- a/client/views/connectionstatuspanel.ejs +++ /dev/null @@ -1,3 +0,0 @@ -
-
Connected
-
\ No newline at end of file diff --git a/client/views/index.ejs b/client/views/index.ejs index dd19f256..558d6b67 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -2,24 +2,37 @@ Olympus client - - + + + + + - +
- <%- include('selectionwheel.ejs') %> - <%- include('selectionscroll.ejs') %> -
-
- <%- include('unitinfopanel.ejs') %> - <%- include('unitcontrolpanel.ejs') %> - <%- include('visibilitycontrolpanel.ejs') %> - <%- include('connectionstatuspanel.ejs') %> - <%- include('mouseinfopanel.ejs') %> + + <%- include('aic/aic.ejs') %> + + <%- include('atc/atc.ejs') %> + <%- include('atc/unitdatatable.ejs') %> + + <%- include('panels/unitcontrol.ejs') %> + <%- include('panels/unitinfo.ejs') %> + <%- include('panels/mouseinfo.ejs') %> + <%- include('panels/connectionstatus.ejs') %> + <%- include('panels/hotgroup.ejs') %> + <%- include('panels/navbar.ejs') %> + + <%- include('other/dialogs.ejs') %> + <%- include('other/popups.ejs') %> + <%- include('other/contextmenus.ejs') %> + +
+ diff --git a/client/views/log/log.ejs b/client/views/log/log.ejs new file mode 100644 index 00000000..d92ecd2d --- /dev/null +++ b/client/views/log/log.ejs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/client/views/mouseinfopanel.ejs b/client/views/mouseinfopanel.ejs deleted file mode 100644 index 4e6bac2c..00000000 --- a/client/views/mouseinfopanel.ejs +++ /dev/null @@ -1,6 +0,0 @@ -
-
---° / --- NM
-
---° / --- NM
-
---° / --- NM
-
---° / --- NM
-
\ No newline at end of file diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs new file mode 100644 index 00000000..0940f076 --- /dev/null +++ b/client/views/other/contextmenus.ejs @@ -0,0 +1,112 @@ +
+
+
+
+ + + + +
+
+
+
+
Aircraft role
+
+ +
+
+
+
+
+
Aircraft type
+
+
Select role first
+ +
+
+
+
+
+
Loadout
+
+
Select type first
+ +
+
+
+
+
+
Spawn altitude +
+
+
+
+
+ + +
+
+ +
+
+
+ +
+
+
+
+
Ground unit role
+
+ +
+
+
+
+
+
Ground unit type
+
+
Select role first
+ +
+
+
+ +
+
+ + + + + +
+
+ + + + +
+
+ +
+ +
+ +
+ +

+ +
+ +
+ +

Parking available:

+
+ + + + +
\ No newline at end of file diff --git a/client/views/other/dialogs.ejs b/client/views/other/dialogs.ejs new file mode 100644 index 00000000..bbf5badb --- /dev/null +++ b/client/views/other/dialogs.ejs @@ -0,0 +1,234 @@ +
+
+
+

DCS Olympus

+

Dynamic Unit Command

+
Version v0.3.0
+
+ +
+
Username
+
Password
+ +
+ +

+ + +
+
+ +
+
+ +
+

Olympus 1-1

+
+ +
+ + +
+
+

General settings

+
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + +
+
+

Tasking

+
+
+ +
+ +
+ +
+ +
+
+ + +
+
+

TACAN options

+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+
X
+
+
+
+ +
+ +
+
+
+
+ + +
+
+

Radio options

+
+
+ +
+ + +
+
+ +
+ +
+
.000
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+
+
+
+ + + +
+ + +
+
+ +
+

Custom formation

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+ +
+ +
+
+
+ + +
\ No newline at end of file diff --git a/client/views/other/popups.ejs b/client/views/other/popups.ejs new file mode 100644 index 00000000..2ec87347 --- /dev/null +++ b/client/views/other/popups.ejs @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/client/views/panels/connectionstatus.ejs b/client/views/panels/connectionstatus.ejs new file mode 100644 index 00000000..ac92da78 --- /dev/null +++ b/client/views/panels/connectionstatus.ejs @@ -0,0 +1,6 @@ +
+
+
+
+
+
\ No newline at end of file diff --git a/client/views/panels/hotgroup.ejs b/client/views/panels/hotgroup.ejs new file mode 100644 index 00000000..0838c635 --- /dev/null +++ b/client/views/panels/hotgroup.ejs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/client/views/panels/mouseinfo.ejs b/client/views/panels/mouseinfo.ejs new file mode 100644 index 00000000..c4434cc6 --- /dev/null +++ b/client/views/panels/mouseinfo.ejs @@ -0,0 +1,34 @@ +
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/client/views/panels/navbar.ejs b/client/views/panels/navbar.ejs new file mode 100644 index 00000000..922fe414 --- /dev/null +++ b/client/views/panels/navbar.ejs @@ -0,0 +1,66 @@ + \ No newline at end of file diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs new file mode 100644 index 00000000..22fffd52 --- /dev/null +++ b/client/views/panels/unitcontrol.ejs @@ -0,0 +1,83 @@ +
+ +

Selected Units

+ +
+ +
+ + +
+
+ +
+ +
+

Controls

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

Rules of engagement

+
+ +
+
+ +
+

Reaction to threat

+
+ +
+
+ +
+

Radar & ECM

+
+ +
+
+ +
+

Unit active

+
+
Toggling this disables unit AI completely. It will no longer move, react or emit radio waves.
+
+ +
+

Follow roads

+
+
+ +
+ +
+ + + +
+ +
\ No newline at end of file diff --git a/client/views/panels/unitinfo.ejs b/client/views/panels/unitinfo.ejs new file mode 100644 index 00000000..3c03ca9c --- /dev/null +++ b/client/views/panels/unitinfo.ejs @@ -0,0 +1,31 @@ +
+ +
+ +
+

+
+
+
+
+
+
+ +
+ +
+ +
+
+
+ +
+
+
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/client/views/selectionscroll.ejs b/client/views/selectionscroll.ejs deleted file mode 100644 index 3a78b420..00000000 --- a/client/views/selectionscroll.ejs +++ /dev/null @@ -1,11 +0,0 @@ -
-
-
- -
-
-
-
\ No newline at end of file diff --git a/client/views/selectionwheel.ejs b/client/views/selectionwheel.ejs deleted file mode 100644 index f8a149c6..00000000 --- a/client/views/selectionwheel.ejs +++ /dev/null @@ -1,8 +0,0 @@ -
-
-
- -
\ No newline at end of file diff --git a/client/views/uikit/uikit.ejs b/client/views/uikit/uikit.ejs new file mode 100644 index 00000000..e4c0a2f4 --- /dev/null +++ b/client/views/uikit/uikit.ejs @@ -0,0 +1,1313 @@ + + + + Olympus UI Kit + + + + + + + +
+ +

Olympus UI Kit

+ +
+
Typeography
+
Navbar
+
Context menu
+
Unit control panel
+
Mouse info panel
+
Buttons
+
Ground Units
+
Planes
+
Weapons
+
.ol-panel
+
Icons
+
+ +
+ +
+ +
Headings
+
+ +
+

h1 | open sans | 32px

+

h2 | open sans | 24px

+

h3 | open sans | 18.72px

+

h4 | open sans | 16px

+
h5 | open sans | 13.28px
+
h6 | open sans | 10.72px
+
+ +
+ +
+ +
+ +
Paragraph
+
+ +
+
Plain
+

Nullam iaculis nisi sed mi tincidunt pretium blandit tempus urna. Vestibulum non ex vitae massa tristique auctor. Praesent orci justo, porttitor pellentesque convallis non, commodo at augue.

+
+ +
+
In a panel
+
+

Donec nibh est, fringilla sed pharetra eu, varius vel sem. Aliquam ac libero leo. Sed consectetur enim aliquam dui pellentesque luctus. Pellentesque vel iaculis quam.

+
+
+ +
+ +
+ +
+ + +
+ +
+ +
Primary nav
+
+ +
+ <%- include('navbar.ejs') %> +
+ +
+ +
+ +
+ +
+ +
+ +
Context menu
+
+
+ <%- include('contextmenus.ejs') %> +
+
+
+ +
+ +
+ +
+ +
Unit Control Panel
+
+ +
+ <%- include('unitcontrolpanel.ejs') %> +
+ +
+ +
+ +
+ +
+ +
+ +
Primary nav
+
+ +
+ <%- include('mouseinfopanel.ejs') %> +
+ +
+ +
+ +
+ + +
+ +
+ +
Buttons
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
Ground
+
+ +
+ +
Neutral
+ +
+
+
+
Z
+
+ +
+ +
+ +
Blue
+ +
+
+
+
Y
+
+ +
+ +
+ +
Red
+ +
+
+
+
X
+
+ +
+ +
+ +
+ + +
+ +
SAM
+
+ +
+ +
Neutral
+ +
+
+
+
Z
+
+ +
+ +
+ +
Blue
+ +
+
+
+
Y
+
+ +
+ +
+ +
Red
+ +
+
+
+
X
+
+ +
+ +
+ +
+ +
+ +
navyunit
+
+ +
+ +
Neutral
+ +
+
+
+
Z
+
+ +
+ +
+ +
Blue
+ +
+
+
+
Y
+
+ +
+ +
+ +
Red
+ +
+
+
+
X
+
+ +
+ +
+ +
+ +
+ +
Buildings
+
+ +
+ +
Neutral
+ +
+
+
+
J
+
+ +
+ +
+ +
Blue
+ +
+
+
+
K
+
+ +
+ +
+ +
Red
+ +
+
+
+
L
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
Fuel states (AI only)
+
+ +
+ +
0% (empty)
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
10%
+ +
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
20%
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
50%
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
75%
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
100%
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+ +
+ +
+ +
+ +
+ +
Status icons
+
+ +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+
+ +
+ +
Dead
+
+ +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ +
+ +
+
+
+
+
+
4
+
+
+
18
+
+
+
+
+
+
+
+
+
+
+
Springfield 3-1 | Longname
+
260
+
31
+
+
+ +
+ + +
+
+ +
+ +
+ +
+ +
Missile
+ +
+ +
+ +
+
+
+ +
+ + +
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
Plain panel
+ +
+ +
+ +
+ Disconnected +
+ +
+ +
+ +
+ +
+ +
Panel list
+ +
+ +
+ +
Basic list
+ +
+
+
List item 1
+
List item 2
+
List item 3
+
+
+ +
+ +
+ +
List with .highlight-primary
+ +
+
+
List item with highlight-primary
+
List item with highlight-bluefor
+
List item with highlight-redfor
+
List item with highlight-neutral
+
+
+ +
+ +
+ +
Sortable list
+ +
+
+
+
+
List item 1
+
+
+
+
List item 2
+
+
+
+
List item 3
+
+
+ +
+ +
+ + + +
+ +
+ +
+ +
Panel board
+ +
+ +
+ +
+
+
+

Unit Callsign

+
Airframe
+
Group
+
+
+

Flight data

+
+
+

Loadout

+
+
+
+ +
+ + +
+ +
+ +
+ +
Button group
+ +
+ +
+ +
+
+ + + +
+
+ +
+ + +
+ +
+ + +
+
ol select
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+ +
+ +
+
+ The selected value goes here +
+
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ Options go up +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ + +
+
Airfield menu
+
+
+ +
+

Al Alhambra

+
+
Runway 1
+
31 / 13
+
Runway 2
+
27 / 09
+
TCN
+
19X
+
ILS
+
-
+
+
+

Parking available:

+
+
Shelters
+
2
+
Open air
+
5
+
+ + +
+ +
+
+
+ + +
+
Airfield menu
+
+
+ +
+ +
+ +
+

Olympus 1-1

+
+
Name
+
AI Controlled
+
+
+
+ +
+ +
+
+
+ Empty loadout +
+
+ + +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+

Olympus 1-1

+
+
Name
+
AI Controlled
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
Icons
+
+ +
+
Actions
+
+
+ + icons_actions_gas +
+
+ + icons_actions_nothing +
+
+ + icons_actions_rtb +
+
+ + icons_actions_search +
+
+
+ +
+
RoE
+
+
+ + icons_roe_free +
+
+ + icons_roe_return +
+
+ + icons_roe_stop +
+
+ + icons_roe_target +
+
+
+ +
+
Threat
+
+
+ + icons_threat_protect +
+
+ + icons_threat_retreat +
+
+
+ +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/client/views/unitcontrolpanel.ejs b/client/views/unitcontrolpanel.ejs deleted file mode 100644 index fba42229..00000000 --- a/client/views/unitcontrolpanel.ejs +++ /dev/null @@ -1,65 +0,0 @@ -
-
-
-
-
-
-
- -
Selected units
- -
- -
- -
-
Create formation
-
Undo formation
-
-
- -
Controls
-
-
-
Altitude
-
- -
-
-
Speed
-
- -
-
- -
Formation
-
-
Echelon
-
Fingertip
-
Trail
-
Line abreast
-
- -
- -
Rules of engagement
-
-
Free
-
Designated free
-
Designated
-
Return
-
Hold
-
- -
- -
Reaction to threat
-
-
None
-
Passive
-
Evade
-
Escape
-
Abort
-
- -
\ No newline at end of file diff --git a/client/views/unitinfopanel.ejs b/client/views/unitinfopanel.ejs deleted file mode 100644 index 03ffd10f..00000000 --- a/client/views/unitinfopanel.ejs +++ /dev/null @@ -1,30 +0,0 @@ -
-
-
-
-
-
-
-
-
-
Flight data
-
-
- -
Ground Speed
-
- -
Altitude
-
- -
Heading
-
-
-
-
-
Loadout
- -
Fuel
-
-
-
\ No newline at end of file diff --git a/client/views/visibilitycontrolpanel.ejs b/client/views/visibilitycontrolpanel.ejs deleted file mode 100644 index e8caf556..00000000 --- a/client/views/visibilitycontrolpanel.ejs +++ /dev/null @@ -1,6 +0,0 @@ -
-
-
-
-
-
\ No newline at end of file diff --git a/img/OlympusLogoFinal_4k.png b/img/OlympusLogoFinal_4k.png new file mode 100644 index 00000000..b59050a5 Binary files /dev/null and b/img/OlympusLogoFinal_4k.png differ diff --git a/img/OlympusLogoFinal_Flat_4k.png b/img/OlympusLogoFinal_Flat_4k.png new file mode 100644 index 00000000..7b5df01e Binary files /dev/null and b/img/OlympusLogoFinal_Flat_4k.png differ diff --git a/img/icon.png b/img/icon.png new file mode 100644 index 00000000..dc5c994e Binary files /dev/null and b/img/icon.png differ diff --git a/img/icon_284.png b/img/icon_284.png new file mode 100644 index 00000000..b11172d3 Binary files /dev/null and b/img/icon_284.png differ diff --git a/img/icon_active.png b/img/icon_active.png new file mode 100644 index 00000000..56f59ba4 Binary files /dev/null and b/img/icon_active.png differ diff --git a/img/icon_select.png b/img/icon_select.png new file mode 100644 index 00000000..5771d3b5 Binary files /dev/null and b/img/icon_select.png differ diff --git a/img/olympus.ico b/img/olympus.ico new file mode 100644 index 00000000..d3567a53 Binary files /dev/null and b/img/olympus.ico differ diff --git a/installer/DCSOlympus.iss b/installer/olympus.iss similarity index 69% rename from installer/DCSOlympus.iss rename to installer/olympus.iss index 61c67383..55664142 100644 --- a/installer/DCSOlympus.iss +++ b/installer/olympus.iss @@ -1,25 +1,30 @@ #define nwjsFolder "C:\Users\dpass\Documents\nwjs\" +#define version "v0.3.0-alpha" [Setup] AppName=DCS Olympus -AppVerName=DCS Olympus Alpha v0.0.5 +AppVerName=DCS Olympus {#version} DefaultDirName={usersavedgames}\DCS.openbeta DefaultGroupName=DCSOlympus -OutputBaseFilename=DCSOlympus +OutputBaseFilename=DCSOlympus_{#version} +UninstallFilesDir={app}\Mods\Services\Olympus +;SetupIconFile="..\img\olympus.ico" [Tasks] ; NOTE: The following entry contains English phrases ("Create a desktop icon" and "Additional icons"). You are free to translate them into another language if required. Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - [Files] ; NOTE: Don't use "Flags: ignoreversion" on any shared system files -Source: "..\scripts\OlympusExport.lua"; DestDir: "{app}\Scripts"; Flags: ignoreversion +;Source: "..\scripts\OlympusExport.lua"; DestDir: "{app}\Scripts"; Flags: ignoreversion +;Source: "..\scripts\OlympusPatcher.exe"; DestDir: "{app}\Scripts"; Flags: ignoreversion Source: "..\scripts\OlympusHook.lua"; DestDir: "{app}\Scripts\Hooks"; Flags: ignoreversion +Source: "..\olympus.json"; DestDir: "{app}\Mods\Services\Olympus"; Flags: onlyifdoesntexist Source: "..\scripts\OlympusCommand.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion Source: "..\scripts\unitPayloads.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion -Source: "..\scripts\OlympusMission.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion -Source: "..\scripts\mist_4_4_90.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion +Source: "..\scripts\templates.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion +Source: "..\scripts\mist.lua"; DestDir: "{app}\Mods\Services\Olympus\Scripts"; Flags: ignoreversion +Source: "..\mod\*"; DestDir: "{app}\Mods\Services\Olympus"; Flags: ignoreversion recursesubdirs; Source: "..\bin\*.dll"; DestDir: "{app}\Mods\Services\Olympus\bin"; Flags: ignoreversion; Source: "..\client\bin\*"; DestDir: "{app}\Mods\Services\Olympus\client\bin"; Flags: ignoreversion; Source: "..\client\node_modules\*"; DestDir: "{app}\Mods\Services\Olympus\client\node_modules"; Flags: ignoreversion recursesubdirs; @@ -27,6 +32,7 @@ Source: "..\client\public\*"; DestDir: "{app}\Mods\Services\Olympus\client\publi Source: "..\client\routes\*"; DestDir: "{app}\Mods\Services\Olympus\client\routes"; Flags: ignoreversion recursesubdirs; Source: "..\client\views\*"; DestDir: "{app}\Mods\Services\Olympus\client\views"; Flags: ignoreversion recursesubdirs; Source: "..\client\*.*"; DestDir: "{app}\Mods\Services\Olympus\client"; Flags: ignoreversion; +Source: "..\img\olympus.ico"; DestDir: "{app}\Mods\Services\Olympus\img"; Flags: ignoreversion; Source: "{#nwjsFolder}\*.*"; DestDir: "{app}\Mods\Services\Olympus\client"; Flags: ignoreversion recursesubdirs; [Code] @@ -55,4 +61,10 @@ Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; Value ChangesEnvironment=yes [Icons] -Name: "{userdesktop}\DCS Olympus Client"; Filename: "{app}\Mods\Services\Olympus\client\nw.exe"; Tasks: desktopicon \ No newline at end of file +Name: "{userdesktop}\DCS Olympus Client"; Filename: "{app}\Mods\Services\Olympus\client\nw.exe"; Tasks: desktopicon; IconFilename: "{app}\Mods\Services\Olympus\img\olympus.ico" + +;[Run] +;Filename: "{app}\Scripts\OlympusPatcher.exe"; Parameters: "-i" + +;[UninstallRun] +;Filename: "{app}\Scripts\OlympusPatcher.exe"; Parameters: "-u" \ No newline at end of file diff --git a/missions/Olympus.miz b/missions/olympus.miz similarity index 100% rename from missions/Olympus.miz rename to missions/olympus.miz diff --git a/mod/Options/options.dlg b/mod/Options/options.dlg new file mode 100644 index 00000000..db77ba48 --- /dev/null +++ b/mod/Options/options.dlg @@ -0,0 +1,163 @@ + +-- Olympus + +-- Layout + +local TopMargin = 48 +local LeftMargin = 32 +local CenterMargin = 80 + +local LeftColumnX = LeftMargin +local LeftColumnLabelWidth = 232 +local LeftColumnOptionWidth = 272 +local LeftColumnWidth = LeftColumnLabelWidth + LeftColumnOptionWidth + +local RightColumnX = LeftColumnX + LeftColumnWidth + CenterMargin +local RightColumnLabelWidth = 192 +local RightColumnOptionWidth = 128 +local RightColumnWidth = RightColumnLabelWidth + RightColumnOptionWidth + +local LineHeight = 24 +local TotalLineHeight = LineHeight + 8 + +local HelpLineHeight = 16 + +-- Styles + +local TitleSkin = { + ["params"] = { + ["name"] = "staticOptionsTitleSkin", + }, + ["states"] = { + ["released"] = { + [1] = { + ["text"] = { + ["horzAlign"] = { + ["type"] = "min" + } + } + } + } + } + } + +local OptionLabelSkin = { + ["params"] = { + ["name"] = "staticOptionsCaptionSkin", + } + } + +local WarningSkin = { + ["params"] = { + ["name"] = "staticOptionsCaptionSkin", + }, + ["states"] = { + ["released"] = { + [1] = { + ["text"] = { + ["horzAlign"] = { + ["type"] = "min" + }, + ["color"] = "0xEFB441ff", + } + } + } + } + } + +local CheckBoxSkin = { + ["params"] = { + ["name"] = "checkBoxSkin_options", + } + } + +local ComboListSkin = { + ["params"] = { + ["name"] = "comboListSkin_options", + } + } + +local EditBoxSkin = { + ["params"] = { + ["name"] = "editBoxSkin_login", + } + } + +-- Content + +dialog = { + ["children"] = { + ["containerPlugin"] = { + ["children"] = { + + ----------------------------------------------- + -- [X] Enable Olympus Module + ----------------------------------------------- + + -- Olympus Module Enabled + + ["olympusModuleEnabledCheckbox"] = { + ["params"] = { + ["bounds"] = { + ["x"] = LeftColumnX, + ["y"] = TopMargin, + ["w"] = 256, + ["h"] = LineHeight, + }, + ["enabled"] = true, + ["state"] = false, + ["text"] = "$Olympus_MODULE_ENABLED_LABEL", + ["tooltip"] = "", + ["visible"] = true, + ["zindex"] = 0, + ["tabOrder"] = 1, + }, + ["skin"] = CheckBoxSkin, + ["type"] = "CheckBox", + }, + }, + ["params"] = { + ["bounds"] = { + ["h"] = 600, + ["w"] = 974, + ["x"] = 0, + ["y"] = 0, + }, + ["enabled"] = true, + ["text"] = "", + ["tooltip"] = "", + ["visible"] = true, + ["zindex"] = 0, + }, + ["skin"] = { + ["params"] = { + ["name"] = "panelSkin", + }, + }, + ["type"] = "Panel", + }, + }, + ["params"] = { + ["bounds"] = { + ["h"] = 851, + ["w"] = 1135, + ["x"] = 0, + ["y"] = 0, + }, + ["draggable"] = true, + ["enabled"] = true, + ["hasCursor"] = true, + ["lockFlow"] = false, + ["modal"] = false, + ["offscreen"] = false, + ["resizable"] = false, + ["text"] = "New dialog", + ["zOrder"] = 0, + }, + ["skin"] = { + ["params"] = { + ["name"] = "windowSkin", + }, + }, + ["type"] = "Window", +} \ No newline at end of file diff --git a/mod/Options/optionsData.lua b/mod/Options/optionsData.lua new file mode 100644 index 00000000..68c333d3 --- /dev/null +++ b/mod/Options/optionsData.lua @@ -0,0 +1,7 @@ +cdata = +{ + -- Module on/off + + Olympus_MODULE_ENABLED_LABEL = _("Olympus Module Enabled"), + +} diff --git a/mod/Options/optionsDb.lua b/mod/Options/optionsDb.lua new file mode 100644 index 00000000..6379b762 --- /dev/null +++ b/mod/Options/optionsDb.lua @@ -0,0 +1,60 @@ +local DbOption = require("Options.DbOption") +local i18n = require('i18n') + +-- Constants + + +-- Local variables + +local olympusConfigDialog = nil + +-- Update UI + +local function UpdateOptions() + + -- Check parameters + + if olympusConfigDialog == nil then + return + end + + local moduleEnabled = olympusConfigDialog.olympusModuleEnabledCheckbox:getState() + +end + +-- Callbacks + +local function OnShowDialog(dialogBox) + + -- Setup local variables + + if olympusConfigDialog ~= dialogBox then + olympusConfigDialog = dialogBox + end + + -- Update dialog box state + + UpdateOptions() + +end + +-- Module on/off + +local olympusModuleEnabled = DbOption.new():setValue(true):checkbox() +:callback(function(value) + UpdateOptions() +end) + +-- Returns dialog box controls and callbacks + +return +{ + -- Events + + callbackOnShowDialog = OnShowDialog, + + -- Module on/off + + olympusModuleEnabled = olympusModuleEnabled, + +} diff --git a/mod/Theme/icon-38x38.png b/mod/Theme/icon-38x38.png new file mode 100644 index 00000000..9ddcb4b9 Binary files /dev/null and b/mod/Theme/icon-38x38.png differ diff --git a/mod/Theme/icon.png b/mod/Theme/icon.png new file mode 100644 index 00000000..dc5c994e Binary files /dev/null and b/mod/Theme/icon.png differ diff --git a/mod/Theme/icon_284.png b/mod/Theme/icon_284.png new file mode 100644 index 00000000..b11172d3 Binary files /dev/null and b/mod/Theme/icon_284.png differ diff --git a/mod/Theme/icon_active.png b/mod/Theme/icon_active.png new file mode 100644 index 00000000..56f59ba4 Binary files /dev/null and b/mod/Theme/icon_active.png differ diff --git a/mod/Theme/icon_select.png b/mod/Theme/icon_select.png new file mode 100644 index 00000000..5771d3b5 Binary files /dev/null and b/mod/Theme/icon_select.png differ diff --git a/mod/entry.lua b/mod/entry.lua new file mode 100644 index 00000000..b2a04abe --- /dev/null +++ b/mod/entry.lua @@ -0,0 +1,42 @@ +local self_ID = "DCS-Olympus" + +declare_plugin(self_ID, +{ + image = "Olympus.png", + installed = true, -- if false that will be place holder , or advertising + dirName = current_mod_path, + binaries = + { +-- 'Olympus', + }, + load_immediately = true, + + displayName = "Olympus", + shortName = "Olympus", + fileMenuName = "Olympus", + + version = "0.1.1-alpha", + state = "installed", + developerName= "DCS Refugees 767 squadron", + info = _("DCS Olympus is a mod for DCS World. It allows users to spawn, control, task, group, and remove units from a DCS World server using a real-time map interface, similarly to Real Time Strategy games. The user interface also provides useful informations units, like loadouts, fuel, tasking, and so on. In the future, more features for DCS World GCI and JTAC will be available."), + + Skins = + { + { + name = "Olympus", + dir = "Theme" + }, + }, + + Options = + { + { + name = "Olympus", + nameId = "Olympus", + dir = "Options", + CLSID = "{Olympus-options}" + }, + }, +}) + +plugin_done() \ No newline at end of file diff --git a/olympus.json b/olympus.json new file mode 100644 index 00000000..3618c236 --- /dev/null +++ b/olympus.json @@ -0,0 +1,9 @@ +{ + "server": { + "address": "localhost", + "port": 30000 + }, + "authentication": { + "password": "password" + } +} diff --git a/scripts/Export.lua b/scripts/Export.lua deleted file mode 100644 index f60c236d..00000000 --- a/scripts/Export.lua +++ /dev/null @@ -1 +0,0 @@ -local Olympuslfs=require('lfs');dofile(Olympuslfs.writedir()..'Scripts/OlympusExport.lua') diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 0ea22476..81ccd4ff 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,15 +1,44 @@ +local version = "v0.3.0-alpha" + +local debug = false + Olympus.unitCounter = 1 Olympus.payloadRegistry = {} +Olympus.groupIndex = 0 +Olympus.groupStep = 40 +Olympus.OlympusDLL = nil +Olympus.DLLsloaded = false +Olympus.OlympusModPath = os.getenv('DCSOLYMPUS_PATH')..'\\bin\\' + +function Olympus.debug(message, displayFor) + if debug == true then + trigger.action.outText(message, displayFor) + end +end + function Olympus.notify(message, displayFor) trigger.action.outText(message, displayFor) end +function Olympus.loadDLLs() + -- Add the .dll paths + package.cpath = package.cpath..';'..Olympus.OlympusModPath..'?.dll;' + + local status + status, Olympus.OlympusDLL = pcall(require, 'olympus') + if status then + return true + else + return false + end +end + -- 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 + if unit and unit["getObjectID"] and unit:getObjectID() == ID then return unit end end @@ -19,9 +48,11 @@ end function Olympus.getCountryIDByCoalition(coalition) local countryID = 0 if coalition == 'red' then - countryID = country.id.RUSSIA + countryID = country.id.CJTF_RED + elseif coalition == 'blue' then + countryID = country.id.CJTF_BLUE else - countryID = country.id.USA + countryID = country.id.UN_PEACEKEEPERS end return countryID end @@ -50,25 +81,123 @@ function Olympus.buildEnrouteTask(options) } } end + -- Start being an active tanker + elseif options['id'] == 'Tanker' then + task = { + id = 'Tanker', + params = {}, + } + -- Start being an active AWACS + elseif options['id'] == 'AWACS' then + task = { + id = 'AWACS', + params = {}, + } end return task end -- Builds a valid task depending on the provided options -function Olympus.buildTask(options) +function Olympus.buildTask(groupName, options) local task = nil - -- Engage specific target by ID. Checks if target exists. - if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then - local leader = Olympus.getUnitByID(options['leaderID']) - if leader and leader:isExist() then + + local group = Group.getByName(groupName) + if (Olympus.isArray(options)) then + local tasks = {} + for idx, subOptions in pairs(options) do + tasks[idx] = Olympus.buildTask(groupName, subOptions) or Olympus.buildEnrouteTask(subOptions) + end + task = { + id = 'ComboTask', + params = { + tasks = tasks + } + } + Olympus.debug(Olympus.serializeTable(task), 30) + else + if options['id'] == 'FollowUnit' and options['leaderID'] and options['offset'] then + local leader = Olympus.getUnitByID(options['leaderID']) + if leader and leader:isExist() then + task = { + id = 'Follow', + params = { + groupId = leader:getGroup():getID(), + pos = options['offset'], + lastWptIndexFlag = false, + lastWptIndex = 1 + } + } + end + elseif options['id'] == 'Refuel' then task = { - id = 'Follow', + id = 'Refueling', + params = {} + } + elseif options['id'] == 'Orbit' then + task = { + id = 'Orbit', + params = { + pattern = options['pattern'] or "Circle" + } + } + if options['altitude'] then + if options ['altitudeType'] then + if options ['altitudeType'] == "AGL" then + local groundHeight = 0 + if group then + local groupPos = mist.getLeadPos(group) + groundHeight = land.getHeight({x = groupPos.x, y = groupPos.z}) + end + task['params']['altitude'] = groundHeight + options['altitude'] + else + task['params']['altitude'] = options['altitude'] + end + else + task['params']['altitude'] = options['altitude'] + end + end + if options['speed'] then + task['params']['speed'] = options['speed'] + end + elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'Bombing', params = { - groupId = leader:getGroup():getID(), - pos = options['offset'], - lastWptIndexFlag = false, - lastWptIndex = 1 - } + point = {x = point.x, y = point.z}, + attackQty = 1 + } + } + elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'CarpetBombing', + params = { + x = point.x, + y = point.z, + carpetLength = 1000, + attackType = 'Carpet', + expend = "All", + attackQty = 1, + attackQtyLimit = true + } + } + elseif options['id'] == 'AttackMapObject' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'AttackMapObject', + params = { + point = {x = point.x, y = point.z} + } + } + elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'FireAtPoint', + params = { + point = {x = point.x, y = point.z}, + radius = options['radius'] + } } end end @@ -76,23 +205,27 @@ function Olympus.buildTask(options) end -- Move a unit. Since many tasks in DCS are Enroute tasks, this function is an important 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 +function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions) + Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2) + local group = Group.getByName(groupName) + if group then if category == "Aircraft" then - local startPoint = mist.getLeadPos(unit:getGroup()) + local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + if altitudeType == "AGL" then + altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude + end + local path = {} if taskOptions and taskOptions['id'] == 'Land' then path = { - [1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO'), + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), [2] = mist.fixedWing.buildWP(endPoint, landing, speed, 0, 'AGL') } else path = { - [1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO'), + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') } end @@ -115,35 +248,40 @@ function Olympus.move(ID, lat, lng, altitude, speed, category, taskOptions) }, }, } - group = unit:getGroup() local groupCon = group:getController() if groupCon then groupCon:setTask(missionTask) end - Olympus.notify("Olympus.move executed successfully on a Aircraft", 2) + Olympus.debug("Olympus.move executed successfully on a Aircraft", 2) elseif category == "GroundUnit" then vars = { - group = unit:getGroup(), + group = group, point = coord.LLtoLO(lat, lng, 0), - form = "Off Road", heading = 0, - speed = speed, - disableRoads = true + speed = speed } + + if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then + vars["disableRoads"] = false + else + vars["form"] = "Off Road" + vars["disableRoads"] = true + end + mist.groupToRandomPoint(vars) - Olympus.notify("Olympus.move executed succesfully on a ground unit", 2) + Olympus.debug("Olympus.move executed succesfully on a ground unit", 2) else - Olympus.notify("Olympus.move not implemented yet for " .. category, 2) + Olympus.debug("Olympus.move not implemented yet for " .. category, 2) end else - Olympus.notify("Error in Olympus.move " .. unitName, 2) + Olympus.debug("Error in Olympus.move " .. groupName, 2) end end -- Creates a simple smoke on the ground function Olympus.smoke(color, lat, lng) - Olympus.notify("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 2) + Olympus.debug("Olympus.smoke " .. color .. " (" .. lat .. ", " .. lng ..")", 2) local colorEnum = nil if color == "green" then colorEnum = trigger.smokeColor.Green @@ -159,21 +297,44 @@ function Olympus.smoke(color, lat, lng) trigger.action.smoke(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), colorEnum) 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) +end + -- Spawns a single ground unit function Olympus.spawnGroundUnit(coalition, unitType, lat, lng) - Olympus.notify("Olympus.spawnGroundUnit " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) + Olympus.debug("Olympus.spawnGroundUnit " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) - local unitTable = - { - [1] = + + local unitTable = {} + + if Olympus.hasKey(templates, unitType) then + for idx, value in pairs(templates[unitType].units) do + unitTable[#unitTable + 1] = { + ["type"] = value.name, + ["x"] = spawnLocation.x + value.dx, + ["y"] = spawnLocation.z + value.dy, + ["playerCanDrive"] = true, + ["heading"] = 0, + ["skill"] = "High" + } + end + else + unitTable = { - ["type"] = unitType, - ["x"] = spawnLocation.x, - ["y"] = spawnLocation.z, - ["playerCanDrive"] = true, - ["heading"] = 0, - }, - } + [1] = + { + ["type"] = unitType, + ["x"] = spawnLocation.x, + ["y"] = spawnLocation.z, + ["playerCanDrive"] = true, + ["heading"] = 0, + ["skill"] = "High" + }, + } + end local countryID = Olympus.getCountryIDByCoalition(coalition) @@ -186,19 +347,19 @@ function Olympus.spawnGroundUnit(coalition, unitType, lat, lng) } mist.dynAdd(vars) Olympus.unitCounter = Olympus.unitCounter + 1 - Olympus.notify("Olympus.spawnGround completed succesfully", 2) + Olympus.debug("Olympus.spawnGround completed succesfully", 2) end -- 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) +function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) local payloadName = spawnOptions["payloadName"] local airbaseName = spawnOptions["airbaseName"] local payload = spawnOptions["payload"] - Olympus.notify("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) + Olympus.debug("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..", " .. alt .. ")", 2) local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) if payload == nil then @@ -218,10 +379,13 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) ["type"] = unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, + ["alt"] = alt, + ["alt_type"] = "BARO", ["skill"] = "Excellent", ["payload"] = { ["pylons"] = payload, + ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, @@ -251,17 +415,18 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) { [1] = { - ["action"] = "From Runway", + ["action"] = "From Parking Area Hot", ["task"] = { ["id"] = "ComboTask", ["params"] = {["tasks"] = {},}, }, - ["type"] = "TakeOff", + ["type"] = "TakeOffParkingHot", ["ETA"] = 0, ["ETA_locked"] = true, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, + ["alt_type"] = "BARO", ["formation_template"] = "", ["airdromeId"] = airbaseID, ["speed_locked"] = true, @@ -269,8 +434,61 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) }, } end + else + route = { + ["points"] = + { + [1] = + { + ["alt"] = alt, + ["alt_type"] = "BARO", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "EPLRS", + ["params"] = + { + ["value"] = true + }, + }, + }, + }, + [2] = + { + ["number"] = 2, + ["auto"] = false, + ["id"] = "Orbit", + ["enabled"] = true, + ["params"] = + { + ["pattern"] = "Circle" + }, + }, + }, + }, + }, + ["type"] = "Turning Point", + ["x"] = spawnLocation.x, + ["y"] = spawnLocation.z, + }, -- end of [1] + }, -- end of ["points"] + } -- end of ["route"] end - + local vars = { units = unitTable, @@ -286,70 +504,88 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) -- 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 successfully", 2) + Olympus.debug("Olympus.spawnAir completed successfully", 2) end -- 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, lat, lng) - Olympus.notify("Olympus.clone " .. ID, 2) +function Olympus.clone(ID, lat, lng, category) + Olympus.debug("Olympus.clone " .. ID .. ", " .. category, 2) local unit = Olympus.getUnitByID(ID) if unit then local coalition = Olympus.getCoalitionByCoalitionID(unit:getCoalition()) - -- TODO: only works on Aircraft - local spawnOptions = { - payload = Olympus.payloadRegistry[unit:getName()] - } - Olympus.spawnAircraft(coalition, unit:getTypeName(), lat, lng, spawnOptions) + if category == "Aircraft" then + local spawnOptions = { + payload = Olympus.payloadRegistry[unit:getName()] + } + Olympus.spawnAircraft(coalition, unit:getTypeName(), lat, lng, unit:getPoint().y, spawnOptions) + elseif category == "GroundUnit" then + Olympus.spawnGroundUnit(coalition, unit:getTypeName(), lat, lng) + end end - Olympus.notify("Olympus.clone completed successfully", 2) + Olympus.debug("Olympus.clone completed successfully", 2) end -function Olympus.delete(ID, lat, lng) - Olympus.notify("Olympus.delete " .. ID, 2) +function Olympus.delete(ID, explosion) + Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) if unit then - unit:destroy(); - Olympus.notify("Olympus.delete completed successfully", 2) - end -end - -function Olympus.setTask(ID, taskOptions) - Olympus.notify("Olympus.setTask " .. ID .. " " .. Olympus.serializeTable(taskOptions), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - local task = Olympus.buildTask(taskOptions); - if task then - unit:getGroup():getController():setTask(task) - Olympus.notify("Olympus.setTask completed successfully", 2) + 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 + Olympus.debug("Olympus.delete completed successfully", 2) + else + unit:destroy(); --works for AI units not players + Olympus.debug("Olympus.delete completed successfully", 2) end end end -function Olympus.resetTask(ID) - Olympus.notify("Olympus.resetTask " .. ID, 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():resetTask() - Olympus.notify("Olympus.resetTask completed successfully", 2) +function Olympus.setTask(groupName, taskOptions) + Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) + local group = Group.getByName(groupName) + if group then + local task = Olympus.buildTask(groupName, taskOptions); + Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20) + if task then + group:getController():setTask(task) + Olympus.debug("Olympus.setTask completed successfully", 2) + end end end -function Olympus.setCommand(ID, command) - Olympus.notify("Olympus.setCommand " .. ID .. " " .. Olympus.serializeTable(command), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setCommand(command) - Olympus.notify("Olympus.setCommand completed successfully", 2) +function Olympus.resetTask(groupName) + Olympus.debug("Olympus.resetTask " .. groupName, 2) + local group = Group.getByName(groupName) + if group then + group:getController():resetTask() + Olympus.debug("Olympus.resetTask completed successfully", 2) end end -function Olympus.setOption(ID, optionID, optionValue) - Olympus.notify("Olympus.setCommand " .. ID .. " " .. optionID .. " " .. optionValue, 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setOption(optionID, optionValue) - Olympus.notify("Olympus.setOption completed successfully", 2) +function Olympus.setCommand(groupName, command) + Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setCommand(command) + Olympus.debug("Olympus.setCommand completed successfully", 2) + end +end + +function Olympus.setOption(groupName, optionID, optionValue) + Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOption(optionID, optionValue) + Olympus.debug("Olympus.setOption completed successfully", 2) + end +end + +function Olympus.setOnOff(groupName, onOff) + Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOnOff(onOff) + Olympus.debug("Olympus.setOnOff completed successfully", 2) end end @@ -385,4 +621,148 @@ function Olympus.serializeTable(val, name, skipnewlines, depth) return tmp end -Olympus.notify("OlympusCommand script loaded successfully", 2) \ No newline at end of file +function Olympus.isArray(t) + local i = 0 + for _ in pairs(t) do + i = i + 1 + if t[i] == nil then return false end + end + return true +end + +function Olympus.hasValue(tab, val) + for index, value in ipairs(tab) do + if value == val then + return true + end + end + + return false +end + +function Olympus.hasKey(tab, key) + for k, value in pairs(tab) do + if k == key then + return true + end + end + + return false +end + +function Olympus.setMissionData(arg, time) + local missionData = {} + + -- Bullseye data + local bullseyes = {} + for i = 0, 2 do + local bullseyeVec3 = coalition.getMainRefPoint(i) + local bullseyeLatitude, bullseyeLongitude, bullseyeAltitude = coord.LOtoLL(bullseyeVec3) + bullseyes[i] = {} + bullseyes[i]["latitude"] = bullseyeLatitude + bullseyes[i]["longitude"] = bullseyeLongitude + bullseyes[i]["coalition"] = Olympus.getCoalitionByCoalitionID(i) + end + + -- Units tactical data + local unitsData = {} + + local startIndex = Olympus.groupIndex + local endIndex = startIndex + Olympus.groupStep + local index = 0 + if mist ~= nil and mist.DBs ~= nil and mist.DBs.groupsByName ~= nil then + for groupName, gp in pairs(mist.DBs.groupsByName) do + index = index + 1 + if index > startIndex then + if groupName ~= nil then + local group = Group.getByName(groupName) + if group ~= nil then + -- Get the targets detected by the group controller + local controller = group:getController() + local controllerTargets = controller:getDetectedTargets() + + for index, unit in pairs(group:getUnits()) do + local unitController = unit:getController() + local table = {} + table["contacts"] = {} + + for i, target in ipairs(controllerTargets) do + for det, enum in pairs(Controller.Detection) do + if target.object ~= nil then + local detected = unitController:isTargetDetected(target.object, enum) + + if detected then + target["detectionMethod"] = det + table["contacts"][#table["contacts"] + 1] = target + end + end + end + end + + table["hasTask"] = controller:hasTask() + + table["ammo"] = unit:getAmmo() + table["fuel"] = unit:getFuel() + table["life"] = unit:getLife() / unit:getLife0() + unitsData[unit:getObjectID()] = table + end + end + end + end + if index >= endIndex then + break + end + end + if index ~= endIndex then + Olympus.groupIndex = 0 + else + Olympus.groupIndex = endIndex + end + end + + -- Airbases data + local base = world.getAirbases() + local airbases = {} + 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"] = Olympus.getCoalitionByCoalitionID(Airbase.getCoalition(base[i])) + info["latitude"] = latitude + info["longitude"] = longitude + if Airbase.getUnit(base[i]) then + info["unitId"] = Airbase.getUnit(base[i]):getID() + end + airbases[i] = info + end + + local mission = {} + mission.theatre = env.mission.theatre + mission.elapsedTime = DCS.getRealTime() + mission.time = mist.time.getDHMS(timer.getAbsTime()) + mission.startTime = env.mission.start_time + mission.date = env.mission.date + + -- Assemble missionData table + missionData["bullseyes"] = bullseyes + missionData["unitsData"] = unitsData + missionData["airbases"] = airbases + missionData["mission"] = mission + + Olympus.missionData = missionData + Olympus.OlympusDLL.setMissionData() + return time + 1 +end + +local OlympusName = 'Olympus ' .. version .. ' C++ module'; +isOlympusModuleInitialized=true; +Olympus.DLLsloaded = Olympus.loadDLLs() +if Olympus.DLLsloaded then + Olympus.notify(OlympusName..' successfully loaded.', 20) +else + Olympus.notify('Failed to load '..OlympusName, 20) +end + +timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1) + +Olympus.notify("OlympusCommand script " .. version .. " loaded successfully", 2, true) \ No newline at end of file diff --git a/scripts/OlympusExport.lua b/scripts/OlympusExport.lua deleted file mode 100644 index 29663e8a..00000000 --- a/scripts/OlympusExport.lua +++ /dev/null @@ -1,38 +0,0 @@ -Olympus = {} -Olympus.OlympusDLL = nil -Olympus.cppRESTDLL = nil -Olympus.DLLsloaded = false -Olympus.OlympusModPath = os.getenv('DCSOLYMPUS_PATH')..'\\bin\\' - -log.write('Olympus.EXPORT.LUA', log.INFO, 'Executing OlympusExport.lua') - -function Olympus.loadDLLs() - -- Add the .dll paths - package.cpath = package.cpath..';'..Olympus.OlympusModPath..'?.dll;' - - local status - log.write('Olympus.HOOKS.LUA', log.INFO, 'Loading olympus.dll from ['..Olympus.OlympusModPath..']') - status, Olympus.OlympusDLL = pcall(require, 'olympus') - if status then - log.write('Olympus.HOOKS.LUA', log.INFO, 'olympus.dll loaded successfully') - return true - else - log.write('Olympus.HOOKS.LUA', log.ERROR, 'Error loading olympus.dll: '..Olympus.OlympusDLL) - return false - end -end - -do - if isOlympusModuleInitialized~=true then - local OlympusName = 'Olympus 0.0.1 C++ module'; - isOlympusModuleInitialized=true; - Olympus.DLLsloaded = Olympus.loadDLLs() - if Olympus.DLLsloaded then - log.write('Olympus.EXPORT.LUA', log.INFO, OlympusName..' successfully loaded.') - else - log.write('Olympus.EXPORT.LUA', log.ERROR, 'Failed to load '..OlympusName) - end - else - log.write('Olympus.EXPORT.LUA', log.INFO, 'olympus.dll already initialized') - end -end \ No newline at end of file diff --git a/scripts/OlympusHook.lua b/scripts/OlympusHook.lua index 10c9432a..b87c8926 100644 --- a/scripts/OlympusHook.lua +++ b/scripts/OlympusHook.lua @@ -1,3 +1,5 @@ +local version = 'v0.3.0-alpha' + Olympus = {} Olympus.OlympusDLL = nil Olympus.cppRESTDLL = nil @@ -24,7 +26,7 @@ end do if isOlympusModuleInitialized~=true then - local OlympusName = 'Olympus 0.0.1 C++ module'; + local OlympusName = 'Olympus ' .. version .. ' C++ module'; Olympus.loadDLLs(); -- Register callbacks diff --git a/scripts/OlympusMission.lua b/scripts/OlympusMission.lua deleted file mode 100644 index 87bd6b90..00000000 --- a/scripts/OlympusMission.lua +++ /dev/null @@ -1,106 +0,0 @@ - -Olympus = {} -function Olympus.notify(message, displayFor) - trigger.action.outText(message, displayFor) -end - -function Olympus.setMissionData(arg, time) - local missionData = {} - - -- Bullseye data - local bullseye = {} - for i = 0, 2 do - local bullseyeVec3 = coalition.getMainRefPoint(i) - local bullseyeLatitude, bullseyeLongitude, bullseyeAltitude = coord.LOtoLL(bullseyeVec3) - bullseye[i] = {} - bullseye[i]["lat"] = bullseyeLatitude - bullseye[i]["lng"] = bullseyeLongitude - end - - -- Units tactical data - -- TODO find some way to spread the load of getting this data (split) - local unitsData = {} - for groupName, gp in pairs(mist.DBs.groupsByName) do - if groupName ~= nil then - local group = Group.getByName(groupName) - if group ~= nil then - local controller = group:getController() - for index, unit in pairs(group:getUnits()) do - local table = {} - table["targets"] = {} - table["targets"]["visual"] = controller:getDetectedTargets(1) - table["targets"]["radar"] = controller:getDetectedTargets(4) - table["targets"]["rwr"] = controller:getDetectedTargets(16) - table["targets"]["other"] = controller:getDetectedTargets(2, 8, 32) - - table["hasTask"] = controller:hasTask() - - table["ammo"] = unit:getAmmo() - table["fuel"] = unit:getFuel() - table["life"] = unit:getLife() / unit:getLife0() - unitsData[unit:getObjectID()] = table - end - end - 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) - return time + 5 -end - -function Olympus.serializeTable(val, name, skipnewlines, depth) - skipnewlines = skipnewlines or false - depth = depth or 0 - - local tmp = string.rep(" ", depth) - if name then - if type(name) == "number" then - tmp = tmp .. "[" .. name .. "]" .. " = " - else - tmp = tmp .. name .. " = " - end - end - - 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) - elseif type(val) == "string" then - tmp = tmp .. string.format("%q", val) - elseif type(val) == "boolean" then - tmp = tmp .. (val and "true" or "false") - else - tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\"" - end - - return tmp -end - -timer.scheduleFunction(Olympus.setMissionData, {}, timer.getTime() + 1) -Olympus.notify("OlympusMission script loaded correctly", 10) diff --git a/scripts/coals.lua b/scripts/coals.lua new file mode 100644 index 00000000..445e1473 --- /dev/null +++ b/scripts/coals.lua @@ -0,0 +1,47 @@ +coal = {} + +function coal.notify(message, displayFor) + trigger.action.outText(message, displayFor) +end + +function coal.listRed() + coal.coals = env.mission.coalitions.red --solid naming this + coal.notify(mist.utils.tableShow(coal.coals),5) + pickOne = math.random(#coal.coals) + countryIs = country.name[coal.coals[pickOne]] + coal.notify(countryIs,10) +end + +function coal.listBlue() + coal.coals = env.mission.coalitions.blue --solid naming this + coal.notify(mist.utils.tableShow(coal.coals),5) + pickOne = math.random(#coal.coals) + countryIs = country.name[coal.coals[pickOne]] + coal.notify(countryIs,10) +end + +function coal.listNeutrals() + coal.coals = env.mission.coalitions.neutrals --solid naming this + coal.notify(mist.utils.tableShow(coal.coals),5) + pickOne = math.random(#coal.coals) + countryIs = country.name[coal.coals[pickOne]] + coal.notify(countryIs,10) +end + + +do + longRangeShots = missionCommands.addSubMenu("Coal check") + missionCommands.addCommand ("List reds", longRangeShots, coal.listRed) + missionCommands.addCommand ("List blue", longRangeShots, coal.listBlue) + missionCommands.addCommand ("List neutrals", longRangeShots, coal.listNeutrals) + +end + +coal.notify("coals.lua loaded", 2) + + +-- env.mission.coalitions.red +-- env.mission.coalitions.blue +-- env.mission.coalitions.neutrals + +--coalition.getCountryCoalition(countryID) \ No newline at end of file diff --git a/scripts/coolEffects.lua b/scripts/coolEffects.lua new file mode 100644 index 00000000..52efb537 --- /dev/null +++ b/scripts/coolEffects.lua @@ -0,0 +1,305 @@ +effects = {} +effects.shooterName = "TestInfantry" +effects.napalmCounter = 1 + +effects.fireCounter = 1 + +function effects.notify(message, displayFor) + trigger.action.outText(message, displayFor, false) +end + +-------------------------------------------- +-------------------------------------------- +-------------------------------------------- +----NAPALM + + +function effects.napalmSingle () + unit = Unit.getByName(effects.shooterName) + local unitPos = unit:getPosition().p + vec3 = mist.utils.makeVec3GL(unitPos) + effects.spawnNapalm (vec3) +end + + +function effects.spawnNapalm (vec3) + + napeName = "napalmStrike" .. effects.napalmCounter + effects.napalmCounter = effects.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(effects.explode,vec3, timer.getTime() + 0.1) + timer.scheduleFunction(effects.napalam_death,napeName, timer.getTime() + 0.12) +end + +function effects.explode(vec3) + trigger.action.explosion(vec3, 10) +end + +function effects.napalam_death(staticName) --yes i know bad pun, removes the fuel tank after a set time + StaticObject.getByName(staticName):destroy() +end + +-------------------------------------------- +-------------------------------------------- +-------------------------------------------- +----Basic smoke or fire that despawns +function effects.smokeFire () + unit = Unit.getByName(effects.shooterName) + local unitPos = unit:getPosition().p + vec3 = mist.utils.makeVec3GL(unitPos) + effects.createFire (vec3, 2) + -- 1 = small smoke and fire + -- 2 = medium smoke and fire + -- 3 = large smoke and fire + -- 4 = huge smoke and fire + -- 5 = small smoke + -- 6 = medium smoke + -- 7 = large smoke + -- 8 = huge smoke +end + +function effects.createFire (vec3, size) + smokeName = "smokeName" .. effects.fireCounter + effects.fireCounter = effects.fireCounter + 1 + trigger.action.effectSmokeBig(vec3 , size , 1, smokeName) + trigger.action.explosion(vec3, 1) -- looks wierd to spawn in on flat land without this + timer.scheduleFunction(effects.removeFire,smokeName, timer.getTime() + 20) --you could set a timer, or if selected give option to despawn later +end + +function effects.removeFire (smokeName) + trigger.action.effectSmokeStop(smokeName) +end + +-------------------------------------------- +-------------------------------------------- +-------------------------------------------- +----White phosporus secondaries extra effect, like round cooking off +--if you up the number going pop to somewhere in the 200-400 region with a white phosporus impact it would look mental cool +function effects.secondaries () + unit = Unit.getByName(effects.shooterName) + local unitPos = unit:getPosition().p + vec3 = mist.utils.makeVec3GL(unitPos) + --trigger.action.smoke(vec3 , 2 ) + 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 + +-------------------------------------------- +-------------------------------------------- +-------------------------------------------- +----Depth Charges +-- these also make, on land, good dust clouds for a bomb hit in a sandy area? +-- local surface = land.getSurfaceType(mist.utils.makeVec2(unitPos)) -- optional check for water, value 3 or 2 + +function effects.depthCharge () + local unit = Unit.getByName(effects.shooterName) + local unitPos = unit:getPosition().p + + vec3 = mist.utils.makeVec3GL(unitPos) + vec3.y = vec3.y - 1000 + bang = vec3 + distance = 20 + explosionSize = 2 + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + trigger.action.explosion(vec3,explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + trigger.action.explosion(vec3,explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + bang = mist.getRandPointInCircle(vec3 , distance ,1,359,0) + trigger.action.explosion(mist.utils.makeVec3GL(bang),explosionSize) + timer.scheduleFunction(effects.depthChargeMain,vec3, timer.getTime() + 5) +end + + + +function effects.depthChargeMain (vec3) + explosionSize = 250 + trigger.action.explosion(vec3,explosionSize) + trigger.action.explosion(vec3,explosionSize) + vec3.x = vec3.x + trigger.action.explosion(vec3,explosionSize) + vec3.x = vec3.x - 10 + trigger.action.explosion(vec3,explosionSize) + vec3.z = vec3.z + trigger.action.explosion(vec3,explosionSize) + vec3.z = vec3.z - 10 +end + +-------------------------------------------- +-------------------------------------------- +-------------------------------------------- +----Normal small explosion + +function effects.normalSmallExplosion (vec3) + unit = Unit.getByName(effects.shooterName) + local unitPos = unit:getPosition().p + vec3 = mist.utils.makeVec3GL(unitPos) + trigger.action.explosion(vec3,10) +end + + + +do + longRangeShots = missionCommands.addSubMenu("Effects") + missionCommands.addCommand ("Napalm", longRangeShots, effects.napalmSingle) + missionCommands.addCommand ("Fire or smoke", longRangeShots, effects.smokeFire) + missionCommands.addCommand ("Secondary explosions", longRangeShots, effects.secondaries) + missionCommands.addCommand ("Depth Charge", longRangeShots, effects.depthCharge) + missionCommands.addCommand ("A regular explosion", longRangeShots, effects.normalSmallExplosion) +end + +effects.notify("effects.lua ran", 2) \ No newline at end of file diff --git a/scripts/dynamicTanking.lua b/scripts/dynamicTanking.lua new file mode 100644 index 00000000..daa5761d --- /dev/null +++ b/scripts/dynamicTanking.lua @@ -0,0 +1,681 @@ +tankers = {} +tankers.tankerName = "TankerClone" + +function tankers.notify(message, displayFor) + trigger.action.outText(message, displayFor, false) +end + + +function tankers.setFrequency(freq) + + unit = Unit.getByName(tankers.tankerName) + local controller = unit:getController() + + freq = freq or 260 --in MHz, 260 channel 19 is our default tanker thing in refs + + SetFrequency = { + id = 'SetFrequency', + params = { + frequency = freq*1000000 , --in Hz + modulation = 0, --AM 0 or FM 1 + } + } + + controller:setCommand(SetFrequency) +end + +function tankers.changeCallsign() + ---https://wiki.hoggitworld.com/view/DCS_command_setCallsign + unit = Unit.getByName(tankers.tankerName) + local controller = unit:getController() + + SetCallsign = { + id = 'SetCallsign', + params = { + callname = 3, --1 texaco, --2 arco -- 3 shell + number = 1, --1 through 9 valid for tankers only ever 1? + } + } + + controller:setCommand(SetCallsign) +end + +--remember to only pick valid tacan channel ranges + + +-- https://wiki.radioreference.com/index.php/Instrument_Landing_System_(ILS)_Frequencies -- what freqs go with which tacans +-- you want the reply channels on the tankers so the fighter tunes the one you want + +function tankers.setTacan(channel, xRay) + + defaultTac = 40 + defaultXray = true + channel = channel or defaultTac -- the channel you want to tell the fighters to enter in, if not provided defaults + xRay = xRay or defaultXray -- X or Y are only options so true or false + + unit = Unit.getByName(tankers.tankerName) + local controller = unit:getController() + + --tacan maths is easy + --for X ray reply it is, channel + 961, Yankee reply is channel + 1087 + + if xRay == true then + --to not break everyone elses datalink / tacan 37 and above (X) + if channel > 36 then + freq = channel + 961 + ActivateBeacon = { + id = 'ActivateBeacon', + params = { + type = 4, + system = 3, + name = "TKR", + callsign = "ABC", --what shows as a listed word / plays as morese code, 3 max no spaces + frequency = freq*1000000, + } + } + controller:setCommand(ActivateBeacon) + end + + elseif xRay == false then + --to not break everyone elses datalink / tacan 30 - 46 (Y) but I don't think the "above" is correct + if channel > 29 then + freq = channel + 1087 + ActivateBeacon = { + id = 'ActivateBeacon', + params = { + type = 4, + system = 3, + name = "TKR", + callsign = "ABC", --what shows as a listed word / plays as morese code, 3 max no spaces + frequency = freq*1000000, + } + } + controller:setCommand(ActivateBeacon) + end + end +end + +function tankers.dryPlugTanking () -- for whatever reason this ends up being no fuel transfer? + --tankers.setFrequency(260) + --tankers.setTacan(41, true) + --tankers.changeCallsign() + + unit = Unit.getByName(tankers.tankerName) + cvn = Unit.getByName("CVN") + local cvnPos = cvn:getPosition().p + local tnkrPos = unit:getPosition().p + local speed = 250 + local controller = unit:getController() + + --if you want to try making a tanker do something else + --https://www.digitalcombatsimulator.com/en/support/faq/1267/#3307682 maybe? stop conditions etc + --personally i think delete the thing if it doesn't work + --there is a recovery tanker option, but for me it makes planes stall and hit the floor and we can fake it with this already + + -- this might all seem very over the top compared to the docs, but if you don't do it the tanker instantly RTBs, though you can tank on final which is hillarious + + task1 = { + ["number"] = 1, + ["auto"] = false, + ["id"] = "ControlledTask", + ["enabled"] = true, + ["params"] = + { + ["task"] = + { + ["id"] = "Tanker", + ["params"] = + { + }, -- end of ["params"] + }, -- end of ["task"] + ["stopCondition"] = + { + ["duration"] = 600, + ["userFlag"] = "1", + }, -- end of ["stopCondition"] + }, -- end of ["params"] + } + + task2 = { + ["number"] = 2, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "ActivateBeacon", + ["params"] = + { + ["type"] = 4, + ["AA"] = false, + ["callsign"] = "TKR", + ["modeChannel"] = "Y", + ["channel"] = 71, + ["system"] = 5, + ["unitId"] = 188, + ["bearing"] = true, + ["frequency"] = 1032000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + } + task3 = + { + ["number"] = 3, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetFrequency", + ["params"] = + { + ["power"] = 10, + ["modulation"] = 0, + ["frequency"] = 305000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + } + + task4 = + { + ["number"] = 4, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetInvisible", + ["params"] = + { + ["value"] = true, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + } + + point1 = { + ['speed_locked'] = false, + ['type'] = 'Turning Point', + ['action'] = 'Turning Point', + ['alt_type'] = 'BARO', + ['y'] = cvnPos.z, + ['x'] = cvnPos.x, + ['speed'] = 128.611, + ['task'] = { + ['id'] = 'ComboTask', + ['params'] = { + ['tasks'] = { + [1] = task1, --tanker first + [2] = task2, --whatever second + [3] = task3, + [4] = task4, + } + } + }, + ['alt'] = tnkrPos.y + } + point2 = { + ['speed_locked'] = true, + ['type'] = 'Turning Point', + ['action'] = 'Turning Point', + ['alt_type'] = 'BARO', + ['y'] = 30553, + ['x'] = 35881, + ['speed'] = 128.611, + ['task'] = { + ['id'] = 'ComboTask', + ['params'] = { + ['tasks'] = { + } + } + }, + ['alt'] = 2133.6 + } + + missionTask = + { + ['id'] = 'Mission', + ['params'] = { + ['route'] = { + ['points'] = { + [1] = point1, + --[2] = point2, + } + }, + ['airborne'] = true + } + } + controller:pushTask(missionTask) +end + +function tankers.followInFront () + unit = Unit.getByName(tankers.tankerName) + local controller = unit:getController() + + FollowAheadOfGroup = { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "Follow", + ["number"] = 1, + ["params"] = + { + ["lastWptIndexFlagChangedManually"] = false, + ["groupId"] = 74, + ["lastWptIndex"] = 2, + ["lastWptIndexFlag"] = false, + ["pos"] = + { + ["y"] = 152.4, --mins for KC 135 to accidentally stern rejoin and overfly + ["x"] = 1000.8, + ["z"] = 39.9288, + }, -- end of ["pos"] + }, -- end of ["params"] + } + + controller:pushTask(FollowAheadOfGroup) +end + +function tankers.followInFrontClose () + unit = Unit.getByName(tankers.tankerName) + local controller = unit:getController() + + FollowAheadOfGroup = { + ["enabled"] = true, + ["auto"] = false, + ["id"] = "Follow", + ["number"] = 1, + ["params"] = + { + ["lastWptIndexFlagChangedManually"] = false, + ["groupId"] = 74, + ["lastWptIndex"] = 2, + ["lastWptIndexFlag"] = false, + ["pos"] = + { + ["y"] = 25, --mins for KC 135 to accidentally stern rejoin and overfly + ["x"] = 150, + ["z"] = 41.45, + }, -- end of ["pos"] + }, -- end of ["params"] + } + + controller:pushTask(FollowAheadOfGroup) +end + +function tankers.cloneTanker() + +local groupName = 'TankerClone' -- Name of the group in the ME + +group = mist.getGroupData(groupName) +group.route = { points = mist.getGroupRoute(groupName, true) } +group.groupName = "Tanker1" +group.groupId = nil +group.units[1].unitId = nil +group.units[1].unitName = newName +group.country = country +group.category = 'AIRPLANE' + +mist.dynAdd(group) + +end + +function tankers.newTanker() + +local groupName = 'TankerClone' -- Name of the group in the ME +local cloneGroupPos = Unit.getByName(groupName):getPosition().p +cvn = Unit.getByName("CVN") +local cvnPos = cvn:getPosition().p + + +group = mist.getGroupData(groupName) +group.route = { + ["points"] = + { + [1] = + { + ["alt"] = 2133.6, + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["speed"] = 179.86111111111, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = false, + ["id"] = "ControlledTask", + ["enabled"] = true, + ["params"] = + { + ["task"] = + { + ["id"] = "Tanker", + ["params"] = + { + }, -- end of ["params"] + }, -- end of ["task"] + ["stopCondition"] = + { + ["duration"] = 900, + }, -- end of ["stopCondition"] + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "ActivateBeacon", + ["params"] = + { + ["type"] = 4, + ["AA"] = false, + ["callsign"] = "TKR", + ["modeChannel"] = "Y", + ["channel"] = 71, + ["system"] = 5, + ["unitId"] = 188, + ["bearing"] = true, + ["frequency"] = 1032000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetFrequency", + ["params"] = + { + ["power"] = 10, + ["modulation"] = 0, + ["frequency"] = 260000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + [4] = + { + ["number"] = 4, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetInvisible", + ["params"] = + { + ["value"] = true, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [4] + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["type"] = "Turning Point", + ["ETA"] = 96.50677034026, + ["ETA_locked"] = false, + ["y"] = cvnPos.z, + ["x"] = cvnPos.x, + ["formation_template"] = "", + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + } + +--group.units[1].type = "S-3B Tanker" +group.groupName = "Tanker1" +group.groupId = nil +group.units[1].unitId = nil +group.units[1].unitName = newName +group.country = country +group.category = 'AIRPLANE' +group.units[1].x = cloneGroupPos.x +group.units[1].y = cloneGroupPos.z +group.units[1].z = cloneGroupPos.y +group.units[1].speed = 999999 + + +Group.destroy(Group.getByName(groupName)) +mist.dynAdd(group) +--timer.scheduleFunction(mist.dynAdd,group, timer.getTime() + 0.00000000001) + +end + + +function tankers.startEnrouteTankingTest (vec3) -- this is the one that works well, clone an existing tanker that is currently mission editor tanking + + --tankers.setFrequency(260) + --tankers.setTacan(41, true) + --tankers.changeCallsign() + route = mist.getGroupRoute(tankers.tankerName, true) + unit = Unit.getByName(tankers.tankerName) + cvn = Unit.getByName("CVN") + + local cvnPos = cvn:getPosition().p + + local vec3 = vec3 or cvnPos + route[1].x = unit:getPosition().p.x + route[1].y = unit:getPosition().p.z + route[2].x = vec3.x + route[2].y = vec3.z + route[2].z = vec3.y + 100 + + mist.goRoute(tankers.tankerName , route ) +end + +function tankers.hyperSpace (vec3) -- this is the one that works well, clone an existing tanker that is currently mission editor tanking +local groupName = 'TankerClone' -- Name of the group in the ME +local cloneGroupPos = Unit.getByName(groupName):getPosition().p +cvn = Unit.getByName("CVN") +local cvnPos = cvn:getPosition().p + + +group = mist.getGroupData(groupName) +group.route = { + ["points"] = + { + [1] = + { + ["alt"] = 2133.6, + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["speed"] = 179.86111111111, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = false, + ["id"] = "ControlledTask", + ["enabled"] = true, + ["params"] = + { + ["task"] = + { + ["id"] = "Tanker", + ["params"] = + { + }, -- end of ["params"] + }, -- end of ["task"] + ["stopCondition"] = + { + ["duration"] = 900, + }, -- end of ["stopCondition"] + }, -- end of ["params"] + }, -- end of [1] + [2] = + { + ["number"] = 2, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "ActivateBeacon", + ["params"] = + { + ["type"] = 4, + ["AA"] = false, + ["callsign"] = "TKR", + ["modeChannel"] = "Y", + ["channel"] = 71, + ["system"] = 5, + ["unitId"] = 188, + ["bearing"] = true, + ["frequency"] = 1032000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [2] + [3] = + { + ["number"] = 3, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetFrequency", + ["params"] = + { + ["power"] = 10, + ["modulation"] = 0, + ["frequency"] = 260000000, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [3] + [4] = + { + ["number"] = 4, + ["auto"] = false, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "SetInvisible", + ["params"] = + { + ["value"] = true, + }, -- end of ["params"] + }, -- end of ["action"] + }, -- end of ["params"] + }, -- end of [4] + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["type"] = "Turning Point", + ["ETA"] = 96.50677034026, + ["ETA_locked"] = false, + ["y"] = cvnPos.z, + ["x"] = cvnPos.x, + ["formation_template"] = "", + ["speed_locked"] = true, + }, -- end of [1] + }, -- end of ["points"] + } + +--group.units[1].type = "S-3B Tanker" +group.groupName = "Tanker1" +group.groupId = nil +group.units[1].unitId = nil +group.units[1].unitName = newName +group.country = country +group.category = 'AIRPLANE' +group.units[1].x = cvnPos.x-100 +group.units[1].y = cvnPos.z +group.units[1].z = cloneGroupPos.y +group.units[1].heading = 0.000000000001 +group.units[1].speed = 300 + +--Group.destroy(Group.getByName(groupName)) +mist.dynAdd(group) +group.groupName = "Tanker2" +group.units[1].x = cvnPos.x+100 +group.units[1].heading = 3.1415926537 +group.units[1].y = cvnPos.z +mist.dynAdd(group) + +end + + +handler = {} + +local function protectedCall(...) + local status, retval = pcall(...) + if not status then + + end +end + +function tankers.eventHandler (event) + if (26 == event.id) then --this is when someone types into a mark + local vec3 = mist.utils.makeVec3GL(event.pos) + tankers.startEnrouteTankingTest (vec3) + end +end + +function handler:onEvent(event) + protectedCall(tankers.eventHandler, event) +end + +do + --world.addEventHandler(handler) + world.addEventHandler(handler) +end + +do + longRangeShots = missionCommands.addSubMenu("Dynamic Tanking") + missionCommands.addCommand ("Hyperspace entry", longRangeShots, tankers.hyperSpace) + missionCommands.addCommand ("Start tanking", longRangeShots, tankers.startEnrouteTankingTest) + missionCommands.addCommand ("Frequency change approved", longRangeShots, tankers.setFrequency) + missionCommands.addCommand ("Callsign change approved", longRangeShots, tankers.changeCallsign) + missionCommands.addCommand ("Tacan change approved", longRangeShots, tankers.setTacan) + missionCommands.addCommand ("Start a new tanker", longRangeShots, tankers.newTanker) + missionCommands.addCommand ("Rejoin on a unit", longRangeShots, tankers.followInFront) + missionCommands.addCommand ("Rejoin close", longRangeShots, tankers.followInFrontClose) + + +end + +tankers.notify("tankers.lua loaded",2) \ No newline at end of file diff --git a/scripts/forceBubble.lua b/scripts/forceBubble.lua new file mode 100644 index 00000000..803e887b --- /dev/null +++ b/scripts/forceBubble.lua @@ -0,0 +1,112 @@ +--Spawn a SAM integrated with IADS +--Spawn a normal SAM +--SAM bubble shields + +forceBub = {} +forceBub.handler = {} +forceBub.missileList = {} +forceBub.missilesActive = 0 +forceBub.shieldOn = false + +function forceBub.notify(message, displayFor) + trigger.action.outText(message, displayFor, true) +end + +function forceBub.setShield() + forceBub.notify("Shield on", 2) +end + +function forceBub.stopShield() + forceBub.shieldOn = false +end + + + +local function protectedCall(...) + local status, retval = pcall(...) + if not status then + --rf.notify("Caught error " .. retval,2) + end +end + +function forceBub.handler:onEvent(event) + protectedCall(forceBub.eventHandler, event) +end + +function forceBub.checkMissiles () + local currentTime = timer.getTime() + if forceBub.missilesActive > 0 then + for index, data in pairs(forceBub.missileList) do + output = mist.utils.tableShow(forceBub.missileList[index]) + if forceBub.missileList[index].exists == true then + if Object.isExist(forceBub.missileList[index].weapon) == true then + forceBub.missileList[index].pos = forceBub.missileList[index].weapon:getPosition() + + + local missilePosition = forceBub.missileList[index].pos.p + + unit = Unit.getByName("Test") + local unitPosition = unit:getPosition().p + + local distance = mist.utils.get3DDist(unitPosition , missilePosition ) + forceBub.notify(distance,1) + + if forceBub.shieldOn == true and distance < 100 then --this distance is the sweet spot any less and you probably take damage and die, less than 75 death + + trigger.action.explosion(missilePosition , 1) --just blows up the missile + + end + + + else + forceBub.missileList[index] = nil + forceBub.missilesActive = forceBub.missilesActive - 1 + end + else + end + + end + end + timer.scheduleFunction(forceBub.checkMisProtectCall,{},currentTime + 0.01) +end + + + +function forceBub.eventHandler (event) + --forceBub.notify(mist.utils.tableShow(event),10) + if (event.id == 1) then + --check if weapon is a missile + --rf.notify("Missile fired id " .. event.weapon.id_ ,2) + forceBub.notify(mist.utils.tableShow(Weapon.getDesc(event.weapon)),10) + if Weapon.getDesc(event.weapon).missileCategory == 2 then + local newMis = {} + newMis.id = event.weapon.id_ + newMis.pos = event.weapon:getPosition() + newMis.weapon = event.weapon + newMis.exists = Object.isExist(newMis.weapon) + forceBub.missileList[event.weapon.id_] = newMis + forceBub.missilesActive = forceBub.missilesActive + 1 + end + end +end + +function forceBub.checkMisProtectCall() + protectedCall(forceBub.checkMissiles,{}) +end + +function forceBub.setShield() + forceBub.shieldOn = true +end + +do + forceField = missionCommands.addSubMenu("Force Field") + missionCommands.addCommand ("Forcefield on", forceField, forceBub.setShield) + missionCommands.addCommand ("Stop Field", forceField, forceBub.stopShield) +end + +do + world.addEventHandler(forceBub.handler) +end + +protectedCall(forceBub.checkMissiles,{}) +forceBub.notify("forceBubble.lua loaded", 2) \ No newline at end of file diff --git a/scripts/laserDiscoDiscoLaser.lua b/scripts/laserDiscoDiscoLaser.lua new file mode 100644 index 00000000..f6b2ffde --- /dev/null +++ b/scripts/laserDiscoDiscoLaser.lua @@ -0,0 +1,21 @@ +lddl = {} +lddl.refreshRate = 1 + + + + +function lddl.pointer () + origin = Unit.getByName("Laser") + originPos = origin:getPosition().p + targetPos = Unit.getByName("LaserTGT"):getPosition().p + laser = Spot.createInfraRed(origin , originPos, targetPos) + timer.scheduleFunction(lddl.removePointer,laser, timer.getTime() + lddl.refreshRate) + +end + +function lddl.removePointer(laser) + Spot.destroy(laser) + timer.scheduleFunction(lddl.pointer,{}, timer.getTime() + lddl.refreshRate) +end + +lddl.pointer () \ No newline at end of file diff --git a/scripts/mist_4_4_90.lua b/scripts/mist.lua similarity index 73% rename from scripts/mist_4_4_90.lua rename to scripts/mist.lua index fbf4f6ac..431848d8 100644 --- a/scripts/mist_4_4_90.lua +++ b/scripts/mist.lua @@ -34,17 +34,19 @@ mist = {} -- don't change these mist.majorVersion = 4 -mist.minorVersion = 4 -mist.build = 90 +mist.minorVersion = 5 +mist.build = 107 -- forward declaration of log shorthand local log - +local dbLog + local mistSettings = { errorPopup = false, -- errors printed by mist logger will create popup warning you warnPopup = false, infoPopup = false, logLevel = 'warn', + dbLog = 'warn', } do -- the main scope @@ -73,11 +75,11 @@ do -- the main scope mist.nextGroupId = 1 mist.nextUnitId = 1 - local dbLog + local function initDBs() -- mist.DBs scope mist.DBs = {} - + mist.DBs.markList = {} mist.DBs.missionData = {} if env.mission then @@ -106,6 +108,25 @@ do -- the main scope zone.point.x = zone_data.x zone.point.y = 0 zone.point.z = zone_data.y + zone.properties = {} + if zone_data.properties then + for propInd, prop in pairs(zone_data.properties) do + if prop.value and type(prop.value) == 'string' and prop.value ~= "" then + zone.properties[prop.key] = prop.value + end + end + end + if zone.verticies then -- trust but verify + local r = 0 + for i = 1, #zone.verticies do + local dist = mist.utils.get2DDist(zone.point, zone.verticies[i]) + if dist > r then + r = mist.utils.deepCopy(dist) + end + end + zone.radius = r + + end mist.DBs.zonesByName[zone_data.name] = zone mist.DBs.zonesByNum[#mist.DBs.zonesByNum + 1] = mist.utils.deepCopy(zone) --[[deepcopy so that the zone in zones_by_name and the zone in @@ -113,6 +134,85 @@ do -- the main scope end end end + + mist.DBs.drawingByName = {} + mist.DBs.drawingIndexed = {} + + if env.mission.drawings and env.mission.drawings.layers then + for i = 1, #env.mission.drawings.layers do + local l = env.mission.drawings.layers[i] + + for j = 1, #l.objects do + local copy = mist.utils.deepCopy(l.objects[j]) + --log:warn(copy) + local doOffset = false + copy.layer = l.name + + local theta = copy.angle or 0 + theta = math.rad(theta) + if copy.primitiveType == "Polygon" then + + if copy.polygonMode == 'rect' then + local h, w = copy.height, copy.width + copy.points = {} + copy.points[1] = {x = h/2, y = w/2} + copy.points[2] = {x = -h/2, y = w/2} + copy.points[3] = {x = -h/2, y = -w/2} + copy.points[4] = {x = h/2, y = -w/2} + doOffset = true + elseif copy.polygonMode == "circle" then + copy.points = {x = copy.mapX, y = copy.mapY} + elseif copy.polygonMode == 'oval' then + copy.points = {} + local numPoints = 24 + local angleStep = (math.pi*2)/numPoints + doOffset = true + for v = 1, numPoints do + local pointAngle = v * angleStep + local x = copy.r1 * math.cos(pointAngle) + local y = copy.r2 * math.sin(pointAngle) + + table.insert(copy.points,{x=x,y=y}) + + end + elseif copy.polygonMode == "arrow" then + doOffset = true + end + + + if theta ~= 0 and copy.points and doOffset == true then + + --log:warn('offsetting Values') + for p = 1, #copy.points do + local offset = mist.vec.rotateVec2(copy.points[p], theta) + copy.points[p] = offset + end + --log:warn(copy.points[1]) + end + + elseif copy.primitiveType == "Line" and copy.closed == true then + table.insert(copy.points, mist.utils.deepCopy(copy.points[1])) + end + if copy.points and #copy.points > 1 then + for u = 1, #copy.points do + copy.points[u].x = mist.utils.round(copy.points[u].x + copy.mapX, 2) + copy.points[u].y = mist.utils.round(copy.points[u].y + copy.mapY, 2) + end + + end + if mist.DBs.drawingByName[copy.name] then + log:warn("Drawing by the name of [ $1 ] already exists in DB. Failed to add to mist.DBs.drawingByName.", copy.name) + else + + mist.DBs.drawingByName[copy.name] = copy + end + table.insert(mist.DBs.drawingIndexed, copy) + end + + end + + end + mist.DBs.navPoints = {} mist.DBs.units = {} @@ -151,28 +251,31 @@ do -- the main scope for cntry_id, cntry_data in pairs(coa_data.country) do local countryName = string.lower(cntry_data.name) + if cntry_data.id and country.names[cntry_data.id] then + countryName = string.lower(country.names[cntry_data.id]) + end mist.DBs.units[coa_name][countryName] = {} mist.DBs.units[coa_name][countryName].countryId = cntry_data.id if type(cntry_data) == 'table' then --just making sure - for obj_type_name, obj_type_data in pairs(cntry_data) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check - local category = obj_type_name + local category = obj_cat_name - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! mist.DBs.units[coa_name][countryName][category] = {} - for group_num, group_data in pairs(obj_type_data.group) do + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group mist.DBs.units[coa_name][countryName][category][group_num] = {} local groupName = group_data.name - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then groupName = env.getValueDictByKey(groupName) end mist.DBs.units[coa_name][countryName][category][group_num].groupName = groupName @@ -196,7 +299,7 @@ do -- the main scope local units_tbl = mist.DBs.units[coa_name][countryName][category][group_num].units --pointer to the units table for this group units_tbl[unit_num] = {} - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then units_tbl[unit_num].unitName = env.getValueDictByKey(unit_data.name) else units_tbl[unit_num].unitName = unit_data.name @@ -240,6 +343,7 @@ do -- the main scope if category == 'static' then units_tbl[unit_num].categoryStatic = unit_data.category units_tbl[unit_num].shape_name = unit_data.shape_name + units_tbl[unit_num].linkUnit = unit_data.linkUnit if unit_data.mass then units_tbl[unit_num].mass = unit_data.mass end @@ -251,10 +355,10 @@ do -- the main scope end --for unit_num, unit_data in pairs(group_data.units) do end --if group_data and group_data.units then - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --if type(cntry_data) == 'table' then end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table @@ -305,6 +409,11 @@ do -- the main scope ['Arco'] = 2, ['Shell'] = 3, }, + ['TRANSPORT'] = { + ['Heavy'] = 9, + ['Trash'] = 10, + ['Cargo'] = 11, + ['Ascot'] = 12, ['JTAC'] = { ['Axeman'] = 1, ['Darknight'] = 2, @@ -346,14 +455,105 @@ do -- the main scope ['rules'] = { ['canUseAircraft'] = true, ['appliesTo'] = { - 'A-10C', + 'A-10C_2', + 'A-10C', 'A-10A', }, }, - }, - }, - }, - } + }, + ['f16'] = { + Viper = 9, + Venom = 10, + Lobo = 11, + Cowboy = 12, + Python = 13, + Rattler =14, + Panther = 15, + Wolf = 16, + Weasel = 17, + Wild = 18, + Ninja = 19, + Jedi = 20, + rules = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'F-16C_50', + 'F-16C bl.52d', + 'F-16C bl.50', + 'F-16A MLU', + 'F-16A', + }, + }, + + }, + ['f18'] = { + ['Hornet'] = 9, + ['Squid'] = 10, + ['Ragin'] = 11, + ['Roman'] = 12, + Sting = 13, + Jury =14, + Jokey = 15, + Ram = 16, + Hawk = 17, + Devil = 18, + Check = 19, + Snake = 20, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + + "FA-18C_hornet", + 'F/A-18C', + }, + }, + }, + ['b1'] = { + ['Bone'] = 9, + ['Dark'] = 10, + ['Vader'] = 11, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'B-1B', + }, + }, + }, + ['b52'] = { + ['Buff'] = 9, + ['Dump'] = 10, + ['Kenworth'] = 11, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'B-52H', + }, + }, + }, + ['f15e'] = { + ['Dude'] = 9, + ['Thud'] = 10, + ['Gunny'] = 11, + ['Trek'] = 12, + Sniper = 13, + Sled =14, + Best = 15, + Jazz = 16, + Rage = 17, + Tahoe = 18, + ['rules'] = { + ['canUseAircraft'] = true, + ['appliesTo'] = { + 'F-15E', + --'F-15ERAZBAM', + }, + }, + }, + + }, + }, + }, + } mist.DBs.const.shapeNames = { ["Landmine"] = "landmine", ["FARP CP Blindage"] = "kp_ug", @@ -443,6 +643,42 @@ do -- the main scope ["Small house 1A area"] = "domik1a-all", ["White_Flag"] = "H-Flag_W", ["Airshow_Cone"] = "Comp_cone", + ["Bulk Cargo Ship Ivanov"] = "barge-1", + ["Bulk Cargo Ship Yakushev"] = "barge-2", + ["Outpost"]="block", + ["Road outpost"]="block-onroad", + ["Container camo"] = "bw_container_cargo", + ["Tech Hangar A"] = "ceh_ang_a", + ["Bunker 1"] = "dot", + ["Bunker 2"] = "dot2", + ["Tanker Elnya 160"] = "elnya", + ["F-shape barrier"] = "f_bar_cargo", + ["Helipad Single"] = "farp", + ["FARP"] = "farps", + ["Fueltank"] = "fueltank_cargo", + ["Gate"] = "gate", + ["FARP Fuel Depot"] = "gsm rus", + ["Armed house"] = "home1_a", + ["FARP Command Post"] = "kp-ug", + ["Watch Tower Armed"] = "ohr-vyshka", + ["Oiltank"] = "oiltank_cargo", + ["Pipes small"] = "pipes_small_cargo", + ["Pipes big"] = "pipes_big_cargo", + ["Oil platform"] = "plavbaza", + ["Tetrapod"] = "tetrapod_cargo", + ["Fuel tank"] = "toplivo", + ["Trunks long"] = "trunks_long_cargo", + ["Trunks small"] = "trunks_small_cargo", + ["Passenger liner"] = "yastrebow", + ["Passenger boat"] = "zwezdny", + ["Oil rig"] = "oil_platform", + ["Gas platform"] = "gas_platform", + ["Container 20ft"] = "container_20ft", + ["Container 40ft"] = "container_40ft", + ["Downed pilot"] = "cadaver", + ["Parachute"] = "parash", + ["Pilot F15 Parachute"] = "pilot_f15_parachute", + ["Pilot standing"] = "pilot_parashut", } @@ -504,8 +740,8 @@ do -- the main scope mist.DBs.MEgroupsById = mist.utils.deepCopy(mist.DBs.groupsById) mist.DBs.deadObjects = {} - - do + + do local mt = {} function mt.__newindex(t, key, val) @@ -649,9 +885,14 @@ do -- the main scope newTable.category = 'static' else unitOneRef = newObject:getUnits() - newTable.countryId = tonumber(unitOneRef[1]:getCountry()) - newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) - newTable.category = tonumber(newObject:getCategory()) + if #unitOneRef > 0 and unitOneRef[1] and type(unitOneRef[1]) == 'table' then + newTable.countryId = tonumber(unitOneRef[1]:getCountry()) + newTable.coalitionId = tonumber(unitOneRef[1]:getCoalition()) + newTable.category = tonumber(newObject:getCategory()) + else + log:warn('getUnits failed to return on $1 ; Built Data: $2.', event, newTable) + return false + end end for countryData, countryId in pairs(country.id) do if newTable.country and string.upper(countryData) == string.upper(newTable.country) or countryId == newTable.countryId then @@ -782,6 +1023,8 @@ do -- the main scope newTable.units[1].canCargo = data.canCargo newTable.units[1].categoryStatic = data.categoryStatic newTable.units[1].type = data.type + newTable.units[1].linkUnit = data.linkUnit + mistAddedObjects[index] = nil break end @@ -818,6 +1061,7 @@ do -- the main scope --dbLog:info('iterate') for name, gData in pairs(tempSpawnedGroups) do --env.info(name) + --dbLog:info(gData) local updated = false local stillExists = false if not gData.checked then @@ -825,9 +1069,9 @@ do -- the main scope local _g = gData.gp or Group.getByName(name) if mist.DBs.groupsByName[name] then -- first check group level properties, groupId, countryId, coalition - -- dbLog:info('Found in DBs, check if updated') + --dbLog:info('Found in DBs, check if updated') local dbTable = mist.DBs.groupsByName[name] - -- dbLog:info(dbTable) + --dbLog:info(dbTable) if gData.type ~= 'static' then -- dbLog:info('Not static') @@ -848,11 +1092,11 @@ do -- the main scope end --dbLog:info('Updated: $1', updated) if updated == false and gData.type ~= 'static' then -- time to check units - --dbLog:info('No Group Mismatch, Check Units') + --dbLog:info('No Group Mismatch, Check Units') if _g and _g:isExist() == true then stillExists = true for index, uObject in pairs(_g:getUnits()) do - --dbLog:info(index) + --dbLog:info(index) if mist.DBs.unitsByName[uObject:getName()] then --dbLog:info('UnitByName table exists') local uTable = mist.DBs.unitsByName[uObject:getName()] @@ -870,8 +1114,10 @@ do -- the main scope if stillExists == true and (updated == true or not mist.DBs.groupsByName[name]) then --dbLog:info('Get Table') - writeGroups[#writeGroups+1] = {data = dbUpdate(name, gData.type), isUpdated = updated} - + local dbData = dbUpdate(name, gData.type) + if dbData and type(dbData) == 'table' then + writeGroups[#writeGroups+1] = {data = dbData, isUpdated = updated} + end end -- Work done, so remove end @@ -998,19 +1244,20 @@ do -- the main scope local function groupSpawned(event) -- dont need to add units spawned in at the start of the mission if mist is loaded in init line if event.id == world.event.S_EVENT_BIRTH and timer.getTime0() < timer.getAbsTime() then - --dbLog:info('unitSpawnEvent') - + --log:info('unitSpawnEvent') + --log:info(event) + --log:info(event.initiator:getTypeName()) --table.insert(tempSpawnedUnits,(event.initiator)) ------- -- New functionality below. ------- if Object.getCategory(event.initiator) == 1 and not Unit.getPlayerName(event.initiator) then -- simple player check, will need to later check to see if unit was spawned with a player in a flight - --dbLog:info('Object is a Unit') + --log:info('Object is a Unit') if Unit.getGroup(event.initiator) then - --dbLog:info(Unit.getGroup(event.initiator):getName()) + -- log:info(Unit.getGroup(event.initiator):getName()) local g = Unit.getGroup(event.initiator) if not tempSpawnedGroups[g:getName()] then - --dbLog:info('added') + --log:info('added') tempSpawnedGroups[g:getName()] = {type = 'group', gp = g} tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 end @@ -1018,7 +1265,7 @@ do -- the main scope log:error('Group not accessible by unit in event handler. This is a DCS bug') end elseif Object.getCategory(event.initiator) == 3 or Object.getCategory(event.initiator) == 6 then - --dbLog:info('Object is Static') + --log:info('Object is Static') tempSpawnedGroups[StaticObject.getName(event.initiator)] = {type = 'static'} tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 end @@ -1169,10 +1416,13 @@ do -- the main scope for i = 1, #st do local s = st[i] if StaticObject.isExist(s) then - if not mist.DBs.unitsByName[s:getName()] then - --env.info(StaticObject.getID(s) .. ' Not found in DB yet') - tempSpawnedGroups[s:getName()] = {type = 'static'} - tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + local name = s:getName() + if not mist.DBs.unitsByName[name] then + dbLog:warn('$1 Not found in DB yet. ID: $2', name, StaticObject.getID(s)) + if string.len(name) > 0 then -- because in this mission someone sent the name was returning as an empty string. Gotta be careful. + tempSpawnedGroups[s:getName()] = {type = 'static'} + tempSpawnGroupsCounter = tempSpawnGroupsCounter + 1 + end end end end @@ -1180,6 +1430,7 @@ do -- the main scope end end + --- init function. -- creates logger, adds default event handler @@ -1284,8 +1535,9 @@ do -- the main scope --- Spawns a static object to the game world. -- @todo write good docs -- @tparam table staticObj table containing data needed for the object creation - function mist.dynAddStatic(newObj) - log:info(newObj) + function mist.dynAddStatic(n) + --log:info(newObj) + local newObj = mist.utils.deepCopy(n) if newObj.units and newObj.units[1] then -- if its mist format for entry, val in pairs(newObj.units[1]) do if newObj[entry] and newObj[entry] ~= val or not newObj[entry] then @@ -1366,7 +1618,7 @@ do -- the main scope mistAddedObjects[#mistAddedObjects + 1] = mist.utils.deepCopy(newObj) if newObj.x and newObj.y and newObj.type and type(newObj.x) == 'number' and type(newObj.y) == 'number' and type(newObj.type) == 'string' then - log:info(newObj) + --log:warn(newObj) coalition.addStaticObject(country.id[newCountry], newObj) return newObj @@ -1379,8 +1631,10 @@ do -- the main scope -- Same as coalition.add function in SSE. checks the passed data to see if its valid. -- Will generate groupId, groupName, unitId, and unitName if needed -- @tparam table newGroup table containting values needed for spawning a group. - function mist.dynAdd(newGroup) - + function mist.dynAdd(ng) + + local newGroup = mist.utils.deepCopy(ng) + --log:warn(newGroup) --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupOrig.lua') local cntry = newGroup.country if newGroup.countryId then @@ -1452,7 +1706,11 @@ do -- the main scope end if newGroup.clone and mist.DBs.groupsByName[newGroup.name] or not newGroup.name then - newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) + --if newGroup.baseName then + -- idea of later. So custmozed naming can be created + -- else + newGroup.name = tostring(newCountry .. tostring(typeName) .. mistDynAddIndex[typeName]) + --end end if not newGroup.hidden then @@ -1537,16 +1795,32 @@ do -- the main scope end end else -- if aircraft and no route assigned. make a quick and stupid route so AI doesnt RTB immediately - if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then + --if newCat == 'AIRPLANE' or newCat == 'HELICOPTER' then newGroup.route = {} newGroup.route.points = {} newGroup.route.points[1] = {} - end + --end end newGroup.country = newCountry - - --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroup.lua') + -- update and verify any self tasks + if newGroup.route and newGroup.route.points then + for i, pData in pairs(newGroup.route.points) do + if pData.task and pData.task.params and pData.task.params.tasks and #pData.task.params.tasks > 0 then + for tIndex, tData in pairs(pData.task.params.tasks) do + if tData.params and tData.params.action then + if tData.params.action.id == "EPLRS" then + tData.params.action.params.groupId = newGroup.groupId + elseif tData.params.action.id == "ActivateBeacon" or tData.params.action.id == "ActivateICLS" then + tData.params.action.params.unitId = newGroup.units[1].unitId + end + end + end + end + + end + end + --mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua') --log:warn(newGroup) -- sanitize table newGroup.groupName = nil @@ -1559,7 +1833,7 @@ do -- the main scope for unitIndex, unitData in pairs(newGroup.units) do newGroup.units[unitIndex].unitName = nil end - + coalition.addGroup(country.id[newCountry], Unit.Category[newCat], newGroup) return newGroup @@ -1816,10 +2090,10 @@ do if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for if group_data.route and group_data.route.points and #group_data.route.points > 0 then local points = {} @@ -1834,10 +2108,10 @@ do end return end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then @@ -2214,9 +2488,25 @@ do --- Returns a table containing unit names. -- @tparam table tbl sequential strings -- @treturn table @{UnitNameTable} - function mist.makeUnitTable(tbl) + function mist.makeUnitTable(tbl, exclude) --Assumption: will be passed a table of strings, sequential --log:info(tbl) + + + local excludeType = {} + if exclude then + if type(exclude) == 'table' then + for x, y in pairs(exclude) do + excludeType[x] = true + excludeType[y] = true + end + else + excludeType[exclude] = true + end + + end + + local units_by_name = {} local l_munits = mist.DBs.units --local reference for faster execution @@ -2277,12 +2567,15 @@ do elseif unit:sub(4,12) == '[vehicle]' then category = 'vehicle' country_start = 13 + elseif unit:sub(4, 11) == '[static]' then + category = 'static' + country_start = 12 end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do if country == string.lower(unit:sub(country_start)) then -- match for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2310,12 +2603,15 @@ do elseif unit:sub(5,13) == '[vehicle]' then category = 'vehicle' country_start = 14 + elseif unit:sub(5, 12) == '[static]' then + category = 'static' + country_start = 13 end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do if country == string.lower(unit:sub(country_start)) then -- match for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2340,12 +2636,14 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' - end + elseif unit:sub(7) == '[static]' then + category = 'static' + end for coa, coa_tbl in pairs(l_munits) do if coa == 'blue' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2368,12 +2666,14 @@ do category = 'ship' elseif unit:sub(8) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(8) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'blue' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2398,12 +2698,14 @@ do category = 'ship' elseif unit:sub(6) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(6) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'red' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2426,12 +2728,14 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(7) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do if coa == 'red' then for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2456,11 +2760,13 @@ do category = 'ship' elseif unit:sub(6) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(6) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2482,11 +2788,13 @@ do category = 'ship' elseif unit:sub(7) == '[vehicle]' then category = 'vehicle' + elseif unit:sub(7) == '[static]' then + category = 'static' end for coa, coa_tbl in pairs(l_munits) do for country, country_table in pairs(coa_tbl) do for unit_type, unit_type_tbl in pairs(country_table) do - if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) then + if type(unit_type_tbl) == 'table' and (category == '' or unit_type == category) and not excludeType[unit_type] then for group_ind, group_tbl in pairs(unit_type_tbl) do if type(group_tbl) == 'table' then for unit_ind, unit in pairs(group_tbl.units) do @@ -2517,6 +2825,126 @@ do return units_tbl end +function mist.getUnitsByAttribute(att, rnum, id) + local cEntry = {} + cEntry.typeName = att.type or att.typeName or att.typename + cEntry.country = att.country + cEntry.coalition = att.coalition + cEntry.skill = att.skill + cEntry.categry = att.category + + local num = rnum or 1 + + if cEntry.skill == 'human' then + cEntry.skill = {'Client', 'Player'} + end + + + local checkedVal = {} + local units = {} + for uName, uData in pairs(mist.DBs.unitsByName) do + local matched = 0 + for cName, cVal in pairs(cEntry) do + if type(cVal) == 'table' then + for sName, sVal in pairs(cVal) do + if (uData[cName] and uData[cName] == sVal) or (uData[cName] and uData[cName] == sName) then + matched = matched + 1 + end + end + else + if uData[cName] and uData[cName] == cVal then + matched = matched + 1 + end + end + end + if matched >= num then + if id then + units[uData.unitId] = true + else + + units[uName] = true + end + end + end + + local rtn = {} + for name, _ in pairs(units) do + table.insert(rtn, name) + end + return rtn + +end + +function mist.getGroupsByAttribute(att, rnum, id) + local cEntry = {} + cEntry.typeName = att.type or att.typeName or att.typename + cEntry.country = att.country + cEntry.coalition = att.coalition + cEntry.skill = att.skill + cEntry.categry = att.category + + local num = rnum or 1 + + if cEntry.skill == 'human' then + cEntry.skill = {'Client', 'Player'} + end + local groups = {} + for gName, gData in pairs(mist.DBs.groupsByName) do + local matched = 0 + for cName, cVal in pairs(cEntry) do + if type(cVal) == 'table' then + for sName, sVal in pairs(cVal) do + if cName == 'skill' or cName == 'typeName' then + local lMatch = 0 + for uId, uData in pairs(gData.units) do + if (uData[cName] and uData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then + lMatch = lMatch + 1 + break + end + end + if lMatch > 0 then + matched = matched + 1 + end + end + if (gData[cName] and gData[cName] == sVal) or (gData[cName] and gData[cName] == sName) then + matched = matched + 1 + break + end + end + else + if cName == 'skill' or cName == 'typeName' then + local lMatch = 0 + for uId, uData in pairs(gData.units) do + if (uData[cName] and uData[cName] == sVal) then + lMatch = lMatch + 1 + break + end + end + if lMatch > 0 then + matched = matched + 1 + end + end + if gData[cName] and gData[cName] == cVal then + matched = matched + 1 + end + end + end + if matched >= num then + if id then + groups[gData.groupid] = true + else + groups[gName] = true + end + end + end + local rtn = {} + for name, _ in pairs(groups) do + table.insert(rtn, name) + end + return rtn + +end + function mist.getDeadMapObjsInZones(zone_names) -- zone_names: table of zone names -- returns: table of dead map objects (indexed numerically) @@ -2552,6 +2980,160 @@ function mist.getDeadMapObjsInPolygonZone(zone) end return map_objs end +mist.shape = {} +function mist.shape.insideShape(shape1, shape2, full) + if shape1.radius then -- probably a circle + if shape2.radius then + return mist.shape.circleInCircle(shape1, shape2, full) + elseif shape2[1] then + return mist.shape.circleInPoly(shape1, shape2, full) + end + + elseif shape1[1] then -- shape1 is probably a polygon + if shape2.radius then + return mist.shape.polyInCircle(shape1, shape2, full) + elseif shape2[1] then + return mist.shape.polyInPoly(shape1, shape2, full) + end + end + return false +end + +function mist.shape.circleInCircle(c1, c2, full) + if not full then -- quick partial check + if mist.utils.get2DDist(c1.point, c2.point) <= c2.radius then + return true + end + end + local theta = mist.utils.getHeadingPoints(c2.point, c1.point) -- heading from + if full then + return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta), c2.point) <= c2.radius + else + return mist.utils.get2DDist(mist.projectPoint(c1.point, c1.radius, theta + math.pi), c2.point) <= c2.radius + end + return false +end + + +function mist.shape.circleInPoly(circle, poly, full) + + if poly and type(poly) == 'table' and circle and type(circle) == 'table' and circle.radius and circle.point then + if not full then + for i = 1, #poly do + if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then + return true + end + end + end + -- no point is inside of the zone, now check if any part is + local count = 0 + for i = 1, #poly do + local theta -- heading of each set of points + if i == #poly then + theta = mist.utils.getHeadingPoints(poly[i],poly[1]) + else + theta = mist.utils.getHeadingPoints(poly[i],poly[i+1]) + end + -- offset + local pPoint = mist.projectPoint(circle.point, circle.radius, theta - (math.pi/180)) + local oPoint = mist.projectPoint(circle.point, circle.radius, theta + (math.pi/180)) + + + if mist.pointInPolygon(pPoint, poly) == true then + if (full and mist.pointInPolygon(oPoint, poly) == true) or not full then + return true + + end + + end + end + + end + return false +end + + +function mist.shape.polyInPoly(p1, p2, full) + local count = 0 + for i = 1, #p1 do + + if mist.pointInPolygon(p1[i], p2) then + count = count + 1 + end + if (not full) and count > 0 then + return true + end + end + if count == #p1 then + return true + end + + return false +end + +function mist.shape.polyInCircle(poly, circle, full) + local count = 0 + for i = 1, #poly do + if mist.utils.get2DDist(circle.point, poly[i]) <= circle.radius then + if full then + count = count + 1 + else + return true + end + end + end + if count == #poly then + return true + end + + return false +end + +function mist.shape.getPointOnSegment(point, seg, isSeg) + local p = mist.utils.makeVec2(point) + local s1 = mist.utils.makeVec2(seg[1]) + local s2 = mist.utils.makeVec2(seg[2]) + + + local cx, cy = p.x - s1.x, p.y - s1.y + local dx, dy = s2.x - s1.x, s2.x - s1.y + local d = (dx*dx + dy*dy) + + if d == 0 then + return {x = s1.x, y = s1.y} + end + local u = (cx*dx + cy*dy)/d + if isSeg then + if u < 0 then + u = 0 + elseif u > 1 then + u = 1 + end + end + return {x = s1.x + u*dx, y = s1.y + u*dy} +end + + +function mist.shape.segmentIntersect(segA, segB) + local dx1, dy1 = segA[2].x - segA[1].x, segA[2] - segA[1].y + local dx2, dy2 = segB[2].x - segB[1].x, segB[2] - segB[1].y + local dx3, dy3 = segA[1].x - segB[1].x, segA[1].y - segB[1].y + local d = dx1*dy2 - dy1*dx2 + if d == 0 then + return false + end + local t1 = (dx2*dy3 - dy2*dx3)/d + if t1 < 0 or t1 > 1 then + return false + end + local t2 = (dx1*dy3 - dy1*dx3)/d + if t2 < 0 or t2 > 1 then + return false + end + -- point of intersection + return true, segA[1].x + t1*dx1, segA[1].y + t1*dy1 +end + function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm --[[local type_tbl = { @@ -2591,17 +3173,23 @@ function mist.pointInPolygon(point, poly, maxalt) --raycasting point in polygon. end end +function mist.mapValue(val, inMin, inMax, outMin, outMax) + return (val - inMin) * (outMax - outMin) / (inMax - inMin) + outMin +end + function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) local units = {} for i = 1, #unit_names do - units[#units + 1] = Unit.getByName(unit_names[i]) + units[#units + 1] = Unit.getByName(unit_names[i]) or StaticObject.getByName(unit_names[i]) end local inZoneUnits = {} for i =1, #units do - if units[i]:isActive() and mist.pointInPolygon(units[i]:getPosition().p, polyZone, max_alt) then - inZoneUnits[#inZoneUnits + 1] = units[i] + local lUnit = units[i] + local lCat = lUnit:getCategory() + if ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) and mist.pointInPolygon(lUnit:getPosition().p, polyZone, max_alt) then + inZoneUnits[#inZoneUnits + 1] = lUnit end end @@ -2609,8 +3197,7 @@ function mist.getUnitsInPolygon(unit_names, polyZone, max_alt) end function mist.getUnitsInZones(unit_names, zone_names, zone_type) - - zone_type = zone_type or 'cylinder' + zone_type = zone_type or 'cylinder' if zone_type == 'c' or zone_type == 'cylindrical' or zone_type == 'C' then zone_type = 'cylinder' end @@ -2622,9 +3209,13 @@ function mist.getUnitsInZones(unit_names, zone_names, zone_type) local units = {} local zones = {} - + + if zone_names and type(zone_names) == 'string' then + zone_names = {zone_names} + end for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) + + local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k]) if unit then units[#units + 1] = unit end @@ -2632,31 +3223,40 @@ function mist.getUnitsInZones(unit_names, zone_names, zone_type) for k = 1, #zone_names do - local zone = trigger.misc.getZone(zone_names[k]) + local zone = mist.DBs.zonesByName[zone_names[k]] if zone then - zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z} + zones[#zones + 1] = {radius = zone.radius, x = zone.point.x, y = zone.point.y, z = zone.point.z, verts = zone.verticies} end end local in_zone_units = {} - for units_ind = 1, #units do - for zones_ind = 1, #zones do + local lUnit = units[units_ind] + local unit_pos = lUnit:getPosition().p + local lCat = lUnit:getCategory() + for zones_ind = 1, #zones do if zone_type == 'sphere' then --add land height value for sphere zone type local alt = land.getHeight({x = zones[zones_ind].x, y = zones[zones_ind].z}) if alt then zones[zones_ind].y = alt end end - local unit_pos = units[units_ind]:getPosition().p - if unit_pos and units[units_ind]:isActive() == true then - if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] - break - end + + if unit_pos and ((lCat == 1 and lUnit:isActive() == true) or lCat ~= 1) then -- it is a unit and is active or it is not a unit + if zones[zones_ind].verts then + if mist.pointInPolygon(unit_pos, zones[zones_ind].verts) then + in_zone_units[#in_zone_units + 1] = lUnit + end + + else + if zone_type == 'cylinder' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = lUnit + break + elseif zone_type == 'sphere' and (((unit_pos.x - zones[zones_ind].x)^2 + (unit_pos.y - zones[zones_ind].y)^2 + (unit_pos.z - zones[zones_ind].z)^2)^0.5 <= zones[zones_ind].radius) then + in_zone_units[#in_zone_units + 1] = lUnit + break + end + end end end end @@ -2679,14 +3279,14 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty local zone_units = {} for k = 1, #unit_names do - local unit = Unit.getByName(unit_names[k]) + local unit = Unit.getByName(unit_names[k]) or StaticObject.getByName(unit_names[k]) if unit then units[#units + 1] = unit end end for k = 1, #zone_unit_names do - local unit = Unit.getByName(zone_unit_names[k]) + local unit = Unit.getByName(zone_unit_names[k]) or StaticObject.getByName(zone_unit_names[k]) if unit then zone_units[#zone_units + 1] = unit end @@ -2695,15 +3295,18 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty local in_zone_units = {} for units_ind = 1, #units do + local lUnit = units[units_ind] + local lCat = lUnit:getCategory() + local unit_pos = lUnit:getPosition().p for zone_units_ind = 1, #zone_units do - local unit_pos = units[units_ind]:getPosition().p + local zone_unit_pos = zone_units[zone_units_ind]:getPosition().p - if unit_pos and zone_unit_pos and units[units_ind]:isActive() == true then + if unit_pos and zone_unit_pos and ((lCat == 1 and lUnit:isActive()) or lCat ~= 1) then if zone_type == 'cylinder' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] + in_zone_units[#in_zone_units + 1] = lUnit break elseif zone_type == 'sphere' and (((unit_pos.x - zone_unit_pos.x)^2 + (unit_pos.y - zone_unit_pos.y)^2 + (unit_pos.z - zone_unit_pos.z)^2)^0.5 <= radius) then - in_zone_units[#in_zone_units + 1] = units[units_ind] + in_zone_units[#in_zone_units + 1] = lUnit break end end @@ -2721,7 +3324,8 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) -- get the positions all in one step, saves execution time. for unitset1_ind = 1, #unitset1 do local unit1 = Unit.getByName(unitset1[unitset1_ind]) - if unit1 and unit1:isActive() == true then + local lCat = unit1:getCategory() + if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) then unit_info1[#unit_info1 + 1] = {} unit_info1[#unit_info1].unit = unit1 unit_info1[#unit_info1].pos = unit1:getPosition().p @@ -2730,7 +3334,8 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius) for unitset2_ind = 1, #unitset2 do local unit2 = Unit.getByName(unitset2[unitset2_ind]) - if unit2 and unit2:isActive() == true then + local lCat = unit2:getCategory() + if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) then unit_info2[#unit_info2 + 1] = {} unit_info2[#unit_info2].unit = unit2 unit_info2[#unit_info2].pos = unit2:getPosition().p @@ -2766,7 +3371,8 @@ end function mist.getAvgPoint(points) local avgX, avgY, avgZ, totNum = 0, 0, 0, 0 for i = 1, #points do - local nPoint = mist.utils.makeVec3(points[i]) + --log:warn(points[i]) + local nPoint = mist.utils.makeVec3(points[i]) if nPoint.z then avgX = avgX + nPoint.x avgY = avgY + nPoint.y @@ -2859,13 +3465,13 @@ function mist.getBRString(vars) local metric = vars.metric local avgPos = mist.getAvgPos(units) if avgPos then - local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} - local dir = mist.utils.getDir(vec, ref) - local dist = mist.utils.get2DDist(avgPos, ref) - if alt then - alt = avgPos.y - end - return mist.tostringBR(dir, dist, alt, metric) + local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z} + local dir = mist.utils.getDir(vec, ref) + local dist = mist.utils.get2DDist(avgPos, ref) + if alt then + alt = avgPos.y + end + return mist.tostringBR(dir, dist, alt, metric) end end @@ -2891,10 +3497,11 @@ function mist.getLeadingPos(vars) unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p end end + if #unitPosTbl > 0 then -- one more more units found. -- first, find the unit most in the heading direction local maxPos = -math.huge - + heading = heading * -1 -- rotated value appears to be opposite of what was expected local maxPosInd -- maxPos - the furthest in direction defined by heading; maxPosInd = for i = 1, #unitPosTbl do local rotatedVec2 = mist.vec.rotateVec2(mist.utils.makeVec2(unitPosTbl[i]), heading) @@ -2985,8 +3592,147 @@ function mist.getLeadingBRString(vars) end end +--[[getPathLength from GSH +-- Returns the length between the defined set of points. Can also return the point index before the cutoff was achieved +p - table of path points, vec2 or vec3 +cutoff - number distance after which to stop at +topo - boolean for if it should get the topographical distance + +]] + +function mist.getPathLength(p, cutoff, topo) + local l = 0 + local cut = 0 or cutOff + local path = {} + + for i = 1, #p do + if topo then + table.insert(path, mist.utils.makeVec3GL(p[i])) + else + table.insert(path, mist.utils.makeVec3(p[i])) + end + end + + for i = 1, #path do + if i + 1 <= #path then + if topo then + l = mist.utils.get3DDist(path[i], path[i+1]) + l + else + l = mist.utils.get2DDist(path[i], path[i+1]) + l + end + end + if cut ~= 0 and l > cut then + return l, i + end + end + return l end +--[[ +Return a series of points to simplify the input table. Best used in conjunction with findPathOnRoads to turn the massive table into a list of X points. +p - table of path points, can be vec2 or vec3 +num - number of segments. +exact - boolean for whether or not it returns the exact distance or uses the first WP to that distance. + + +]] + +function mist.getPathInSegments(p, num, exact) + local tot = mist.getPathLength(p) + local checkDist = tot/num + local typeUsed = 'vec2' + + local points = {[1] = p[1]} + local curDist = 0 + for i = 1, #p do + if i + 1 <= #p then + curDist = mist.utils.get2DDist(p[i], p[i+1]) + curDist + if curDist > checkDist then + curDist = 0 + if exact then + -- get avg point between the two + -- insert into point table + -- need to be accurate... maybe reassign the point for the value it is checking? + -- insert into p table? + else + table.insert(points, p[i]) + end + end + + end + + end + return points + +end + + +function mist.getPointAtDistanceOnPath(p, dist, r, rtn) + log:info('find distance: $1', dist) + local rType = r or 'roads' + local point = {x= 0, y = 0, z = 0} + local path = {} + local ret = rtn or 'vec2' + local l = 0 + if p[1] and #p == 2 then + path = land.findPathOnRoads(rType, p[1].x, p[1].y, p[2].x, p[2].y) + else + path = p + end + for i = 1, #path do + if i + 1 <= #path then + nextPoint = path[i+1] + if topo then + l = mist.utils.get3DDist(path[i], path[i+1]) + l + else + l = mist.utils.get2DDist(path[i], path[i+1]) + l + end + end + if l > dist then + local diff = dist + if i ~= 1 then -- get difference + diff = l - dist + end + local dir = mist.utils.getHeadingPoints(mist.utils.makeVec3(path[i]), mist.utils.makeVec3(path[i+1])) + local x, y + if r then + x, y = land.getClosestPointOnRoads(rType, mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1)) + else + x, y = mist.utils.round((math.cos(dir) * diff) + path[i].x,1), mist.utils.round((math.sin(dir) * diff) + path[i].y,1) + end + + if ret == 'vec2' then + return {x = x, y = y}, dir + elseif ret == 'vec3' then + return {x = x, y = 0, z = y}, dir + end + + return {x = x, y = y}, dir + end + end + log:warn('Find point at distance: $1, path distance $2', dist, l) + return false +end + + +function mist.projectPoint(point, dist, theta) + local newPoint = {} + if point.z then + newPoint.z = mist.utils.round(math.sin(theta) * dist + point.z, 3) + newPoint.y = mist.utils.deepCopy(point.y) + else + newPoint.y = mist.utils.round(math.sin(theta) * dist + point.y, 3) + end + newPoint.x = mist.utils.round(math.cos(theta) * dist + point.x, 3) + + return newPoint +end + +end + + + + --- Group functions. -- @section groups do -- group functions scope @@ -3037,6 +3783,9 @@ do -- group functions scope newData.units = {} local newUnits = newGroup:getUnits() + if #newUnits == 0 then + log:warn('getCurrentGroupData has returned no units for: $1', gpName) + end for unitNum, unitData in pairs(newGroup:getUnits()) do newData.units[unitNum] = {} local uName = unitData:getName() @@ -3074,7 +3823,7 @@ do -- group functions scope end - function mist.getGroupData(gpName) + function mist.getGroupData(gpName, route) local found = false local newData = {} if mist.DBs.groupsByName[gpName] then @@ -3115,14 +3864,17 @@ do -- group functions scope newData.units[unitNum].unitName = unitData.unitName newData.units[unitNum].heading = unitData.heading -- added to DBs newData.units[unitNum].playerCanDrive = unitData.playerCanDrive -- added to DBs - + newData.units[unitNum].livery_id = unitData.livery_id + newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft + newData.units[unitNum].AddPropVehicle = unitData.AddPropVehicle + if newData.category == 'plane' or newData.category == 'helicopter' then newData.units[unitNum].payload = payloads[unitNum] - newData.units[unitNum].livery_id = unitData.livery_id + newData.units[unitNum].onboard_num = unitData.onboard_num newData.units[unitNum].callsign = unitData.callsign - newData.units[unitNum].AddPropAircraft = unitData.AddPropAircraft + end if newData.category == 'static' then newData.units[unitNum].categoryStatic = unitData.categoryStatic @@ -3132,6 +3884,10 @@ do -- group functions scope end end --log:info(newData) + if route then + newData.route = mist.getGroupRoute(gpName, true) + end + return newData else log:error('$1 not found in MIST database', gpName) @@ -3156,10 +3912,10 @@ do -- group functions scope if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then for unitIndex, unitData in pairs(group_data.units) do --group index if unitData.unitId == unitId then @@ -3198,10 +3954,10 @@ do -- group functions scope if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then local payloads = {} for unitIndex, unitData in pairs(group_data.units) do --group index @@ -3223,16 +3979,53 @@ do -- group functions scope end log:warn("Couldn't find payload for group: $1", groupIdent) return - end + function mist.getGroupTable(groupIdent) + local gpId = groupIdent + if type(groupIdent) == 'string' and not tonumber(groupIdent) then + if mist.DBs.MEgroupsByName[groupIdent] then + gpId = mist.DBs.MEgroupsByName[groupIdent].groupId + else + log:error('$1 not found in mist.DBs.MEgroupsByName', groupIdent) + end + end + + if gpId then + for coa_name, coa_data in pairs(env.mission.coalition) do + if type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do + if group_data and group_data.groupId == gpId then + return group_data + end + end + end + end + end + end + end + end + end + else + log:error('Need string or number. Got: $1', type(groupIdent)) + return false + end + log:warn("Couldn't find table for group: $1", groupIdent) + + end + function mist.getValidRandomPoint(vars) end function mist.teleportToPoint(vars) -- main teleport function that all of teleport/respawn functions call - --log:info(vars) + --log:warn(vars) local point = vars.point local gpName if vars.gpName then @@ -3243,6 +4036,14 @@ do -- group functions scope log:error('Missing field groupName or gpName in variable table') end + --[[New vars to add, mostly for when called via inZone functions + anyTerrain + offsetWP1 + offsetRoute + initTasks + + ]] + local action = vars.action local disperse = vars.disperse or false @@ -3250,8 +4051,9 @@ do -- group functions scope local radius = vars.radius or 0 local innerRadius = vars.innerRadius - local route = vars.route local dbData = false + + local newGroupData if gpName and not vars.groupData then @@ -3272,18 +4074,33 @@ do -- group functions scope action = 'tele' newGroupData = vars.groupData end + + if vars.newGroupName then + newGroupData.groupName = vars.newGroupName + end + if #newGroupData.units == 0 then + log:warn('$1 has no units in group table', gpName) + return + end + --log:info('get Randomized Point') local diff = {x = 0, y = 0} local newCoord, origCoord local validTerrain = {'LAND', 'ROAD', 'SHALLOW_WATER', 'WATER', 'RUNWAY'} - if string.lower(newGroupData.category) == 'ship' then - validTerrain = {'SHALLOW_WATER' , 'WATER'} - elseif string.lower(newGroupData.category) == 'vehicle' then - validTerrain = {'LAND', 'ROAD'} + if vars.anyTerrain then + -- do nothing + elseif vars.validTerrain then + validTerrain = vars.validTerrain + else + if string.lower(newGroupData.category) == 'ship' then + validTerrain = {'SHALLOW_WATER' , 'WATER'} + elseif string.lower(newGroupData.category) == 'vehicle' then + validTerrain = {'LAND', 'ROAD'} + end end - local offsets = {} + if point and radius >= 0 then local valid = false -- new thoughts @@ -3303,7 +4120,7 @@ do -- group functions scope ---- old for i = 1, 100 do newCoord = mist.getRandPointInCircle(point, radius, innerRadius) - if mist.isTerrainValid(newCoord, validTerrain) then + if vars.anyTerrain or mist.isTerrainValid(newCoord, validTerrain) then origCoord = mist.utils.deepCopy(newCoord) diff = {x = (newCoord.x - newGroupData.units[1].x), y = (newCoord.y - newGroupData.units[1].y)} valid = true @@ -3381,20 +4198,46 @@ do -- group functions scope end - if route then - newGroupData.route = route - end - --log:info(newGroupData) + + local tempRoute + + if mist.DBs.MEgroupsByName[gpName] and not vars.route then + -- log:warn('getRoute') + tempRoute = mist.getGroupRoute(gpName, true) + elseif vars.route then + -- log:warn('routeExist') + tempRoute = mist.utils.deepCopy(vars.route) + end + -- log:warn(tempRoute) + if tempRoute then + if (vars.offsetRoute or vars.offsetWP1 or vars.initTasks) then + for i = 1, #tempRoute do + -- log:warn(i) + if (vars.offsetRoute) or (i == 1 and vars.offsetWP1) or (i == 1 and vars.initTasks) then + -- log:warn('update offset') + tempRoute[i].x = tempRoute[i].x + diff.x + tempRoute[i].y = tempRoute[i].y + diff.y + elseif vars.initTasks and i > 1 then + --log:warn('deleteWP') + tempRoute[i] = nil + end + end + end + newGroupData.route = tempRoute + end + + + --log:warn(newGroupData) --mist.debug.writeData(mist.utils.serialize,{'teleportToPoint', newGroupData}, 'newGroupData.lua') if string.lower(newGroupData.category) == 'static' then - --log:info(newGroupData) + --log:warn(newGroupData) return mist.dynAddStatic(newGroupData) end return mist.dynAdd(newGroupData) end - function mist.respawnInZone(gpName, zone, disperse, maxDisp) + function mist.respawnInZone(gpName, zone, disperse, maxDisp, v) if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() @@ -3405,9 +4248,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName @@ -3416,10 +4259,17 @@ do -- group functions scope vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp + + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end + return mist.teleportToPoint(vars) end - function mist.cloneInZone(gpName, zone, disperse, maxDisp) + function mist.cloneInZone(gpName, zone, disperse, maxDisp, v) --log:info('cloneInZone') if type(gpName) == 'table' then gpName = gpName:getName() @@ -3428,9 +4278,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} vars.gpName = gpName @@ -3440,10 +4290,15 @@ do -- group functions scope vars.disperse = disperse vars.maxDisp = maxDisp --log:info('do teleport') + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end return mist.teleportToPoint(vars) end - function mist.teleportInZone(gpName, zone, disperse, maxDisp) -- groupName, zoneName or table of Zone Names, keepForm is a boolean + function mist.teleportInZone(gpName, zone, disperse, maxDisp, v) -- groupName, zoneName or table of Zone Names, keepForm is a boolean if type(gpName) == 'table' and gpName:getName() then gpName = gpName:getName() else @@ -3451,9 +4306,9 @@ do -- group functions scope end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) - elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone] + elseif type(zone) == 'table' and not zone.radius then + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end local vars = {} @@ -3463,6 +4318,11 @@ do -- group functions scope vars.radius = zone.radius vars.disperse = disperse vars.maxDisp = maxDisp + if v and type(v) == 'table' then + for index, val in pairs(v) do + vars[index] = val + end + end return mist.teleportToPoint(vars) end @@ -3688,14 +4548,20 @@ do -- group functions scope end return choices[rtnVal] end + + function mist.stringCondense(s) + local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} + for i , str in pairs(exclude) do + s = string.gsub(s, str, '') + end + return s + end function mist.stringMatch(s1, s2, bool) - local exclude = {'%-', '%(', '%)', '%_', '%[', '%]', '%.', '%#', '% ', '%{', '%}', '%$', '%%', '%?', '%+', '%^'} + if type(s1) == 'string' and type(s2) == 'string' then - for i , str in pairs(exclude) do - s1 = string.gsub(s1, str, '') - s2 = string.gsub(s2, str, '') - end + s1 = mist.stringCondense(s1) + s2 = mist.stringCondense(s2) if not bool then s1 = string.lower(s1) s2 = string.lower(s2) @@ -3823,6 +4689,25 @@ do -- mist.util scope function mist.utils.celsiusToFahrenheit(c) return c*(9/5)+32 end + + function mist.utils.hexToRGB(hex, l) -- because for some reason the draw tools use hex when everything is rgba 0 - 1 + local int = 255 + if l then + int = 1 + end + if hex and type(hex) == 'string' then + local val = {} + hex = string.gsub(hex, '0x', '') + if string.len(hex) == 8 then + val[1] = tonumber("0x"..hex:sub(1,2)) / int + val[2] = tonumber("0x"..hex:sub(3,4)) / int + val[3] = tonumber("0x"..hex:sub(5,6)) / int + val[4] = tonumber("0x"..hex:sub(7,8)) / int + + return val + end + end + end function mist.utils.converter(t1, t2, val) if type(t1) == 'string' then @@ -4024,7 +4909,7 @@ do -- mist.util scope --- Returns the center of a zone as Vec3. -- @tparam string|table zone trigger zone name or table -- @treturn Vec3 center of the zone - function mist.utils.zoneToVec3(zone) + function mist.utils.zoneToVec3(zone, gl) local new = {} if type(zone) == 'table' then if zone.point then @@ -4032,7 +4917,7 @@ do -- mist.util scope new.y = zone.point.y new.z = zone.point.z elseif zone.x and zone.y and zone.z then - return zone + new = mist.utils.deepCopy(zone) end return new elseif type(zone) == 'string' then @@ -4041,11 +4926,21 @@ do -- mist.util scope new.x = zone.point.x new.y = zone.point.y new.z = zone.point.z - return new end end + if new.x and gl then + new.y = land.getHeight({x = new.x, y = new.z}) + end + return new end + function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out. + if north then + return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1))) + else + return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1))) + end + end --- Returns heading-error corrected direction. -- True-north corrected direction from point along vector vec. -- @tparam Vec3 vec @@ -4067,6 +4962,12 @@ do -- mist.util scope -- @tparam Vec2|Vec3 point2 second point -- @treturn number distance between given points. function mist.utils.get2DDist(point1, point2) + if not point1 then + log:warn("mist.utils.get2DDist 1st input value is nil") + end + if not point2 then + log:warn("mist.utils.get2DDist 2nd input value is nil") + end point1 = mist.utils.makeVec3(point1) point2 = mist.utils.makeVec3(point2) return mist.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z}) @@ -4077,6 +4978,12 @@ do -- mist.util scope -- @tparam Vec3 point2 second point -- @treturn number distancen between given points in 3D space. function mist.utils.get3DDist(point1, point2) + if not point1 then + log:warn("mist.utils.get2DDist 1st input value is nil") + end + if not point2 then + log:warn("mist.utils.get2DDist 2nd input value is nil") + end return mist.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z}) end @@ -4507,6 +5414,23 @@ end do -- mist.debug scope mist.debug = {} + function mist.debug.changeSetting(s) + if type(s) == 'table' then + for sName, sVal in pairs(s) do + if type(sVal) == 'string' or type(sVal) == 'number' then + if sName == 'log' then + mistSettings[sName] = sVal + mist.log:setLevel(sVal) + elseif sName == 'dbLog' then + mistSettings[sName] = sVal + dblog:setLevel(sVal) + end + else + mistSettings[sName] = sVal + end + end + end + end --- Dumps the global table _G. -- This dumps the global table _G to a file in -- the DCS\Logs directory. @@ -4514,11 +5438,24 @@ do -- mist.debug scope -- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io -- libraries. -- @param fname - function mist.debug.dump_G(fname) + function mist.debug.dump_G(fname, simp) if lfs and io then local fdir = lfs.writedir() .. [[Logs\]] .. fname local f = io.open(fdir, 'w') - f:write(mist.utils.tableShow(_G)) + if simp then + local g = mist.utils.deepCopy(_G) + g.mist = nil + g.slmod = nil + g.env.mission = nil + g.env.warehouses = nil + g.country.by_idx = nil + g.country.by_country = nil + + f:write(mist.utils.tableShow(g)) + else + + f:write(mist.utils.tableShow(_G)) + end f:close() log:info('Wrote debug data to $1', fdir) --trigger.action.outText(errmsg, 10) @@ -4562,6 +5499,141 @@ do -- mist.debug scope end end end + + -- write group table + function mist.debug.writeGroup(gName, data) + if gName and mist.DBs.groupsByName[gName] then + local dat + if data then + dat = mist.getGroupData(gName) + else + dat = mist.getGroupTable(gName) + end + if dat then + dat.route = {points = mist.getGroupRoute(gName, true)} + end + + if io and lfs and dat then + mist.debug.writeData(mist.utils.serialize, {gName, dat}, gName .. '_table.lua') + else + if dat then + trigger.action.outText('Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \nGroup table written to DCS.log file instead.', 10) + log:warn('$1 dataTable: $2', gName, dat) + else + trigger.action.outText('Unable to write group table for: ' .. gName .. '\n Error: insufficient libraries to run mist.debug.writeGroup, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua', 10) + end + end + end + end + + + + -- write all object types in mission. + function mist.debug.writeTypes(fName) + local wt = 'mistDebugWriteTypes.lua' + if fName and type(fName) == 'string' and string.find(fName, '.lua') then + wt = fName + end + local output = {units = {}, countries = {}} + for coa_name_miz, coa_data in pairs(env.mission.coalition) do + if type(coa_data) == 'table' then + if coa_data.country then --there is a country table + for cntry_id, cntry_data in pairs(coa_data.country) do + local countryName = string.lower(cntry_data.name) + if cntry_data.id and country.names[cntry_data.id] then + countryName = string.lower(country.names[cntry_data.id]) + end + output.countries[countryName] = {} + if type(cntry_data) == 'table' then --just making sure + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then --should be an unncessary check + local category = obj_cat_name + if not output.countries[countryName][category] then + -- log:warn('Create: $1', category) + output.countries[countryName][category] = {} + end + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do + if group_data and group_data.units and type(group_data.units) == 'table' then --making sure again- this is a valid group + for i = 1, #group_data.units do + if group_data.units[i] then + local u = group_data.units[i] + local liv = u.livery_id or 'default' + if not output.units[u.type] then -- create unit table + -- log:warn('Create: $1', u.type) + output.units[u.type] = {count = 0, livery_id = {}} + end + + if not output.countries[countryName][category][u.type] then + -- log:warn('Create country, category, unit: $1', u.type) + output.countries[countryName][category][u.type] = 0 + end + -- add to count + output.countries[countryName][category][u.type] = output.countries[countryName][category][u.type] + 1 + output.units[u.type].count = output.units[u.type].count + 1 + + if liv and not output.units[u.type].livery_id[countryName] then + -- log:warn('Create livery country: $1', countryName) + output.units[u.type].livery_id[countryName] = {} + end + if liv and not output.units[u.type].livery_id[countryName][liv] then + --log:warn('Create Livery: $1', liv) + output.units[u.type].livery_id[countryName][liv] = 0 + end + if liv then + output.units[u.type].livery_id[countryName][liv] = output.units[u.type].livery_id[countryName][liv] + 1 + end + if u.payload and u.payload.pylons then + if not output.units[u.type].CLSID then + output.units[u.type].CLSID = {} + output.units[u.type].pylons = {} + end + + for pyIndex, pData in pairs(u.payload.pylons) do + if not output.units[u.type].CLSID[pData.CLSID] then + output.units[u.type].CLSID[pData.CLSID] = 0 + end + output.units[u.type].CLSID[pData.CLSID] = output.units[u.type].CLSID[pData.CLSID] + 1 + + if not output.units[u.type].pylons[pyIndex] then + output.units[u.type].pylons[pyIndex] = {} + end + if not output.units[u.type].pylons[pyIndex][pData.CLSID] then + output.units[u.type].pylons[pyIndex][pData.CLSID] = 0 + end + output.units[u.type].pylons[pyIndex][pData.CLSID] = output.units[u.type].pylons[pyIndex][pData.CLSID] + 1 + end + + end + end + end + end + end + end + end + end + end + end + end + end + end + if io and lfs then + mist.debug.writeData(mist.utils.serialize, {'mistDebugWriteTypes', output}, wt) + else + trigger.action.outText('Error: insufficient libraries to run mist.debug.writeTypes, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua \n writeTypes table written to DCS.log file instead.', 10) + log:warn('mist.debug.writeTypes: $1', output) + end + return output + end + function mist.debug.writeWeapons(unit) + + end + + function mist.debug.mark(msg, coord) + + mist.marker.add({point = coord, text = msg}) + log:warn('debug.mark: $1 $2', msg, coord) + end end --- 3D Vector functions @@ -4633,6 +5705,13 @@ do -- mist.vec scope function mist.vec.rotateVec2(vec2, theta) return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)} end + + function mist.vec.normalize(vec3) + local mag = mist.vec.mag(vec3) + if mag ~= 0 then + return mist.vec.scalar_mult(vec3, 1.0 / mag) + end + end end --- Flag functions. @@ -4826,7 +5905,7 @@ unitTableDef = table or nil if stopflag == -1 or (type(trigger.misc.getUserFlag(stopflag)) == 'number' and trigger.misc.getUserFlag(stopflag) == 0) or (type(trigger.misc.getUserFlag(stopflag)) == 'boolean' and trigger.misc.getUserFlag(stopflag) == 0) then local num_in_zone = 0 for i = 1, #units do - local unit = Unit.getByName(units[i]) + local unit = Unit.getByName(units[i]) or StaticObject.getByName(units[i]) if unit then local pos = unit:getPosition().p if mist.pointInPolygon(pos, zone, maxalt) then @@ -4913,7 +5992,13 @@ unitTableDef = table or nil end end - + --[[ + function mist.flagFunc.weapon_in_zones(vars) + -- borrow from suchoi surprise. While running enabled event handler that checks for weapons in zone. + -- Choice is weapon category or weapon strings. + + end +]] --- Sets a flag if unit(s) is/are inside a moving zone. -- @todo document function mist.flagFunc.units_in_moving_zones(vars) @@ -5282,13 +6367,15 @@ do -- mist.msg scope local caSlots = false local caMSGtoGroup = false + local anyUpdate = false + local lastMessageTime = nil if env.mission.groundControl then -- just to be sure? for index, value in pairs(env.mission.groundControl) do if type(value) == 'table' then for roleName, roleVal in pairs(value) do for rIndex, rVal in pairs(roleVal) do - if env.mission.groundControl[index][roleName][rIndex] > 0 then + if type(rVal) == 'number' and rVal > 0 then caSlots = true break end @@ -5303,11 +6390,124 @@ do -- mist.msg scope end local function mistdisplayV5() - --[[thoughts to improve upon - event handler based activeClients table. - display messages only when there is an update - possibly co-routine it. - ]] + --log:warn("mistdisplayV5: $1", timer.getTime()) + + local clearView = true + if #messageList > 0 then + --log:warn('Updates: $1', anyUpdate) + if anyUpdate == true then + local activeClients = {} + + for clientId, clientData in pairs(mist.DBs.humansById) do + if Unit.getByName(clientData.unitName) and Unit.getByName(clientData.unitName):isExist() == true then + activeClients[clientData.groupId] = clientData.groupName + end + end + anyUpdate = false + if displayActive == false then + displayActive = true + end + --mist.debug.writeData(mist.utils.serialize,{'msg', messageList}, 'messageList.lua') + local msgTableText = {} + local msgTableSound = {} + local curTime = timer.getTime() + for mInd, messageData in pairs(messageList) do + --log:warn(messageData) + if messageData.displayTill < curTime then + messageData:remove() -- now using the remove/destroy function. + else + if messageData.displayedFor then + messageData.displayedFor = curTime - messageData.addedAt + end + local nextSound = 1000 + local soundIndex = 0 + + if messageData.multSound and #messageData.multSound > 0 then + for index, sData in pairs(messageData.multSound) do + if sData.time <= messageData.displayedFor and sData.played == false and sData.time < nextSound then -- find index of the next sound to be played + nextSound = sData.time + soundIndex = index + end + end + if soundIndex ~= 0 then + messageData.multSound[soundIndex].played = true + end + end + + for recIndex, recData in pairs(messageData.msgFor) do -- iterate recipiants + if recData == 'RED' or recData == 'BLUE' or activeClients[recData] then -- rec exists + if messageData.text then -- text + if not msgTableText[recData] then -- create table entry for text + msgTableText[recData] = {} + msgTableText[recData].text = {} + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[1] = '-------Combined Arms Message-------- \n' + end + msgTableText[recData].text[#msgTableText[recData].text + 1] = messageData.text + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else -- add to table entry and adjust display time if needed + if recData == 'RED' or recData == 'BLUE' then + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- Combined Arms Message: \n' + else + msgTableText[recData].text[#msgTableText[recData].text + 1] = '\n ---------------- \n' + end + table.insert(msgTableText[recData].text, messageData.text) + if msgTableText[recData].displayTime < messageData.displayTime - messageData.displayedFor then + msgTableText[recData].displayTime = messageData.displayTime - messageData.displayedFor + else + --msgTableText[recData].displayTime = 10 + end + end + end + if soundIndex ~= 0 then + msgTableSound[recData] = messageData.multSound[soundIndex].file + end + end + + end + messageData.update = nil + + end + + end + ------- new display + + if caSlots == true and caMSGtoGroup == false then + if msgTableText.RED then + trigger.action.outTextForCoalition(coalition.side.RED, table.concat(msgTableText.RED.text), msgTableText.RED.displayTime, clearView) + + end + if msgTableText.BLUE then + trigger.action.outTextForCoalition(coalition.side.BLUE, table.concat(msgTableText.BLUE.text), msgTableText.BLUE.displayTime, clearView) + end + end + + for index, msgData in pairs(msgTableText) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outTextForGroup(index, table.concat(msgData.text), msgData.displayTime, clearView) + end + end + --- new audio + if msgTableSound.RED then + trigger.action.outSoundForCoalition(coalition.side.RED, msgTableSound.RED) + end + if msgTableSound.BLUE then + trigger.action.outSoundForCoalition(coalition.side.BLUE, msgTableSound.BLUE) + end + + + for index, file in pairs(msgTableSound) do + if type(index) == 'number' then -- its a groupNumber + trigger.action.outSoundForGroup(index, file) + end + end + + end + + else + mist.removeFunction(displayFuncId) + displayActive = false + end end local function mistdisplayV4() @@ -5468,14 +6668,15 @@ end]] ]] - + local new = {} new.text = vars.text -- The actual message new.displayTime = vars.displayTime -- How long will the message appear for new.displayedFor = 0 -- how long the message has been displayed so far + new.displayTill = timer.getTime() + vars.displayTime new.name = vars.name -- ID to overwrite the older message (if it exists) Basically it replaces a message that is displayed with new text. new.addedAt = timer.getTime() - new.update = true + --log:warn('New Message: $1', new.text) if vars.multSound and vars.multSound[1] then new.multSound = vars.multSound @@ -5560,19 +6761,21 @@ end]] if messageList[i].name then if messageList[i].name == vars.name then --log:info('updateMessage') + messageList[i].displayTill = timer.getTime() + messageList[i].displayTime messageList[i].displayedFor = 0 messageList[i].addedAt = timer.getTime() messageList[i].sound = new.sound messageList[i].text = new.text messageList[i].msgFor = new.msgFor messageList[i].multSound = new.multSound - messageList[i].update = true + anyUpdate = true + --log:warn('Message updated: $1', new.messageID) return messageList[i].messageID end end end end - + anyUpdate = true messageID = messageID + 1 new.messageID = messageID @@ -5586,7 +6789,7 @@ end]] if displayActive == false then displayActive = true - displayFuncId = mist.scheduleFunction(mistdisplayV4, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) + displayFuncId = mist.scheduleFunction(mistdisplayV5, {}, timer.getTime() + messageDisplayRate, messageDisplayRate) end return messageID @@ -5597,6 +6800,7 @@ end]] for i, msgData in pairs(messageList) do if messageList[i] == self then table.remove(messageList, i) + anyUpdate = true return true --removal successful end end @@ -5607,6 +6811,7 @@ end]] for i, msgData in pairs(messageList) do if messageList[i].messageID == id then table.remove(messageList, i) + anyUpdate = true return true --removal successful end end @@ -6007,6 +7212,9 @@ do -- mist.demos scope end end + + + do --[[ stuff for marker panels marker.add() add marker. Point of these functions is to simplify process and to store all mark panels added. @@ -6026,98 +7234,382 @@ do If mark added to a group before a client joins slot is synced. Mark made for cliet A in Slot A. Client A leaves, Client B joins in slot A. What do they see? + May need to automate process... + + + Could release this. But things I might need to add/change before doing so. + - removing marks and re-adding in same sequence doesn't appear to work. May need to schedule adding mark if updating an entry. + - I really dont like the old message style code for which groups get the message. Perhaps change to unitsTable and create function for getting humanUnitsTable. + = Event Handler, and check it, for marks added via script or user to deconflict Ids. + - Full validation of passed values for a specific shape type. ]] - --[[ - local typeBase = { - ['Mi-8MT'] = {'Mi-8MTV2', 'Mi-8MTV', 'Mi-8'}, - ['MiG-21Bis'] = {'Mig-21'}, - ['MiG-15bis'] = {'Mig-15'}, - ['FW-190D9'] = {'FW-190'}, - ['Bf-109K-4'] = {'Bf-109'}, - } - - - local mId = 1337 + + local usedMarks = {} + + local mDefs = { + coa = { + ['red'] = {fillColor = {.8, 0 , 0, .5}, color = {.8, 0 , 0, .5}, lineType = 2, fontSize = 16}, + ['blue'] = {fillColor = {0, 0 , 0.8, .5}, color = {0, 0 , 0.8, .5}, lineType = 2, fontSize = 16}, + ['all'] = {fillColor = {.1, .1 , .1, .5}, color = {.9, .9 , .9, .5}, lineType = 2, fontSize = 16}, + ['neutral'] = {fillColor = {.1, .1 , .1, .5}, color = {.2, .2 , .2, .5}, lineType = 2, fontSize = 16}, + }, + } + + local userDefs = {['red'] = {},['blue'] = {},['all'] = {},['neutral'] = {}} + local mId = 1000 + + local tNames = {'line', 'circle','rect', 'arrow', 'text', 'quad', 'freeform'} + local tLines = {[0] = 'no line', [1] = 'solid', [2] = 'dashed',[3] = 'dotted', [4] = 'dot dash' ,[5] = 'long dash', [6] = 'two dash'} + local coas = {[-1] = 'all', [0] = 'neutral', [1] = 'red', [2] = 'blue'} + + local altNames = {['poly'] = 7, ['lines'] = 1, ['polygon'] = 7 } + + local function draw(s) + --log:warn(s) + if type(s) == 'table' then + local mType = s.markType + if mType == 'panel' then + if markScope == 'coa' then + trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly) + elseif markScope == 'group' then + trigger.action.markToGroup(s.markId, s.text, s.pos, s.markFor, s.readOnly) + else + trigger.action.markToAll(s.markId, s.text, s.pos, s.readOnly) + end + elseif mType == 'line' then + trigger.action.lineToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'circle' then + trigger.action.circleToAll(s.coa, s.markId, s.pos[1], s.radius, s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'rect' then + trigger.action.rectToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'arrow' then + trigger.action.arrowToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + elseif mType == 'text' then + trigger.action.textToAll(s.coa, s.markId, s.pos[1], s.color, s.fillColor, s.fontSize, s.readOnly, s.text) + elseif mType == 'quad' then + trigger.action.quadToAll(s.coa, s.markId, s.pos[1], s.pos[2], s.pos[3], s.pos[4], s.color, s.fillColor, s.lineType, s.readOnly, s.message) + end + if s.name and not usedMarks[s.name] then + usedMarks[s.name] = s.markId + end + elseif type(s) == 'string' then + --log:warn(s) + mist.utils.dostring(s) + end + end + mist.marker = {} - mist.marker.list = {} + local function markSpamFilter(recList, spamBlockOn) for id, name in pairs(recList) do if name == spamBlockOn then - log:info('already on recList') + --log:info('already on recList') return recList end end - log:info('add to recList') + --log:info('add to recList') table.insert(recList, spamBlockOn) return recList end local function iterate() - mId = mId + 1 - return mId + while mId < 10000000 do + if usedMarks[mId] then + mId = mId + 1 + else + return mist.utils.deepCopy(mId) + end + end + return mist.utils.deepCopy(mId) end + + local function validateColor(val) + if type(val) == 'table' then + for i = 1, #val do + if type(val[i]) == 'number' and val[i] > 1 then + val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent. + end + end + elseif type(val) == 'string' then + val = mist.utils.hexToRGB(val) + + end + return val + end + + local function checkDefs(vName, coa) + --log:warn('CheckDefs: $1 $2', vName, coa) + local coaName + if type(coa) == 'number' then + if coas[coa] then + coaName = coas[coa] + end + elseif type(coa) == 'string' then + coaName = coa + end + + -- log:warn(coaName) + if userDefs[coaName] and userDefs[coaName][vName] then + return userDefs[coaName][vName] + elseif mDefs.coa[coaName] and mDefs.coa[coaName][vName] then + return mDefs.coa[coaName][vName] + end + + end + + function mist.marker.getNextId() + return iterate() + end + + local handle = {} + function handle:onEvent(e) + if world.event.S_EVENT_MARK_ADDED == e.id and e.idx then + usedMarks[e.idx] = e.idx + if not mist.DBs.markList[e.idx] then + --log:info('create maker DB: $1', e.idx) + mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition} + if e.unit then + mist.DBs.markList[e.idx].unit = e.initiaor:getName() + end + --log:info(mist.marker.list[e.idx]) + end + + elseif world.event.S_EVENT_MARK_CHANGE == e.id and e.idx then + if mist.DBs.markList[e.idx] then + mist.DBs.markList[e.idx].text = e.text + end + elseif world.event.S_EVENT_MARK_REMOVE == e.id and e.idx then + if mist.DBs.markList[e.idx] then + mist.DBs.markList[e.idx] = nil + end + end + + end + + local function getMarkId(id) + if mist.DBs.markList[id] then + return id + else + for mEntry, mData in pairs(mist.DBs.markList) do + if id == mData.name or id == mData.id then + return mData.id + end + end + end + + + end + + + local function removeMark(id) + --log:info("Removing Mark: $1", id + local removed = false + if type(id) == 'table' then + for ind, val in pairs(id) do + local r = getMarkId(val) + if r then + trigger.action.removeMark(r) + mist.DBs.markList[r] = nil + removed = true + end + end + + else + local r = getMarkId(id) + trigger.action.removeMark(r) + mist.DBs.markList[r] = nil + removed = true + end + return removed + end + + world.addEventHandler(handle) + function mist.marker.setDefault(vars) + local anyChange = false + if vars and type(vars) == 'table' then + for l1, l1Data in pairs(vars) do + if type(l1Data) == 'table' then + if not userDefs[l1] then + userDefs[l1] = {} + end + + for l2, l2Data in pairs(l1Data) do + userDefs[l1][l2] = l2Data + anyChange = true + end + else + userDefs[l1] = l1Data + anyChange = true + end + end + + end + return anyChange + end - function mist.marker.add(pos, text, markFor, id) - log:warn('markerFunc') - log:info('Pos: $1, Text: $2, markFor: $3, id: $4', pos, text, markFor, id) - if not id then - - else + function mist.marker.add(vars) + --log:warn('markerFunc') + --log:warn(vars) + local pos = vars.point or vars.points or vars.pos + local text = vars.text or '' + local markFor = vars.markFor + local markForCoa = vars.markForCoa or vars.coa -- optional, can be used if you just want to mark to a specific coa/all + local id = vars.id or vars.markId or vars.markid + local mType = vars.mType or vars.markType or vars.type or 0 + local color = vars.color + local fillColor = vars.fillColor + local lineType = vars.lineType or 2 + local readOnly = vars.readOnly or true + local message = vars.message + local fontSize = vars.fontSize + local name = vars.name + local radius = vars.radius or 500 + + local coa = -1 + local usedId = 0 + + + if id then + if type(id) ~= 'number' then + name = id + usedId = iterate() + end + --log:info('checkIfIdExist: $1', id) + --[[ + Maybe it should treat id or name as the same thing/single value. + + If passed number it will use that as the first Id used and will delete/update any marks associated with that same value. + + + ]] + + local lId = id or name + if mist.DBs.markList[id] then ---------- NEED A BETTER WAY TO ASSOCIATE THE ID VALUE. CUrrnetly deleting from table and checking if that deleted entry exists which it wont. + --log:warn('active mark to be removed: $1', id) + name = mist.DBs.markList[id].name or id + removeMark(id) + elseif usedMarks[id] then + --log:info('exists in usedMarks: $1', id) + removeMark(usedMarks[id]) + elseif name and usedMarks[name] then + --log:info('exists in usedMarks: $1', name) + removeMark(usedMarks[name]) + end + usedId = iterate() + usedMarks[id] = usedId -- redefine the value used end - local markType = 'all' + if name then + usedMarks[name] = usedId + end + + if usedId == 0 then + usedId = iterate() + end + if mType then + if type(mType) == 'string' then + for i = 1, #tNames do + --log:warn(tNames[i]) + if mist.stringMatch(mType, tNames[i]) then + mType = i + break + end + end + elseif type(mType) == 'number' and mType > #tNames then + mType = 0 + end + end + --log:warn(mType) + local markScope = 'all' local markForTable = {} - if pos then - pos = mist.utils.makeVec3(pos) + + if pos then + if pos[1] then + for i = 1, #pos do + pos[i] = mist.utils.makeVec3(pos[i]) + end + + else + pos[1] = mist.utils.makeVec3(pos) + end + end if text and type(text) ~= string then text = tostring(text) - else - text = '' end - - if markFor then + + if markForCoa then + if type(markForCoa) == 'string' then + if tonumber(markForCoa) then + coa = coas[tonumber(markForCoa)] + markScope = 'coa' + else + for ind, cName in pairs(coas) do + if mist.stringMatch(cName, markForCoa) then + coa = ind + markScope = 'coa' + break + end + end + end + elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then + coa = markForCoa + markScore = 'coa' + end + + + + elseif markFor then if type(markFor) == 'number' then -- groupId if mist.DBs.groupsById[markFor] then - markType = 'group' + markScope = 'group' end elseif type(markFor) == 'string' then -- groupName if mist.DBs.groupsByName[markFor] then - markType = 'group' + markScope = 'group' markFor = mist.DBs.groupsByName[markFor].groupId end elseif type(markFor) == 'table' then -- multiple groupName, country, coalition, all - markType = 'table' - log:info(markFor) + markScope = 'table' + --log:warn(markFor) for forIndex, forData in pairs(markFor) do -- need to rethink this part and organization. Gotta be a more logical way to send messages to coa, groups, or all. - log:info(forIndex) - log:info(forData) for list, listData in pairs(forData) do - log:info(listData) + --log:warn(listData) forIndex = string.lower(forIndex) if type(listData) == 'string' then listData = string.lower(listData) end if listData == 'all' then - markType = 'all' + markScope = 'all' break elseif (forIndex == 'coa' or forIndex == 'ca') then -- mark for coa or CA. - for name, index in pairs (coalition.side) do + local matches = 0 + for name, index in pairs (coalition.side) do if listData == string.lower(name) then - markType = 'coalition' + markScope = 'coa' + markFor = index + coa = index + matches = matches + 1 end end - elseif (forIndex == 'countries' and string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then - markForTable = markSpamFilter(markForTable, clientData.groupId) + if matches > 1 then + markScope = 'all' + end + elseif forIndex == 'countries' then + for clienId, clientData in pairs(mist.DBs.humansById) do + if (string.lower(clientData.country) == listData) or (forIndex == 'units' and string.lower(clientData.unitName) == listData) then + markForTable = markSpamFilter(markForTable, clientData.groupId) + end + end elseif forIndex == 'unittypes' then -- mark to group -- iterate play units for clientId, clientData in pairs(mist.DBs.humansById) do for typeId, typeData in pairs(listData) do - log:info(typeData) + --log:warn(typeData) local found = false if list == 'all' or clientData.coalition and type(clientData.coalition) == 'string' and mist.stringMatch(clientData.coalition, list) then if mist.matchString(typeData, clientData.type) then @@ -6160,61 +7652,268 @@ do end end else - markType = 'all' + markScope = 'all' end + if mType == 0 then + local data = {markId = usedId, text = text, pos = pos[1], markScope = markScope, markFor = markFor, markType = 'panel', name = name, time = timer.getTime()} + if markScope ~= 'table' then + -- create marks + + mist.DBs.markList[usedId] = data-- add to the DB + + else + if #markForTable > 0 then + --log:info('iterate') + local list = {} + if id and not name then + name = id + end + for i = 1, #markForTable do + local newId = iterate() + local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()} + mist.DBs.markList[newId] = data + table.insert(list, data) - + draw(data) + + end + return list + end + end - - - if markType ~= 'table' then - local newId = iterate() - local data = {markId = newId, text = text, pos = pos, markType = markType, markFor = markFor} + draw(data) + + return data + elseif mType > 0 then + local newId = iterate() + local fCal = {} + fCal[#fCal+1] = mType + fCal[#fCal+1] = coa + fCal[#fCal+1] = usedId + + local likeARainCoat = false + if mType == 7 then + local score = 0 + for i = 1, #pos do + if i < #pos then + local val = ((pos[i+1].x - pos[i].x)*(pos[i+1].z + pos[i].z)) + --log:warn("$1 index score is: $2", i, val) + score = score + val + else + score = score + ((pos[1].x - pos[i].x)*(pos[1].z + pos[i].z)) + end + end + --log:warn(score) + if score > 0 then -- it is anti-clockwise. Due to DCS bug make it clockwise. + likeARainCoat = true + --log:warn('flip') + + for i = #pos, 1, -1 do + fCal[#fCal+1] = pos[i] + end + end + end + if likeARainCoat == false then + for i = 1, #pos do + fCal[#fCal+1] = pos[i] + end + end + if radius and mType == 2 then + fCal[#fCal+1] = radius + end + + if not color then + color = checkDefs('color', coa) + else + color = validateColor(color) + end + fCal[#fCal+1] = color + + + if not fillColor then + fillColor = checkDefs('fillColor', coa) + else + fillColor = validateColor(fillColor) + end + fCal[#fCal+1] = fillColor + + if mType == 5 then -- text to all + if not fontSize then + fontSize = checkDefs('fontSize', coa) or 16 + end + fCal[#fCal+1] = fontSize + else + if not lineType then + lineType = checkDefs('lineType', coa) or 2 + end + end + fCal[#fCal+1] = lineType + if not readOnly then + readOnly = true + end + fCal[#fCal+1] = readOnly + if mType == 5 then + fCal[#fCal+1] = text + else + + fCal[#fCal+1] = message + end + local data = {coa = coa, markId = usedId, pos = pos, markFor = markFor, color = color, readOnly = readOnly, message = message, fillColor = fillColor, lineType = lineType, markType = tNames[mType], name = name, radius = radius, text = text, fontSize = fontSize, time = timer.getTime()} + mist.DBs.markList[usedId] = data + + if mType == 7 or mType == 1 then + local s = "trigger.action.markupToAll(" - -- create marks - if markType == 'coa' then - trigger.action.markToCoalition(newId, text, pos, markFor) - elseif markType == 'group' then - trigger.action.markToGroup(newId, text, pos, markFor) - else - trigger.action.markToAll(iterate(), text, pos) - end - table.insert(mist.marker.list, data) -- add to the DB - else - if #markForTable > 0 then - log:info('iterate') - for i = 1, #markForTable do - local newId = iterate() - local data = {markId = newId, text = text, pos = pos, markFor = markFor} - log:info(data) - table.insert(mist.marker.list, data) - trigger.action.markToGroup(newId, text, pos, markForTable[i]) - end - end - end - + for i = 1, #fCal do + --log:warn(fCal[i]) + if type(fCal[i]) == 'table' or type(fCal[i]) == 'boolean' then + s = s .. mist.utils.oneLineSerialize(fCal[i]) + else + s = s .. fCal[i] + end + if i < #fCal then + s = s .. ',' + end + end + + s = s .. ')' + if name then + usedMarks[name] = usedId + end + draw(s) + + else + + draw(data) + + end + return data + end end function mist.marker.remove(id) - for i, data in pairs(mist.marker.list) do - if id == data.markId then - trigger.action.removeMark(id) - end - end + return removeMark(id) end function mist.marker.get(id) - + if mist.DBs.markList[id] then + return mist.DBs.markList[id] + end + local names = {} + for markId, data in pairs(mist.DBs.markList) do + if data.name and data.name == id then + table.insert(names, data) + end + end + if #names >= 1 then + return names + end end - function mist.marker.coords(pos, cType, markFor, id) -- wrapper function to just display coordinates of a specific format at location - - - end - ]] + function mist.marker.drawZone(name, v) + if mist.DBs.zonesByName[name] then + --log:warn(mist.DBs.zonesByName[name]) + local vars = v or {} + local ref = mist.utils.deepCopy(mist.DBs.zonesByName[name]) + + if ref.type == 2 then -- it is a quad, but use freeform cause it isnt as bugged + vars.mType = 6 + vars.point = ref.verticies + else + vars.mType = 2 + vars.radius = ref.radius + vars.point = ref.point + end + + + if not (vars.ignoreColor and vars.ignoreColor == true) and not vars.fillColor then + vars.fillColor = ref.color + end + + --log:warn(vars) + return mist.marker.add(vars) + end + end + + function mist.marker.drawShape(name, v) + if mist.DBs.drawingByName[name] then + + local d = v or {} + local o = mist.utils.deepCopy(mist.DBs.drawingByName[name]) + --mist.marker.add({point = {x = o.mapX, z = o.mapY}, text = name}) + --log:warn(o) + d.points = o.points or {} + if o.primitiveType == "Polygon" then + d.mType = 7 + + if o.polygonMode == "rect" then + d.mType = 6 + elseif o.polygonMode == "circle" then + d.mType = 2 + d.points = {x = o.mapX, y = o.mapY} + d.radius = o.radius + end + elseif o.primitiveType == "TextBox" then + d.mType = 5 + d.points = {x = o.mapX, y = o.mapY} + d.text = o.text or d.text + d.fontSize = d.fontSize or o.fontSize + end + -- NOTE TO SELF. FIGURE OUT WHICH SHAPES NEED TO BE OFFSET. OVAL YES. + + if o.fillColorString and not d.fillColor then + d.fillColor = mist.utils.hexToRGB(o.fillColorString) + end + if o.colorString then + d.color = mist.utils.hexToRGB(o.colorString) + end + + + if o.thickness == 0 then + d.lineType = 0 + elseif o.style == 'solid' then + d.lineType = 1 + elseif o.style == 'dot' then + d.lineType = 2 + elseif o.style == 'dash' then + d.lineType = 3 + else + d.lineType = 1 + end + + + if o.primitiveType == "Line" and #d.points >= 2 then + d.mType = 1 + local rtn = {} + for i = 1, #d.points -1 do + local var = mist.utils.deepCopy(d) + var.points = {} + var.points[1] = d.points[i] + var.points[2] = d.points[i+1] + table.insert(rtn, mist.marker.add(var)) + end + return rtn + else + if d.mType then + --log:warn(d) + return mist.marker.add(d) + end + end + end + + + end + + + --[[ + function mist.marker.circle(v) + + + end +]] end --- Time conversion functions. -- @section mist.time @@ -6430,6 +8129,7 @@ do -- group tasks scope mist.air = {} mist.air.fixedWing = {} mist.air.heli = {} + mist.ship = {} --- Tasks group to follow a route. -- This sets the mission task for the given group. @@ -6453,7 +8153,7 @@ do -- group tasks scope if group then local groupCon = group:getController() if groupCon then - log:warn(misTask) + --log:warn(misTask) groupCon:setTask(misTask) return true end @@ -6475,17 +8175,17 @@ do -- group tasks scope if type(coa_data) == 'table' then if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do - for obj_type_name, obj_type_data in pairs(cntry_data) do - if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" then -- only these types have points - if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then --there's a group! - for group_num, group_data in pairs(obj_type_data.group) do + for obj_cat_name, obj_cat_data in pairs(cntry_data) do + if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" then -- only these types have points + if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then --there's a group! + for group_num, group_data in pairs(obj_cat_data.group) do if group_data and group_data.groupId == gpId then -- this is the group we are looking for if group_data.route and group_data.route.points and #group_data.route.points > 0 then local points = {} for point_num, point in pairs(group_data.route.points) do local routeData = {} - if env.mission.version > 7 then + if env.mission.version > 7 and env.mission.version < 19 then routeData.name = env.getValueDictByKey(point.name) else routeData.name = point.name @@ -6515,10 +8215,10 @@ do -- group tasks scope log:error('Group route not defined in mission editor for groupId: $1', gpId) return end --if group_data and group_data.name and group_data.name == 'groupname' - end --for group_num, group_data in pairs(obj_type_data.group) do - end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then - end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then - end --for obj_type_name, obj_type_data in pairs(cntry_data) do + end --for group_num, group_data in pairs(obj_cat_data.group) do + end --if ((type(obj_cat_data) == 'table') and obj_cat_data.group and (type(obj_cat_data.group) == 'table') and (#obj_cat_data.group > 0)) then + end --if obj_cat_name == "helicopter" or obj_cat_name == "ship" or obj_cat_name == "plane" or obj_cat_name == "vehicle" or obj_cat_name == "static" then + end --for obj_cat_name, obj_cat_data in pairs(cntry_data) do end --for cntry_id, cntry_data in pairs(coa_data.country) do end --if coa_data.country then --there is a country table end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then @@ -6822,14 +8522,19 @@ do -- group tasks scope end -- need to return a Vec3 or Vec2? - function mist.getRandPointInCircle(p, radius, innerRadius, maxA, minA) + function mist.getRandPointInCircle(p, r, innerRadius, maxA, minA) local point = mist.utils.makeVec3(p) local theta = 2*math.pi*math.random() + local radius = r or 1000 local minR = innerRadius or 0 if maxA and not minA then theta = math.rad(math.random(0, maxA - math.random())) - elseif maxA and minA and minA < maxA then - theta = math.rad(math.random(minA, maxA) - math.random()) + elseif maxA and minA then + if minA < maxA then + theta = math.rad(math.random(minA, maxA) - math.random()) + else + theta = math.rad(math.random(maxA, minA) - math.random()) + end end local rad = math.random() + math.random() if rad > 1 then @@ -6854,14 +8559,21 @@ do -- group tasks scope end function mist.getRandomPointInZone(zoneName, innerRadius, maxA, minA) - if type(zoneName) == 'string' and type(trigger.misc.getZone(zoneName)) == 'table' then - return mist.getRandPointInCircle(trigger.misc.getZone(zoneName).point, trigger.misc.getZone(zoneName).radius, innerRadius, maxA, minA) - end + if type(zoneName) == 'string' then + local zone = mist.DBs.zonesByName[zoneName] + if zone.type and zone.type == 2 then + return mist.getRandomPointInPoly(zone.verticies) + else + return mist.getRandPointInCircle(zone.point, zone.radius, innerRadius, maxA, minA) + end + end return false end function mist.getRandomPointInPoly(zone) - local avg = mist.getAvgPoint(zone) + --env.info('Zone Size: '.. #zone) + local avg = mist.getAvgPoint(zone) + --log:warn(avg) local radius = 0 local minR = math.huge local newCoord = {} @@ -6873,6 +8585,8 @@ do -- group tasks scope minR = mist.utils.get2DDist(avg, zone[i]) end end + --log:warn('Radius: $1', radius) + --log:warn('minR: $1', minR) local lSpawnPos = {} for j = 1, 100 do newCoord = mist.getRandPointInCircle(avg, radius) @@ -6886,6 +8600,19 @@ do -- group tasks scope end return newCoord end + + function mist.getWindBearingAndVel(p) + local point = mist.utils.makeVec3(o) + local gLevel = land.getHeight({x = point.x, y = point.z}) + if point.y <= gLevel then + point.y = gLevel + 10 + end + local t = atmosphere.getWind(point) + local bearing = math.tan(t.z/t.x) + local vel = math.sqrt(t.x^2 + t.z^2) + return bearing, vel + + end function mist.groupToRandomPoint(vars) local group = vars.group --Required @@ -6941,25 +8668,25 @@ do -- group tasks scope return end - function mist.groupRandomDistSelf(gpData, dist, form, heading, speed) + function mist.groupRandomDistSelf(gpData, dist, form, heading, speed, disableRoads) local pos = mist.getLeadPos(gpData) local fakeZone = {} fakeZone.radius = dist or math.random(300, 1000) fakeZone.point = {x = pos.x, y = pos.y, z = pos.z} - mist.groupToRandomZone(gpData, fakeZone, form, heading, speed) + mist.groupToRandomZone(gpData, fakeZone, form, heading, speed, disableRoads) return end - function mist.groupToRandomZone(gpData, zone, form, heading, speed) + function mist.groupToRandomZone(gpData, zone, form, heading, speed, disableRoads) if type(gpData) == 'string' then gpData = Group.getByName(gpData) end if type(zone) == 'string' then - zone = trigger.misc.getZone(zone) + zone = mist.DBs.zonesByName[zone] elseif type(zone) == 'table' and not zone.radius then - zone = trigger.misc.getZone(zone[math.random(1, #zone)]) + zone = mist.DBs.zonesByName[zone[math.random(1, #zone)]] end if speed then @@ -6973,7 +8700,7 @@ do -- group tasks scope vars.headingDegrees = heading vars.speed = speed vars.point = mist.utils.zoneToVec3(zone) - + vars.disableRoads = disableRoads mist.groupToRandomPoint(vars) return @@ -7042,7 +8769,7 @@ do -- group tasks scope function mist.groupToPoint(gpData, point, form, heading, speed, useRoads) if type(point) == 'string' then - point = trigger.misc.getZone(point) + point = mist.DBs.zonesByName[point] end if speed then speed = mist.utils.kmphToMps(speed) @@ -7081,6 +8808,16 @@ do -- group tasks scope return leader:getPosition().p end end + + function mist.groupIsDead(groupName) -- copy more or less from on station + if Group.getByName(groupName) then + local gp = Group.getByName(groupName) + if #gp:getUnits() > 0 or gp:isExist() == true then + return false + end + end + return true + end end @@ -7200,7 +8937,7 @@ do -- mist.Logger scope -- @usage -- log everything --myLogger:setLevel(3) function mist.Logger:setLevel(level) - if not level then + if not level then self.level = 2 else if type(level) == 'string' then diff --git a/scripts/parrotSpeak.lua b/scripts/parrotSpeak.lua new file mode 100644 index 00000000..a24057a4 --- /dev/null +++ b/scripts/parrotSpeak.lua @@ -0,0 +1,184 @@ +--------------------------------THIS FIRST BIT IS THE SRS CODE BLOCK------------------- +-------------------- first 4 things need to be set correctly for the server, they are for ours, we don't do the google creds yet + +STTS = {} +-- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER +STTS.DIRECTORY = "C:\\Users\\Administrator\\Desktop\\DCS\\SRS Refugees" + +STTS.SRS_PORT = 5002 -- LOCAL SRS PORT - DEFAULT IS 5002 +STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json" + +-- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING +STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe" + +local random = math.random +function STTS.uuid() + local template ='yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end + +function STTS.round(x, n) + n = math.pow(10, n or 0) + x = x * n + if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end + return x / n +end + +function STTS.getSpeechTime(length,speed,isGoogle) + -- Function returns estimated speech time in seconds + + -- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word + -- so 5 chars * 100wpm = 500 characters per min = 8.3 chars per second + -- so lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec + -- map function: (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + local wpm = math.ceil(100 * speedFactor) + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) +end + +function STTS.TextToSpeech(message,freqs,modulations, volume,name, coalition,point, speed,gender,culture,voice, googleTTS ) + if os == nil or io == nil then + env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ") + return + end + + speed = speed or 1 + gender = gender or "female" + culture = culture or "" + voice = voice or "" + + + message = message:gsub("\"","\\\"") + + local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs, modulations, coalition,STTS.SRS_PORT, name ) + + if voice ~= "" then + cmd = cmd .. string.format(" -V \"%s\"",voice) + else + + if culture ~= "" then + cmd = cmd .. string.format(" -l %s",culture) + end + + if gender ~= "" then + cmd = cmd .. string.format(" -g %s",gender) + end + end + + if googleTTS == true then + cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS) + end + + if speed ~= 1 then + cmd = cmd .. string.format(" -s %s",speed) + end + + if volume ~= 1.0 then + cmd = cmd .. string.format(" -v %s",volume) + end + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + cmd = cmd ..string.format(" -t \"%s\"",message) + + if string.len(cmd) > 255 then + local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" + local script = io.open(filename,"w+") + script:write(cmd .. " && exit" ) + script:close() + cmd = string.format("\"%s\"",filename) + timer.scheduleFunction(os.remove, filename, timer.getTime() + 1) + end + + if string.len(cmd) > 255 then + env.info("[DCS-STTS] - cmd string too long") + env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n") + end + os.execute(cmd) + + return STTS.getSpeechTime(message,speed,googleTTS) + +end + +function STTS.PlayMP3(pathToMP3,freqs,modulations, volume,name, coalition,point ) + + local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs, modulations, coalition,STTS.SRS_PORT, name, volume ) + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") + os.execute(cmd) + +end + +------------------------THIS BIT IS THE CODE YOU'D RUN IN GAME + +tts = {} +tts.words = "All players, all players, AO update in 5 Magic to all players AO update as follows Weather over North ranges is good, recommending full up war. altimeter 3 0 decimal 1 2 , flare restrictions above 5000,in the MOA's and burnout by 100 ft in western ranges. Chaff below 20,000 for all playersAir picture is multiple groups bandits forming a north south CAP 60 miles north west of bullseye, no SAM,manpad or triple A. All units are approved to start moving into tracks for exercise start, exercise commences in 5 minutes" + +tts.atis = "All players, all players, AO update in 5 Magic to all players AO update as follows Weather over North ranges is good, recommending full up war. altimeter 3 0 decimal 1 2 , flare restrictions above 5000,in the MOA's and burnout by 100 ft in western ranges. Chaff below 20,000 for all playersAir picture is multiple groups bandits forming a north south CAP 60 miles north west of bullseye, no SAM,manpad or triple A. All units are approved to start moving into tracks for exercise start, exercise commences in 5 minutes" + +function tts.notify(message, displayFor) + trigger.action.outText(message, displayFor) +end + +function tts.normal () + STTS.TextToSpeech(tts.words,"251","AM","1.0","SRS",2) +end + +function tts.russian () + STTS.TextToSpeech(tts.words,"251","AM","1.0","SRS",2,null,1,"female","ru-RU","Microsoft Irina Desktop") +end + + +do + longRangeShots = missionCommands.addSubMenu("Crash checks") + missionCommands.addCommand ("Speak", longRangeShots, tts.normal) + missionCommands.addCommand ("Speak russian", longRangeShots, tts.russian) + +end + +tts.notify("crashTest.lua loaded", 2) + diff --git a/scripts/poleGen.lua b/scripts/poleGen.lua new file mode 100644 index 00000000..16b9e336 --- /dev/null +++ b/scripts/poleGen.lua @@ -0,0 +1,628 @@ +--max range, altitude and it fails? +--shoot right up +--fix sa10 and sa 11 + +--different static layouts on a carrier depending on what is going on +--guns on a static ship at sea? + + +pg = {} +handler = {} +pg.name = "Sam" +pg.cloneName = "Clone" +pg.fakeTargetName = "Player" +pg.samCounter = 1 +pg.droneAlt = 20000 +pg.delay = 40 +pg.missileSpeed = 565 +pg.samLoc = {} +pg.samLoc.x = 1 +pg.samLoc.y = 1 +pg.samLoc.z = 1 +pg.missilesActive = 0 +pg.droneName = nil +pg.droneSpeed = 300 +pg.hidden = false +pg.samDB = {[1] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [2] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [3] = {["missileDelay"] = 120, ["missileSpeed"] = 550}, + [4] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [5] = {["missileDelay"] = 65, ["missileSpeed"] = 770}, + [6] = {["missileDelay"] = 60, ["missileSpeed"] = 500}, + [7] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [8] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [9] = {["missileDelay"] = 40, ["missileSpeed"] = 770}, + [10] = {["missileDelay"] = 60, ["missileSpeed"] = 1400}, + } + +function pg.notify(message, displayFor) + trigger.action.outText(message, displayFor, false) +end + + +function pg.fakeSam(vec3) + + playerTarget = Unit.getByName(pg.fakeTargetName) + pointFakeTarget = playerTarget:getPosition().p + vec3 = vec3 or pg.samLoc + + vecSub = mist.vec.sub(pointFakeTarget , vec3) + planeHeading = mist.utils.getDir(vecSub) + missileType = 2 + pg.spawnDrone (vec3,planeHeading,playerTarget,missileType) + + --pg.notify("FakeSam Ran", 10) + + targetID = Group.getByName("TargetDrone" .. pg.samCounter):getUnit(1):getID() + samGroup = Group.getByName("poleGenerator" .. pg.samCounter) + vars = {[1] = targetID, [2] = samGroup} + timer.scheduleFunction(pg.attack ,vars, timer.getTime() + 1) + pg.samCounter = pg.samCounter + 1 + +end + + +function pg.spawnSamSA2 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "S_75M_Volhov", + --["unitId"] = 35, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "SNR_75V", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + +function pg.spawnSamSA3 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "snr s-125 tr", + --["unitId"] = 35, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "5p73 s-125 ln", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + +function pg.spawnSamSA5 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "RPC_5N62V", + --["unitId"] = 35, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "RLS_19J6", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + [3] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "S-200_Launcher", + --["unitId"] = 34, + ["y"] = vec3.z-80, + ["x"] = vec3.x-80, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [3] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + +function pg.spawnSamSA6 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "Kub 1S91 str", + --["unitId"] = 35, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "Kub 2P25 ln", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + +function pg.spawnSamSA10 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "S-300PS 40B6M tr", + --["unitId"] = 35, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "S-300PS 64H6E sr", + --["unitId"] = 34, + ["y"] = vec3.z-10, + ["x"] = vec3.x-10, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + [3] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "S-300PS 54K6 cp", + --["unitId"] = 34, + ["y"] = vec3.z-20, + ["x"] = vec3.x-20, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [3] + [4] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "S-300PS 5P85C ln", + --["unitId"] = 34, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [4] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + +function pg.spawnSamSA11 (vec3,heading) + --where + --name + group = {} + group.groupName = "poleGenerator" .. pg.samCounter + group.units = { + [1] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["hidden"] = pg.hidden, + ["type"] = "SA-11 Buk SR 9S18M1", + --["unitId"] = 35, + ["y"] = vec3.z-40, + ["x"] = vec3.x-40, + --["name"] = "Ground-1-2", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [1] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "SA-11 Buk CC 9S470M1", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + [2] = + { + ["skill"] = "High", + ["coldAtStart"] = false, + ["type"] = "SA-11 Buk LN 9A310M1", + --["unitId"] = 34, + ["y"] = vec3.z, + ["x"] = vec3.x, + --["name"] = "Ground-1-1", + ["heading"] = heading+math.pi, + ["playerCanDrive"] = false, + }, -- end of [2] + } + group.hidden = pg.hidden + group.category = "VEHICLE" + group.country = 54 + mist.dynAdd(group) + --heading +end + + + +function pg.spawnDrone(vec3,planeHeading,playerTarget,missileType) + -- where is the plane going to be in x seconds + playerTargetPos = playerTarget:getPosition().p + futurePlayerTargetPos = playerTargetPos + playerMotionVec = Object.getVelocity(playerTarget) + + --work out what type of SAM we are shooting to work out the delay, missile speed etc + + if missileType == 2 then + pg.delay = pg.samDB[2].missileDelay + pg.missileSpeed = pg.samDB[2].missileSpeed + elseif missileType == 3 then + pg.delay = pg.samDB[3].missileDelay + pg.missileSpeed = pg.samDB[3].missileSpeed + elseif missileType == 5 then + pg.delay = pg.samDB[5].missileDelay + pg.missileSpeed = pg.samDB[5].missileSpeed + elseif missileType == 6 then + pg.delay = pg.samDB[6].missileDelay + pg.missileSpeed = pg.samDB[5].missileSpeed + elseif missileType == 10 then + pg.delay = pg.samDB[10].missileDelay + pg.missileSpeed = pg.samDB[10].missileSpeed + elseif missileType == 11 then + pg.delay = pg.samDB[11].missileDelay + pg.missileSpeed = pg.samDB[11].missileSpeed + else --assume SA2 + pg.delay = pg.samDB[2].missileDelay + pg.missileSpeed = pg.samDB[2].missileSpeed + end + + futurePlayerTargetPos.x = playerTargetPos.x + playerMotionVec.x*pg.delay + futurePlayerTargetPos.y = playerTargetPos.y + playerMotionVec.y*pg.delay + futurePlayerTargetPos.z = playerTargetPos.z + playerMotionVec.z*pg.delay + droneTurnPoint = mist.projectPoint(futurePlayerTargetPos, 10000 ,planeHeading+math.pi) + --this is where the plane will be when the missile is launched + + + + --pythago to get hyp + --x^2 + y^x = hyp^2 + x = mist.utils.get2DDist(futurePlayerTargetPos,vec3) + + y = playerTargetPos.y + --pg.notify(y,5) + hyp = math.sqrt(x^2 + y^2) + roughFlightTime = hyp /pg.missileSpeed-- distance / speed + + futurePlayerTargetPos.x = futurePlayerTargetPos.x + playerMotionVec.x*roughFlightTime + futurePlayerTargetPos.y = futurePlayerTargetPos.y + playerMotionVec.y*roughFlightTime + futurePlayerTargetPos.z = futurePlayerTargetPos.z + playerMotionVec.z*roughFlightTime + --this is where the plane will be when the missile arrives at its altitude + + --now we need to work out where the drone is going to go + vecSub = mist.vec.sub(vec3,futurePlayerTargetPos) + heading = mist.utils.getDir(vecSub) --heading between picked location and future pos + extendDistance = x + 10000 + + alt = (((futurePlayerTargetPos.y) * extendDistance)/x) + + droneAtTimePos = mist.projectPoint(vec3, extendDistance ,heading +math.pi) + + extendDistance = extendDistance + pg.droneSpeed*roughFlightTime + droneAtStartPos = mist.projectPoint(vec3, extendDistance ,heading +math.pi) + --we want to curve the missile in the players direction + --planeHeading + + pg.makeDrone(droneAtStartPos,heading, droneAtTimePos, futurePlayerTargetPos,alt,droneTurnPoint) + if missileType == 2 then + pg.spawnSamSA2 (vec3,heading) + elseif missileType == 3 then + pg.spawnSamSA3 (vec3,heading) + elseif missileType == 5 then + pg.spawnSamSA5 (vec3,heading) + elseif missileType == 6 then + pg.spawnSamSA6 (vec3,heading) + elseif missileType == 10 then + pg.spawnSamSA10 (vec3,heading) + else --assume SA2 + pg.spawnSamSA2 (vec3,heading) + end + + +end + +function pg.makeDrone(spawnVec,heading, routeVec, nextRouteVec,alt,droneTurnPoint) + --this spawns in the drone + group = DroneClone + group.groupName = "TargetDrone" .. pg.samCounter + group.groupId = nil + group.units[1].unitId = nil + group.units[1].unitName = nil + group.units[1].y = spawnVec.z + group.units[1].x = spawnVec.x + group.units[1].heading = heading + group.units[1].alt = alt + group.route["points"][2] = group.route["points"][1] + group.route["points"][3] = group.route["points"][1] + group.route["points"][1]["y"] = routeVec.z + group.route["points"][1]["x"] = routeVec.x + group.route["points"][1]["alt"] = alt + group.route["points"][2]["y"] = nextRouteVec.z + group.route["points"][2]["x"] = nextRouteVec.x + group.route["points"][2]["alt"] = alt + group.route["points"][3]["y"] = droneTurnPoint.z + group.route["points"][3]["x"] = droneTurnPoint.x + group.route["points"][3]["alt"] = alt + group.countryId = 56 + group.category = 'AIRPLANE' + mist.dynAdd(group) +end + + +function pg.attack (vars) + targetID = vars[1] + samGroup = vars[2] + AttackUnit = { + id = 'AttackUnit', + params = { + unitId = targetID, + attackQtyLimit = true, + attackQty = 1, + } + } + + local controller = samGroup:getController() + controller:pushTask(AttackUnit) +end + + + +function pg.radarOff () + group = Group.getByName(pg.name) + local controller = group:getController() + controller:setOption(9,1) +end + +function pg.radarOn () + group = Group.getByName(pg.name) + local controller = group:getController() + controller:setOption(9,0) +end + + DroneClone= + { + ["modulation"] = 0, + ["tasks"] = + { + }, -- end of ["tasks"] + ["task"] = "Reconnaissance", + ["uncontrolled"] = false, + ["route"] = + { + ["points"] = + { + [1] = + { + ["alt"] = 2000, + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["speed"] = 82.222222222222, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + {}, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["type"] = "Turning Point", + ["ETA"] = 0, + ["ETA_locked"] = true, + ["y"] = 0, + ["x"] = 0, + ["formation_template"] = "", + ["speed_locked"] = true, + }, -- end of [1] + [2] = + { + ["alt"] = 2000, + ["action"] = "Turning Point", + ["alt_type"] = "BARO", + ["speed"] = 82.222222222222, + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + }, -- end of ["tasks"] + }, -- end of ["params"] + }, -- end of ["task"] + ["type"] = "Turning Point", + ["ETA"] = 157.20107538291, + ["ETA_locked"] = false, + ["y"] = 0, + ["x"] = 0, + ["formation_template"] = "", + ["speed_locked"] = true, + }, -- end of [2] + }, -- end of ["points"] + }, -- end of ["route"] + --["groupId"] = 1, + ["hidden"] = pg.hidden, + ["units"] = + { + [1] = + { + ["alt"] = 2000, + ["alt_type"] = "BARO", + ["livery_id"] = "'camo' scheme", + ["skill"] = "High", + ["speed"] = 82.222222222222, + ["type"] = "MQ-9 Reaper", + --["unitId"] = 1, + --["psi"] = -3.129323330636, + ["y"] = 0, + ["x"] = 0, + ["payload"] = + { + ["pylons"] = + { + }, -- end of ["pylons"] + ["fuel"] = 1300, + ["flare"] = 0, + ["chaff"] = 0, + ["gun"] = 100, + }, -- end of ["payload"] + ["heading"] = 0.5, + ["callsign"] = + { + [1] = 1, + [2] = 1, + [3] = 1, + ["name"] = "Enfield11", + }, -- end of ["callsign"] + ["onboard_num"] = "010", + }, -- end of [1] + }, -- end of ["units"] + ["y"] = 0, + ["x"] = 0, + ["communication"] = true, + ["start_time"] = 0, + ["frequency"] = 124, + } + + +local function protectedCall(...) + local status, retval = pcall(...) + if not status then + + end +end + +function handler:onEvent(event) + protectedCall(pg.eventHandler, event) +end + +function pg.eventHandler (event) + if (26 == event.id) then --this is when someone types into a mark + local vec3 = mist.utils.makeVec3GL(event.pos) + pg.fakeSam (vec3) + end +end + +function handler:onEvent(event) + protectedCall(pg.eventHandler, event) +end + + +do + longRangeShots = missionCommands.addSubMenu("SAM") + missionCommands.addCommand ("Generate pole", longRangeShots, pg.fakeSam) + world.addEventHandler(handler) +end + +pg.notify("poleGen.lua",10) + diff --git a/scripts/raisedShots.lua b/scripts/raisedShots.lua new file mode 100644 index 00000000..95fdc8f8 --- /dev/null +++ b/scripts/raisedShots.lua @@ -0,0 +1,319 @@ +shots = {} --https://www.youtube.com/watch?v=XNtTEibFvlQ mandatory terrible listening +shots.shooterName = "TestInfantry" +shots.targetName = "TestTarget1" + +function shots.notify(message, displayFor) + trigger.action.outText(message, displayFor, false) +end + + + +--infantry + +function shots.set556 () --red + roundVelocity = 910 + unitBarrelHeight = 1 + shotsToFire = 5 + shotDelay = 4.5 + shots.notify("5.56 M4 Georgia", 2) + shots.fire() +end + +function shots.set556SAW () --red + roundVelocity = 915 + unitBarrelHeight = 0.4 + shotsToFire = 5 + shotDelay = 4.5 + shots.notify("5.56 M249 SAW", 2) + shots.fire() + +end + +function shots.setAk74() --red + roundVelocity = 900 + unitBarrelHeight = 0.9 + shotsToFire = 5 + shotDelay = 4.5 + shots.notify("Ak74", 2) + shots.fire() +end + +--technicals + +function shots.hmmwv() --red + roundVelocity = 928 + unitBarrelHeight = 2.6 + shotsToFire = 5 + shotDelay = 2.5 + shots.notify("Humm drumm Vee", 2) + shots.fire() + shots.notify(unitBarrelHeight,2) + +end + +function shots.technicalDHSKA() --green + roundVelocity = 928 + unitBarrelHeight = 2.2 + shotsToFire = 5 + shotDelay = 2.5 + shots.notify("technicalDHSKA", 2) + shots.fire() + shots.notify(unitBarrelHeight,2) + +end + +function shots.cobra() --green + roundVelocity = 928 + unitBarrelHeight = 2.85 + shotsToFire = 5 + shotDelay = 2.6 + shots.notify("Cobra", 2) + shots.fire() + shots.notify(unitBarrelHeight,2) + +end +--IFVs + +function shots.setWarrior() --white + roundVelocity = 1070 + unitBarrelHeight = 2.28 + shotsToFire = 3 + shotDelay = 6.3 + shots.fire() +end + +function shots.setBMP2() --red + roundVelocity = 970 + unitBarrelHeight = 1.95 + shotsToFire = 3 + shotDelay = 6 + shots.fire() +end + +--Tanks + +function shots.m1a1() --red + roundVelocity = 928 + unitBarrelHeight = 2.15 + shotsToFire = 5 + shotDelay = 3 + shots.notify("Abrams 50 cal", 2) + shots.fire() + shots.notify(unitBarrelHeight,2) + +end + +function shots.t55() --red and green + roundVelocity = 928 + unitBarrelHeight = 1.75 + shotsToFire = 5 + shotDelay = 2.6 + shots.notify("T-72B", 2) + shots.fire() + shots.notify(unitBarrelHeight,2) +end + +---aaaaaaaaaaaaaaaa + +function shots.ZSU57() --red + roundVelocity = 1070 + shotsToFire = 2 + shotDelay = 10.5 + shots.notify("ZSU57", 2) + unitMaxRange = 6000 + shots.fireAAA() +end + +function shots.ZSU23() --red + roundVelocity = 1050 + shotsToFire = 2 + shotDelay = 12 + shots.notify("ZSU23", 2) + unitMaxRange = 2000 + shots.fireAAA() +end + +function shots.vulcan() --red + roundVelocity = 1030 + shotsToFire = 5 + shotDelay = 5 + shots.notify("Vulcan M163", 2) + unitMaxRange = 1500 + shots.fireAAA() +end + +function shots.flak18() --red and green + roundVelocity = 870 + shotsToFire = 1 + shotDelay = 10.5 + shots.notify("Flak 18", 2) + unitMaxRange = 4000 + shots.fireAAA() +end + + + + +-- This one is really obvious, however I set towards the end of this file the "parameters" of the shots. +function shots.fire() + unit = Unit.getByName(shots.shooterName) + target = Unit.getByName(shots.targetName) + local targetMotionVec = Object.getVelocity(target) --if you don't want to do this i.e. shoot a point send a nil or a vec3 of 0's + --local targetMotionVec = nil + local targetPos = target:getPosition().p + shots.calculateAngle(roundVelocity, unit, targetPos, unitBarrelHeight, shotsToFire,shotDelay, targetMotionVec) +end + +--main bit that does the maths for simulating a "roughly level" fire fight, not to be used when aiming AAA at aircraft +function shots.calculateAngle(v, unit, targetPos, unitBarrelHeight, shotsToFire,shotDelay, targetMotionVector) + --v muzzle velocity + --unit is unit object you want shooting + --targetPos is a position on the ground, doesn't have to be a targets actual location + --unitBarrelHeigh is the height the gun is at when it fires + --shotsToFire is how many rounds to shoot, 5 is a nice number except for large calibre slow guns + --shotDelay how long it on average takes to aim from scratch at something and shoot, mostly used in aiming lead + --targetMotionVector a vec3 of the targets motion if nil just shoots at a spot + + g = 9.81 -- gravity, change if on the moon veltro you comment reading prick + + local unitPos = unit:getPosition().p + local x = mist.utils.get2DDist(unitPos,targetPos) -- horizontal range + local x3d = mist.utils.get3DDist(unitPos,targetPos) -- slant range + + y = targetPos.y - unitPos.y + y = y - unitBarrelHeight + + --works out some stuff for trig later, like x,y and hypoteneueussueee + + if targetMotionVector == nil then + --no moving? nothing to ammend + else --work out where target will be when the bullet arrives + shotDelay = shotDelay + x3d/v --time taken to aim and time for bullet to fly to spot + newPosition = mist.utils.tableShow(targetMotionVector) + targetPos.x = targetPos.x + targetMotionVector.x*shotDelay + targetPos.y = targetPos.y + targetMotionVector.y*shotDelay + targetPos.z = targetPos.z + targetMotionVector.z*shotDelay + end + + x = x - 10 --we actually are aiming 10m in front always so need to remove this + + local inner = v^4 - g * (g * x^2 + 2 * y * v^2) -- this is the inner bit of the quadratic equation for ease of code + + if inner < 0 then + -- No solution exists for these parameters, too far away do nothing we can't hit it, saves us crashing if sqrt of negative + else + local angle2 = math.atan((v^2 - math.sqrt(inner)) / (g * x)) -- do the whole quadratic equation + -- we didn't need to do the +- sqrt b^2..... bits as we care about the flat path not the one shot miles up falling down + + local aimUp = 10 * math.tan(angle2)*math.cos(angle2) -- we have to tell dcs to "aim up" at a point 10m ahead of it, this is distance * tan(angle) , so where the fuck has the cos come from? That is because aim correction for shooting up and down is basically modfied by cos(angle) so lazy correction and dcs can't shoot vertically up + local xPosDifference = (targetPos.x - unitPos.x) + local zPosDifference = (targetPos.z - unitPos.z) + local hyp = math.sqrt((xPosDifference*xPosDifference) + (zPosDifference*zPosDifference)) + xPosDifference = (xPosDifference /hyp) * 10 + zPosDifference = (zPosDifference / hyp) * 10 + + unitPos.x = unitPos.x + xPosDifference + unitPos.z = unitPos.z + zPosDifference + + --that was all basic trig maths + + local controller = unit:getController() + FireAtPoint = { + id = 'FireAtPoint', + params = { + point = {x = unitPos.x, y = unitPos.z}, + radius = 0.0001, + expendQty = shotsToFire, + expendQtyEnabled = true, + altitude = unitPos.y+unitBarrelHeight +aimUp, --this is a realtive to sea level shot, so we can shoot down + alt_type = 0, --0 = sea level, 1 = ground level + } + } + controller:pushTask(FireAtPoint) --FIREEEEEE + end +end + + +function shots.fireAAA() + unit = Unit.getByName(shots.shooterName) + target = Unit.getByName(shots.targetName) + targetMotionVec = Object.getVelocity(target) --if you don't want to do this i.e. shoot a point send a nil or a vec3 of 0's + --local targetMotionVec = nil + targetPos = target:getPosition().p + shots.aaa(roundVelocity, unit, unitMaxRange, targetPos, shotsToFire,shotDelay, targetMotionVec) +end + +--aaa fires almost straight up, no line of sight fakery needed or working out lobbing bullets onto random points +--if in range just needs to be told to shoot where the target will be in a few seconds +--if out of range, we just need to extrapolate back and fire inside max range and randomise the up and down, then let it miss + +function shots.aaa(v, unit, unitMaxRange, targetPos, shotsToFire,shotDelay, targetMotionVector) + local unitPos = unit:getPosition().p + local x = mist.utils.get2DDist(unitPos,targetPos) -- horizontal range + local x3d = mist.utils.get3DDist(unitPos,targetPos) -- slant range + + if x3d > unitMaxRange then + if targetMotionVector == nil then + --no moving? nothing to ammend + else --work out where target will be when the bullet arrives + shotDelay = shotDelay + x3d/(v/2) --time taken to aim and time for bullet to fly to spot long range roughly 50% slowdown + newPosition = mist.utils.tableShow(targetMotionVector) + targetPos.x = targetPos.x + targetMotionVector.x*shotDelay + targetPos.y = targetPos.y + targetMotionVector.y*shotDelay + targetPos.z = targetPos.z + targetMotionVector.z*shotDelay + end + + difference = mist.vec.sub(targetPos,unitPos) + unitVec = mist.vec.getUnitVec(difference) + extendPath = mist.vec.scalar_mult(unitVec ,unitMaxRange) + targetPos = mist.vec.add(extendPath,unitPos) + + + local controller = unit:getController() + FireAtPoint = { + id = 'FireAtPoint', + params = { + point = {x = targetPos.x, y = targetPos.z}, + radius = 0.0001, + expendQty = shotsToFire, + expendQtyEnabled = true, + altitude = targetPos.y+math.random(1,500), --this is a realtive to sea level shot, so we can shoot down + alt_type = 0, --0 = sea level, 1 = ground level + } + } + controller:pushTask(FireAtPoint) --FIREEEEEE + else + if targetMotionVector == nil then + --no moving? nothing to ammend + else --work out where target will be when the bullet arrives + shotDelay = shotDelay + x3d/v --time taken to aim and time for bullet to fly to spot + newPosition = mist.utils.tableShow(targetMotionVector) + targetPos.x = targetPos.x + targetMotionVector.x*shotDelay + targetPos.y = targetPos.y + targetMotionVector.y*shotDelay + targetPos.z = targetPos.z + targetMotionVector.z*shotDelay + end + + local controller = unit:getController() + FireAtPoint = { + id = 'FireAtPoint', + params = { + point = {x = targetPos.x, y = targetPos.z}, + radius = 0.0001, + expendQty = shotsToFire, + expendQtyEnabled = true, + altitude = targetPos.y, --this is a realtive to sea level shot, so we can shoot down + alt_type = 0, --0 = sea level, 1 = ground level + } + } + controller:pushTask(FireAtPoint) --FIREEEEEE + end +end + + +do + longRangeShots = missionCommands.addSubMenu("Firefight") + missionCommands.addCommand ("Fire", longRangeShots, shots.vulcan) + +end + +shots.notify("raisedShots.lua ran", 2) \ No newline at end of file diff --git a/scripts/samSimulator.lua b/scripts/samSimulator.lua new file mode 100644 index 00000000..bc17b69a --- /dev/null +++ b/scripts/samSimulator.lua @@ -0,0 +1,540 @@ +--Spawn a SAM integrated with IADS +--Spawn a normal SAM +--SAM bubble shields + +samSim = {} +samSim.samSuffix = 1 + +do -- needs to go early on + redIADS = SkynetIADS:create('Red') + redIADS:setUpdateInterval(5) + redIADS:activate() + --redIADS:addRadioMenu() +end + +function samSim.genSAten() --gens an SA 10 as you can imagine, + unit = Unit.getByName("Test") + local pointVec3Gl = unit:getPosition().p -- this is just to find where my aircraft is and whack an SA10 below it, lazy not relevant + local isHiddenCheck = math.random(100) + if isHiddenCheck > 10 then + isHidden = false + else + isHidden = true -- fairly obvious hides from F10 and F7 view + end + mist.dynAdd( + { + country = 'USSR', + category = 'vehicle', + name = "SAM " .. samSim.samSuffix, + groupName = "SAM " .. samSim.samSuffix, + groupId = 10000+samSim.samSuffix, + hidden = isHidden, + units = + { [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 40B6MD sr", --search radar needs to be first always for avoiding a skynet bug + ["y"] = pointVec3Gl.z + 50, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [1] + [2] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 40B6M tr", + ["y"] = pointVec3Gl.z, + ["x"] = pointVec3Gl.x, + ["heading"] = 4.7123889803847, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [2] + + [3] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 54K6 cp", + ["y"] = pointVec3Gl.z + 100, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [3] + [4] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 64H6E sr", + ["y"] = pointVec3Gl.z - 50, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [4] + [5] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 5P85C ln", + ["y"] = pointVec3Gl.z +200 , + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [5] + [6] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 5P85C ln", + ["y"] = pointVec3Gl.z -200, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.3161255787892, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [6] + [7] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S-300PS 5P85C ln", + ["y"] = pointVec3Gl.z , + ["x"] = pointVec3Gl.x + 200, + ["heading"] = 2.9670597283904, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [7] + [8] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Excellent", + ["type"] = "S-300PS 5P85C ln", + ["y"] = pointVec3Gl.z, + ["x"] = pointVec3Gl.x -200, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [8] + [9] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "generator_5i57", + ["y"] = pointVec3Gl.z +200, + ["x"] = pointVec3Gl.x + 200, + ["heading"] = 6.1086523819802, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [9] + [10] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "ATZ-5", + ["y"] = pointVec3Gl.z -200, + ["x"] = pointVec3Gl.x -200, + ["heading"] = 0.17453292519943, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [10] + [11] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "ATZ-5", + ["y"] = pointVec3Gl.z +550, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [11] + [12] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "GAZ-66", + ["y"] = pointVec3Gl.z +580, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [12] + [13] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "ATZ-60_Maz", + ["y"] = pointVec3Gl.z +600, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [13] + [14] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "KAMAZ Truck", + ["y"] = pointVec3Gl.z +500, + ["x"] = pointVec3Gl.x + 20, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [14] + }, -- end of units + } -- end of function + ) + redIADS:addSAMSite("SAM " .. samSim.samSuffix) --skynet bit + local detectChance = math.random(50,90) + local goLiveRange = math.random(50,90) + local harmStop = math.random(2) + if harmStop == 1 then + harmStopping = true + else + harmStopping = false + end + redIADS:getSAMSiteByGroupName("SAM " .. samSim.samSuffix):setHARMDetectionChance(detectChance) -- doesn't bloody work no idea, don't care want to reinvent this myself + redIADS:getSAMSiteByGroupName("SAM " .. samSim.samSuffix):setGoLiveRangeInPercent(goLiveRange) -- doesn't bloody work no idea, don't care want to reinvent this myself + redIADS:getSAMSiteByGroupName("SAM " .. samSim.samSuffix):setCanEngageHARM(harmStopping) -- doesn't bloody work no idea, don't care want to reinvent this myself + + samSim.samSuffix = samSim.samSuffix + 1 +end + +function samSim.genSAtwo() + unit = Unit.getByName("Test") + local pointVec3Gl = unit:getPosition().p -- this is just to find where my aircraft is and whack an SA10 below it, lazy not relevant + + local isHiddenCheck = math.random(100) + if isHiddenCheck > 50 then + isHidden = false + else + isHidden = true + end + + mist.dynAdd( + { + country = 'USSR', + category = 'vehicle', + name = "SAM " .. samSim.samSuffix, + groupName = "SAM " .. samSim.samSuffix, + groupId = 10000+samSim.samSuffix, + hidden = isHidden, + units = + { + [1] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "SNR_75V", + ["y"] = pointVec3Gl.z, + ["x"] = pointVec3Gl.x, + ["heading"] = 4.7123889803847, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [1] + [2] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S_75M_Volhov", + ["y"] = pointVec3Gl.z + 50, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [2] + [3] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S_75M_Volhov", + ["y"] = pointVec3Gl.z + 100, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [3] + [4] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S_75M_Volhov", + ["y"] = pointVec3Gl.z - 50, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [4] + [5] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S_75M_Volhov", + ["y"] = pointVec3Gl.z +200 , + ["x"] = pointVec3Gl.x, + ["heading"] = 3.1415926535898, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [5] + [6] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "S_75M_Volhov", + ["y"] = pointVec3Gl.z -200, + ["x"] = pointVec3Gl.x, + ["heading"] = 3.3161255787892, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [6] + [7] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "SKP-11", + ["y"] = pointVec3Gl.z , + ["x"] = pointVec3Gl.x + 200, + ["heading"] = 2.9670597283904, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [7] + [8] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Excellent", + ["type"] = "SKP-11", + ["y"] = pointVec3Gl.z, + ["x"] = pointVec3Gl.x -200, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [8] + [9] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "p-19 s-125 sr", + ["y"] = pointVec3Gl.z +200, + ["x"] = pointVec3Gl.x + 200, + ["heading"] = 6.1086523819802, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [9] + [10] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "Ural-4320 APA-5D", + ["y"] = pointVec3Gl.z -200, + ["x"] = pointVec3Gl.x -200, + ["heading"] = 0.17453292519943, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [10] + [11] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "ATMZ-5", + ["y"] = pointVec3Gl.z +550, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [11] + [12] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "Ural-4320T", + ["y"] = pointVec3Gl.z +580, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [12] + [13] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "Ural-4320T", + ["y"] = pointVec3Gl.z +600, + ["x"] = pointVec3Gl.x, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [13] + [14] = + { + ["transportable"] = + { + ["randomTransportable"] = false, + }, -- end of ["transportable"] + ["skill"] = "Random", + ["type"] = "ATMZ-5", + ["y"] = pointVec3Gl.z +500, + ["x"] = pointVec3Gl.x + 20, + ["heading"] = 0, + ["playerCanDrive"] = true, + livery_id = "desert", + }, -- end of [14] + }, -- end of units + } -- end of function + ) + redIADS:addSAMSite("SAM " .. samSim.samSuffix) + local detectChance = math.random(75,100) + local goLiveRange = math.random(50,90) + redIADS:getSAMSiteByGroupName("SAM " .. samSim.samSuffix):setHARMDetectionChance(detectChance) + redIADS:getSAMSiteByGroupName("SAM " .. samSim.samSuffix):setGoLiveRangeInPercent(goLiveRange) + + samSim.samSuffix = samSim.samSuffix + 1 +end + +function samSim.genCMD() + unit = Unit.getByName("Test") + local pointVec3Gl = unit:getPosition().p + mist.dynAddStatic( + { + country = 'USSR', + category = 'Fortifications', + name = "CMD " .. samSim.samSuffix, + type = ".Command Center", + x = pointVec3Gl.x, + y = pointVec3Gl.z, + heading = math.pi*3/2, + } -- end of function + ) + nameToAdd = "CMD ".. samSim.samSuffix --.. " " .. "unit1" + local commandCenter = StaticObject.getByName(nameToAdd) + redIADS:addCommandCenter(commandCenter) + redIADS = SkynetIADS:create(nameToAdd) + redIADS:activate() + samSim.samSuffix = samSim.samSuffix + 1 +end + +function samSim.genEWR() + unit = Unit.getByName("Test") + local pointVec3Gl = unit:getPosition().p + mist.dynAdd( + { + country = 'USSR', + category = 'vehicle', + groupName = "EW " .. samSim.samSuffix, + name = "EW " .. samSim.samSuffix, + groupId = 20000+samSim.samSuffix, + units = + { + [1] = + { + ["skill"] = "Random", + ["type"] = "55G6 EWR", + ["y"] = pointVec3Gl.z, + ["x"] = pointVec3Gl.x, + livery_id = "", + ["heading"] = 0, + ["playerCanDrive"] = true, + }, + }, -- end of units + } -- end of function + ) + nameToAdd = "EW ".. samSim.samSuffix .. " " .. "unit1" -- oddly this is the unit name not the group, if you don't use this naming convention change it + redIADS:addEarlyWarningRadar(nameToAdd) + --redIADS:addEarlyWarningRadarsByPrefix("EW") + samSim.samSuffix = samSim.samSuffix + 1 +end + + + + +do + samSims = missionCommands.addSubMenu("Sam stuff") + missionCommands.addCommand ("Spawn SA 10", samSims, samSim.genSAten) + missionCommands.addCommand ("Spawn EWR", samSims, samSim.genEWR) + missionCommands.addCommand ("Spawn SA 2", samSims, samSim.genSAtwo) + missionCommands.addCommand ("Spawn Command Centre and activate", samSims, samSim.genCMD) +end \ No newline at end of file diff --git a/scripts/templates.lua b/scripts/templates.lua new file mode 100644 index 00000000..9c7624b1 --- /dev/null +++ b/scripts/templates.lua @@ -0,0 +1,1354 @@ +templates = +{ + ["NASAMS Bty CJTF Blue"] = + { + ["type"] = "vehicle", + ["name"] = "NASAMS Bty CJTF Blue", + ["country"] = 80, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "NASAMS_Command_Post", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [1] + [2] = + { + ["dx"] = -14.042657040991, + ["dy"] = 54.532318175887, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 2.3561944901923, + }, -- end of [2] + [3] = + { + ["dx"] = 51.255698199675, + ["dy"] = -5.1489742484409, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [3] + [4] = + { + ["dx"] = 29.255535502103, + ["dy"] = 55.936583879986, + ["name"] = "NASAMS_LN_C", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [4] + [5] = + { + ["dx"] = 21.063985561486, + ["dy"] = 15.680967029068, + ["name"] = "NASAMS_Radar_MPQ64F1", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [5] + [6] = + { + ["dx"] = -18.499094251951, + ["dy"] = -14.299637664692, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [6] + [7] = + { + ["dx"] = -7.042956299847, + ["dy"] = -18.311686379602, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [7] + }, -- end of ["units"] + }, -- end of ["NASAMS Bty CJTF Blue"] + ["NASAMS Bty CJTF Red"] = + { + ["type"] = "vehicle", + ["name"] = "NASAMS Bty CJTF Red", + ["country"] = 81, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "NASAMS_Command_Post", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [1] + [2] = + { + ["dx"] = -14.042657040991, + ["dy"] = 54.532318175887, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 2.3561944901923, + }, -- end of [2] + [3] = + { + ["dx"] = 51.255698199675, + ["dy"] = -5.1489742484409, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [3] + [4] = + { + ["dx"] = 29.255535502103, + ["dy"] = 55.936583879986, + ["name"] = "NASAMS_LN_C", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [4] + [5] = + { + ["dx"] = 21.063985561486, + ["dy"] = 15.680967029068, + ["name"] = "NASAMS_Radar_MPQ64F1", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [5] + [6] = + { + ["dx"] = -18.499094251951, + ["dy"] = -14.299637664692, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [6] + [7] = + { + ["dx"] = -7.042956299847, + ["dy"] = -18.311686379602, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [7] + }, -- end of ["units"] + }, -- end of ["NASAMS Bty CJTF Red"] + ["NASAMS Bty USA"] = + { + ["type"] = "vehicle", + ["name"] = "NASAMS Bty USA", + ["country"] = 2, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "NASAMS_Command_Post", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [1] + [2] = + { + ["dx"] = -14.042657040991, + ["dy"] = 54.532318175887, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 2.3561944901923, + }, -- end of [2] + [3] = + { + ["dx"] = 51.255698199675, + ["dy"] = -5.1489742484409, + ["name"] = "NASAMS_LN_B", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [3] + [4] = + { + ["dx"] = 29.255535502103, + ["dy"] = 55.936583879986, + ["name"] = "NASAMS_LN_C", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [4] + [5] = + { + ["dx"] = 21.063985561486, + ["dy"] = 15.680967029068, + ["name"] = "NASAMS_Radar_MPQ64F1", + ["skill"] = "High", + ["heading"] = 6.2765352930821, + }, -- end of [5] + [6] = + { + ["dx"] = -18.499094251951, + ["dy"] = -14.299637664692, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [6] + [7] = + { + ["dx"] = -10.08824481722, + ["dy"] = -14.611150606768, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [7] + }, -- end of ["units"] + }, -- end of ["NASAMS Bty USA"] + ["BRITISH BOFORS AA BATTERY"] = + { + ["type"] = "vehicle", + ["name"] = "BRITISH BOFORS AA BATTERY", + ["country"] = 4, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "bofors40", + ["skill"] = "Average", + ["heading"] = 5.6374134839417, + }, -- end of [1] + [2] = + { + ["dx"] = 8.6139882987, + ["dy"] = 8.0418525209971, + ["name"] = "bofors40", + ["skill"] = "Average", + ["heading"] = 5.6374134839417, + }, -- end of [2] + }, -- end of ["units"] + }, -- end of ["BRITISH BOFORS AA BATTERY"] + ["SA-11 SAM Battery"] = + { + ["country"] = 0, + ["type"] = "vehicle", + ["name"] = "SA-11 SAM Battery", + ["units"] = + { + [7] = + { + ["dx"] = 120.44134814, + ["dy"] = -112.000602515, + ["name"] = "ATZ-10", + ["skill"] = "High", + ["heading"] = 2.4958208303519, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "SA-11 Buk SR 9S18M1", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [1] + [2] = + { + ["dx"] = 2.6563550499995, + ["dy"] = 100.877549925, + ["name"] = "SA-11 Buk LN 9A310M1", + ["skill"] = "High", + ["heading"] = 4.7123889803847, + }, -- end of [2] + [4] = + { + ["dx"] = -105.82160159, + ["dy"] = 4.8958284879991, + ["name"] = "SA-11 Buk LN 9A310M1", + ["skill"] = "High", + ["heading"] = 0.034906585039887, + }, -- end of [4] + [8] = + { + ["dx"] = 27.63662248, + ["dy"] = 21.425097783998, + ["name"] = "ZiL-131 APA-80", + ["skill"] = "High", + ["heading"] = 1.5882496193148, + }, -- end of [8] + [9] = + { + ["dx"] = 42.172491459991, + ["dy"] = -20.017069267, + ["name"] = "Ural-4320-31", + ["skill"] = "High", + ["heading"] = 0.05235987755983, + }, -- end of [9] + [5] = + { + ["dx"] = 99.169831179999, + ["dy"] = -3.3463691979996, + ["name"] = "SA-11 Buk LN 9A310M1", + ["skill"] = "High", + ["heading"] = 3.1415926535898, + }, -- end of [5] + [10] = + { + ["dx"] = 42.172491459991, + ["dy"] = -28.818736966998, + ["name"] = "Ural-4320-31", + ["skill"] = "High", + ["heading"] = 0.034906585039887, + }, -- end of [10] + [3] = + { + ["dx"] = -3.9905785699957, + ["dy"] = -102.252741427, + ["name"] = "SA-11 Buk LN 9A310M1", + ["skill"] = "High", + ["heading"] = 1.553343034275, + }, -- end of [3] + [6] = + { + ["dx"] = 129.64229952999, + ["dy"] = -104.569064854, + ["name"] = "ATZ-10", + ["skill"] = "High", + ["heading"] = 2.3212879051525, + }, -- end of [6] + [12] = + { + ["dx"] = -13.972843719996, + ["dy"] = -17.667666831003, + ["name"] = "SA-11 Buk CC 9S470M1", + ["skill"] = "High", + ["heading"] = 1.553343034275, + }, -- end of [12] + [11] = + { + ["dx"] = 22.603219309996, + ["dy"] = 21.425097783998, + ["name"] = "Ural-375 PBU", + ["skill"] = "High", + ["heading"] = 1.5882496193148, + }, -- end of [11] + }, -- end of ["units"] + }, -- end of ["SA-11 SAM Battery"] + ["Patriot site"] = + { + ["country"] = 2, + ["type"] = "vehicle", + ["name"] = "Patriot site", + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "Patriot str", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [1] + [2] = + { + ["dx"] = -19.60004352557, + ["dy"] = 9.2644465620397, + ["name"] = "Patriot EPP", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [2] + [4] = + { + ["dx"] = -52.779034586449, + ["dy"] = -20.804649357509, + ["name"] = "Patriot AMG", + ["skill"] = "High", + ["heading"] = 4.7123889803847, + }, -- end of [4] + [8] = + { + ["dx"] = 302.6260036147, + ["dy"] = -119.57223135463, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [8] + [16] = + { + ["dx"] = -81.188374167279, + ["dy"] = -130.70164152705, + ["name"] = "Hummer", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [16] + [17] = + { + ["dx"] = -95.083442423507, + ["dy"] = -114.72675693111, + ["name"] = "M978 HEMTT Tanker", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [17] + [9] = + { + ["dx"] = 173.84730488189, + ["dy"] = -29.940192911235, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [9] + [18] = + { + ["dx"] = -55.049316178425, + ["dy"] = -162.07189504143, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [18] + [5] = + { + ["dx"] = -35.511640445024, + ["dy"] = 16.23725082296, + ["name"] = "Patriot cp", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [5] + [10] = + { + ["dx"] = 287.4638047662, + ["dy"] = 8.9459295797569, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [10] + [20] = + { + ["dx"] = -26.639011004663, + ["dy"] = 25.619801495515, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [20] + [11] = + { + ["dx"] = 101.78340133674, + ["dy"] = 47.897296695373, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 0.3490658503988, + }, -- end of [11] + [3] = + { + ["dx"] = -35.248201985043, + ["dy"] = -0.8707412587828, + ["name"] = "Patriot ECS", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [3] + [6] = + { + ["dx"] = 136.71587389408, + ["dy"] = -115.36026572896, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [6] + [12] = + { + ["dx"] = 187.95200156058, + ["dy"] = 104.06103102239, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 0.3490658503988, + }, -- end of [12] + [13] = + { + ["dx"] = -31.67915746152, + ["dy"] = 171.21775733319, + ["name"] = "M1097 Avenger", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [13] + [7] = + { + ["dx"] = 233.22284679177, + ["dy"] = -169.50440608741, + ["name"] = "Patriot ln", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [7] + [14] = + { + ["dx"] = 46.357670822879, + ["dy"] = -267.34404672193, + ["name"] = "M1097 Avenger", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [14] + [19] = + { + ["dx"] = 139.89969071567, + ["dy"] = -62.41809110227, + ["name"] = "M 818", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [19] + [15] = + { + ["dx"] = -68.129344288536, + ["dy"] = -146.06991632772, + ["name"] = "Hummer", + ["skill"] = "High", + ["heading"] = 5.9341194567807, + }, -- end of [15] + }, -- end of ["units"] + }, -- end of ["Patriot site"] + ["Rapier SAM Battery Oman"] = + { + ["units"] = + { + [7] = + { + ["dx"] = 36.182838894005, + ["dy"] = 58.774643101002, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "rapier_fsa_blindfire_radar", + ["skill"] = "High", + ["heading"] = 0.013157135472537, + }, -- end of [1] + [2] = + { + ["dx"] = -14.178539628003, + ["dy"] = 10.201632172, + ["name"] = "rapier_fsa_optical_tracker_unit", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [2] + [4] = + { + ["dx"] = -50.662342819996, + ["dy"] = 58.961975773003, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 5.7595865315813, + }, -- end of [4] + [8] = + { + ["dx"] = 32.038867964002, + ["dy"] = 61.828095365003, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [8] + [9] = + { + ["dx"] = 22.442303705, + ["dy"] = 69.897933492997, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [9] + [5] = + { + ["dx"] = -54.812159296009, + ["dy"] = -56.368340474997, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 0.80285145591739, + }, -- end of [5] + [10] = + { + ["dx"] = 52.758722615996, + ["dy"] = 42.634966846002, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [10] + [3] = + { + ["dx"] = -66.224154607, + ["dy"] = 1.2103631389982, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 0.017453292519943, + }, -- end of [3] + [6] = + { + ["dx"] = 49.050959151995, + ["dy"] = 45.906522844001, + ["name"] = "Land_Rover_101_FC", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [6] + [11] = + { + ["dx"] = 39.890602357991, + ["dy"] = 55.066879637001, + ["name"] = "Land_Rover_101_FC", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [11] + }, -- end of ["units"] + ["type"] = "vehicle", + ["name"] = "Rapier SAM Battery Oman", + ["country"] = 73, + }, -- end of ["Rapier SAM Battery Oman"] + ["Insurgents mortar crew with car"] = + { + ["country"] = 17, + ["type"] = "vehicle", + ["name"] = "insurgents mortar crew with car", + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "2B11 mortar", + ["skill"] = "Average", + ["heading"] = 2.3091703583438, + }, -- end of [1] + [2] = + { + ["dx"] = -2.0000000010041, + ["dy"] = -3.1428571399883, + ["name"] = "Soldier AK", + ["skill"] = "Average", + ["heading"] = 2.3091703583438, + }, -- end of [2] + [4] = + { + ["dx"] = 4.2857142849971, + ["dy"] = -3.7142857200233, + ["name"] = "VAZ Car", + ["skill"] = "Average", + ["heading"] = 2.3091703583438, + }, -- end of [4] + [3] = + { + ["dx"] = 2.2857142851426, + ["dy"] = 1.7142857114086, + ["name"] = "Soldier AK", + ["skill"] = "Average", + ["heading"] = 2.3091703583438, + }, -- end of [3] + }, -- end of ["units"] + }, -- end of ["insurgents mortar crew with car"] + ["SA-6 SAM Battery"] = + { + ["country"] = 0, + ["type"] = "vehicle", + ["name"] = "SA-6 SAM Battery", + ["units"] = + { + [7] = + { + ["dx"] = 120.44134814, + ["dy"] = -112.000602515, + ["name"] = "ATZ-10", + ["skill"] = "High", + ["heading"] = 2.4958208303519, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "Kub 1S91 str", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [1] + [2] = + { + ["dx"] = 2.6563550499995, + ["dy"] = 100.877549925, + ["name"] = "Kub 2P25 ln", + ["skill"] = "High", + ["heading"] = 4.7123889803847, + }, -- end of [2] + [4] = + { + ["dx"] = -105.82160159, + ["dy"] = 4.8958284879991, + ["name"] = "Kub 2P25 ln", + ["skill"] = "High", + ["heading"] = 0.034906585039887, + }, -- end of [4] + [8] = + { + ["dx"] = 25.605996860002, + ["dy"] = 20.019633917, + ["name"] = "ZiL-131 APA-80", + ["skill"] = "High", + ["heading"] = 1.5882496193148, + }, -- end of [8] + [9] = + { + ["dx"] = 42.172491459991, + ["dy"] = -20.017069267, + ["name"] = "Ural-4320-31", + ["skill"] = "High", + ["heading"] = 0.05235987755983, + }, -- end of [9] + [5] = + { + ["dx"] = 99.169831179999, + ["dy"] = -3.3463691979996, + ["name"] = "Kub 2P25 ln", + ["skill"] = "High", + ["heading"] = 3.1415926535898, + }, -- end of [5] + [10] = + { + ["dx"] = 42.172491459991, + ["dy"] = -28.818736966998, + ["name"] = "Ural-4320-31", + ["skill"] = "High", + ["heading"] = 0.034906585039887, + }, -- end of [10] + [3] = + { + ["dx"] = -3.9905785699957, + ["dy"] = -102.252741427, + ["name"] = "Kub 2P25 ln", + ["skill"] = "High", + ["heading"] = 1.553343034275, + }, -- end of [3] + [6] = + { + ["dx"] = 129.64229952999, + ["dy"] = -104.569064854, + ["name"] = "ATZ-10", + ["skill"] = "High", + ["heading"] = 2.3212879051525, + }, -- end of [6] + [11] = + { + ["dx"] = 22.038305519993, + ["dy"] = 20.113878987999, + ["name"] = "Ural-375 PBU", + ["skill"] = "High", + ["heading"] = 1.5882496193148, + }, -- end of [11] + }, -- end of ["units"] + }, -- end of ["SA-6 SAM Battery"] + ["FLAK18 BATTERY"] = + { + ["type"] = "vehicle", + ["name"] = "FLAK18 BATTERY", + ["country"] = 66, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "flak18", + ["skill"] = "Average", + ["heading"] = 6.261241987952, + }, -- end of [1] + [2] = + { + ["dx"] = -3.8613997810025, + ["dy"] = 54.05821418362, + ["name"] = "flak18", + ["skill"] = "Average", + ["heading"] = 6.261241987952, + }, -- end of [2] + [3] = + { + ["dx"] = -50.700600202996, + ["dy"] = -8.27913509211, + ["name"] = "flak18", + ["skill"] = "Average", + ["heading"] = 6.261241987952, + }, -- end of [3] + [4] = + { + ["dx"] = -42.560838181002, + ["dy"] = 64.09794160531, + ["name"] = "flak18", + ["skill"] = "Average", + ["heading"] = 6.261241987952, + }, -- end of [4] + }, -- end of ["units"] + }, -- end of ["FLAK18 BATTERY"] + ["Rapier SAM Battery UAE"] = + { + ["units"] = + { + [7] = + { + ["dx"] = 36.182838894005, + ["dy"] = 58.774643101002, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "rapier_fsa_blindfire_radar", + ["skill"] = "High", + ["heading"] = 0.013157135472537, + }, -- end of [1] + [2] = + { + ["dx"] = -14.178539628003, + ["dy"] = 10.201632172, + ["name"] = "rapier_fsa_optical_tracker_unit", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [2] + [4] = + { + ["dx"] = -50.662342819996, + ["dy"] = 58.961975773003, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 5.7595865315813, + }, -- end of [4] + [8] = + { + ["dx"] = 32.038867964002, + ["dy"] = 61.828095365003, + ["name"] = "Land_Rover_101_FC", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [8] + [9] = + { + ["dx"] = 22.442303705, + ["dy"] = 69.897933492997, + ["name"] = "Land_Rover_101_FC", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [9] + [5] = + { + ["dx"] = -54.812159296009, + ["dy"] = -56.368340474997, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 0.80285145591739, + }, -- end of [5] + [10] = + { + ["dx"] = 52.758722615996, + ["dy"] = 42.634966846002, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [10] + [3] = + { + ["dx"] = -66.224154607, + ["dy"] = 1.2103631389982, + ["name"] = "rapier_fsa_launcher", + ["skill"] = "High", + ["heading"] = 0.017453292519943, + }, -- end of [3] + [6] = + { + ["dx"] = 49.050959151995, + ["dy"] = 45.906522844001, + ["name"] = "Land_Rover_101_FC", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [6] + [11] = + { + ["dx"] = 39.890602357991, + ["dy"] = 55.066879637001, + ["name"] = "Land_Rover_109_S3", + ["skill"] = "High", + ["heading"] = 4.1364303272266, + }, -- end of [11] + }, -- end of ["units"] + ["type"] = "vehicle", + ["name"] = "Rapier SAM Battery UAE", + ["country"] = 74, + }, -- end of ["Rapier SAM Battery UAE"] + ["SA-2 SAM Battery"] = + { + ["country"] = 0, + ["type"] = "vehicle", + ["name"] = "SA-2 SAM Battery", + ["units"] = + { + [13] = + { + ["dx"] = -137.35942972999, + ["dy"] = -151.259040031, + ["name"] = "ATMZ-5", + ["skill"] = "High", + ["heading"] = 1.0297442586767, + }, -- end of [13] + [7] = + { + ["dx"] = 80.690318210007, + ["dy"] = 54.289070001003, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 0.92502450355699, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "SNR_75V", + ["skill"] = "High", + ["heading"] = 0.0038885041518015, + }, -- end of [1] + [2] = + { + ["dx"] = 79.099061770001, + ["dy"] = -47.096697396999, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 5.4803338512622, + }, -- end of [2] + [4] = + { + ["dx"] = -1.8276942699886, + ["dy"] = -98.244225791997, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 4.6774823953448, + }, -- end of [4] + [8] = + { + ["dx"] = 29.956672100001, + ["dy"] = 44.636165102005, + ["name"] = "ZIL-131 KUNG", + ["skill"] = "High", + ["heading"] = 4.1713369122664, + }, -- end of [8] + [11] = + { + ["dx"] = -49.941045745189, + ["dy"] = 164.38476072691, + ["name"] = "Ural-4320 APA-5D", + ["skill"] = "High", + ["heading"] = 0.68067840827779, + }, -- end of [11] + [9] = + { + ["dx"] = 25.126949860001, + ["dy"] = 47.889142120999, + ["name"] = "ZIL-131 KUNG", + ["skill"] = "High", + ["heading"] = 4.1713369122664, + }, -- end of [9] + [5] = + { + ["dx"] = -85.254996139993, + ["dy"] = -46.869375048002, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 3.8048177693476, + }, -- end of [5] + [10] = + { + ["dx"] = -61.142498229994, + ["dy"] = 165.65584994, + ["name"] = "p-19 s-125 sr", + ["skill"] = "High", + ["heading"] = 2.2165681500328, + }, -- end of [10] + [14] = + { + ["dx"] = -168.04336334999, + ["dy"] = -84.915399762001, + ["name"] = "Ural-4320T", + ["skill"] = "High", + ["heading"] = 5.4279739737024, + }, -- end of [14] + [3] = + { + ["dx"] = 0.21820687000582, + ["dy"] = 106.800532487, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 1.535889741755, + }, -- end of [3] + [6] = + { + ["dx"] = -87.755541969993, + ["dy"] = 57.926227576005, + ["name"] = "S_75M_Volhov", + ["skill"] = "High", + ["heading"] = 2.3561944901923, + }, -- end of [6] + [12] = + { + ["dx"] = -118.28563314999, + ["dy"] = -171.162132111, + ["name"] = "ATMZ-5", + ["skill"] = "High", + ["heading"] = 0.87266462599716, + }, -- end of [12] + [15] = + { + ["dx"] = -151.45745328999, + ["dy"] = -70.817376204999, + ["name"] = "Ural-4320T", + ["skill"] = "High", + ["heading"] = 5.3407075111026, + }, -- end of [15] + }, -- end of ["units"] + }, -- end of ["SA-2 SAM Battery"] + ["SA-3 SAM Battery"] = + { + ["country"] = 0, + ["type"] = "vehicle", + ["name"] = "SA-3 SAM Battery", + ["units"] = + { + [7] = + { + ["dx"] = -0.19719014999282, + ["dy"] = 16.417675958997, + ["name"] = "ZIL-131 KUNG", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "snr s-125 tr", + ["skill"] = "High", + ["heading"] = 6.2641478001644, + }, -- end of [1] + [2] = + { + ["dx"] = -54.395544479994, + ["dy"] = 20.411364487998, + ["name"] = "5p73 s-125 ln", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [2] + [4] = + { + ["dx"] = -26.404573919994, + ["dy"] = 38.533723629997, + ["name"] = "5p73 s-125 ln", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [4] + [8] = + { + ["dx"] = 6.6801895600074, + ["dy"] = 13.694317328001, + ["name"] = "ZiL-131 APA-80", + ["skill"] = "High", + ["heading"] = 3.1241393610699, + }, -- end of [8] + [9] = + { + ["dx"] = 73.148097120007, + ["dy"] = 24.177887045, + ["name"] = "GAZ-66", + ["skill"] = "High", + ["heading"] = 1.6057029118348, + }, -- end of [9] + [5] = + { + ["dx"] = -28.378296209994, + ["dy"] = -36.108864519003, + ["name"] = "5p73 s-125 ln", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [5] + [10] = + { + ["dx"] = 67.422903450002, + ["dy"] = 24.220744209997, + ["name"] = "GAZ-66", + ["skill"] = "High", + ["heading"] = 1.6406094968747, + }, -- end of [10] + [3] = + { + ["dx"] = -55.65154957, + ["dy"] = -15.115636602, + ["name"] = "5p73 s-125 ln", + ["skill"] = "High", + ["heading"] = 3.1590459461097, + }, -- end of [3] + [6] = + { + ["dx"] = 33.753619990006, + ["dy"] = -73.286112753005, + ["name"] = "p-19 s-125 sr", + ["skill"] = "High", + ["heading"] = 6.2641478001644, + }, -- end of [6] + [12] = + { + ["dx"] = 39.803062310006, + ["dy"] = -66.502451588, + ["name"] = "ZiL-131 APA-80", + ["skill"] = "High", + ["heading"] = 1.6406094968747, + }, -- end of [12] + [11] = + { + ["dx"] = 62.727714700013, + ["dy"] = 24.526952171, + ["name"] = "GAZ-66", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [11] + }, -- end of ["units"] + }, -- end of ["SA-3 SAM Battery"] + ["Hawk SAM Battery"] = + { + ["country"] = 2, + ["type"] = "vehicle", + ["name"] = "Hawk SAM Battery", + ["units"] = + { + [7] = + { + ["dx"] = -143.06468370894, + ["dy"] = -28.566666166706, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 2.6179938779915, + }, -- end of [7] + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "Hawk pcp", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [1] + [2] = + { + ["dx"] = 32.279602998366, + ["dy"] = -40.996889924631, + ["name"] = "Hawk sr", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [2] + [4] = + { + ["dx"] = -73.507952767894, + ["dy"] = -58.130136438762, + ["name"] = "Hawk tr", + ["skill"] = "High", + ["heading"] = 3.8397243543875, + }, -- end of [4] + [8] = + { + ["dx"] = 71.695053443613, + ["dy"] = 58.889218891971, + ["name"] = "Hawk tr", + ["skill"] = "High", + ["heading"] = 0.69813170079773, + }, -- end of [8] + [9] = + { + ["dx"] = 142.58862828931, + ["dy"] = 26.52486901707, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 5.7595865315813, + }, -- end of [9] + [5] = + { + ["dx"] = -53.646780668367, + ["dy"] = -135.33353399258, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 4.7123889803847, + }, -- end of [5] + [10] = + { + ["dx"] = 139.59213643564, + ["dy"] = 111.20097058237, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 0.69813170079773, + }, -- end of [10] + [3] = + { + ["dx"] = -34.076534003809, + ["dy"] = 41.931256096927, + ["name"] = "Hawk cwar", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [3] + [6] = + { + ["dx"] = -137.08263885655, + ["dy"] = -113.8479052746, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 3.8397243543875, + }, -- end of [6] + [11] = + { + ["dx"] = 51.990893861743, + ["dy"] = 137.17585691391, + ["name"] = "Hawk ln", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [11] + }, -- end of ["units"] + }, -- end of ["Hawk SAM Battery"] + ["SA-10 SAM Battery"] = + { + ["type"] = "vehicle", + ["name"] = "SA-10 SAM Battery", + ["country"] = 0, + ["units"] = + { + [1] = + { + ["dx"] = 0, + ["dy"] = 0, + ["name"] = "S-300PS 40B6M tr", + ["skill"] = "High", + ["heading"] = 4.7123889803847, + }, -- end of [1] + [2] = + { + ["dx"] = 0.69314285699511, + ["dy"] = 127.97571428004, + ["name"] = "S-300PS 40B6MD sr", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [2] + [3] = + { + ["dx"] = 23.579234991223, + ["dy"] = 246.55467524601, + ["name"] = "S-300PS 54K6 cp", + ["skill"] = "High", + ["heading"] = 3.1415926535898, + }, -- end of [3] + [4] = + { + ["dx"] = -22.516027817794, + ["dy"] = 246.55467524601, + ["name"] = "S-300PS 64H6E sr", + ["skill"] = "High", + ["heading"] = 3.1415926535898, + }, -- end of [4] + [5] = + { + ["dx"] = 83.349983285123, + ["dy"] = -1.3806866992963, + ["name"] = "S-300PS 5P85C ln", + ["skill"] = "High", + ["heading"] = 3.1415926535898, + }, -- end of [5] + [6] = + { + ["dx"] = 82.498640577192, + ["dy"] = 16.104647497996, + ["name"] = "S-300PS 5P85D ln", + ["skill"] = "High", + ["heading"] = 3.3161255787892, + }, -- end of [6] + [7] = + { + ["dx"] = 82.547616217693, + ["dy"] = -18.227276489837, + ["name"] = "S-300PS 5P85D ln", + ["skill"] = "High", + ["heading"] = 2.9670597283904, + }, -- end of [7] + [8] = + { + ["dx"] = -82.640406328603, + ["dy"] = -0.41562629467808, + ["name"] = "S-300PS 5P85C ln", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [8] + [9] = + { + ["dx"] = -81.939684967569, + ["dy"] = 17.115632734494, + ["name"] = "S-300PS 5P85D ln", + ["skill"] = "High", + ["heading"] = 6.1086523819802, + }, -- end of [9] + [10] = + { + ["dx"] = -81.939684967569, + ["dy"] = -17.99454369233, + ["name"] = "S-300PS 5P85D ln", + ["skill"] = "High", + ["heading"] = 0.17453292519943, + }, -- end of [10] + [11] = + { + ["dx"] = -9.0858776818495, + ["dy"] = 187.67713509151, + ["name"] = "generator_5i57", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [11] + [12] = + { + ["dx"] = 0.83760223048739, + ["dy"] = 187.51811292395, + ["name"] = "generator_5i57", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [12] + [13] = + { + ["dx"] = -59.823818980018, + ["dy"] = 168.63468487991, + ["name"] = "ATZ-5", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [13] + [14] = + { + ["dx"] = -59.823818980018, + ["dy"] = 179.2654343833, + ["name"] = "ATZ-5", + ["skill"] = "High", + ["heading"] = 0, + }, -- end of [14] + [15] = + { + ["dx"] = 20.947679329896, + ["dy"] = -62.811427216162, + ["name"] = "GAZ-66", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [15] + [16] = + { + ["dx"] = 66.751355714747, + ["dy"] = 151.35592090525, + ["name"] = "ATZ-60_Maz", + ["skill"] = "High", + ["heading"] = 3.9269908169872, + }, -- end of [16] + [17] = + { + ["dx"] = 59.63926918729, + ["dy"] = 158.46800743265, + ["name"] = "ATZ-60_Maz", + ["skill"] = "High", + ["heading"] = 3.9269908169872, + }, -- end of [17] + [18] = + { + ["dx"] = -16.327227612433, + ["dy"] = -62.472874663305, + ["name"] = "KAMAZ Truck", + ["skill"] = "High", + ["heading"] = 1.5707963267949, + }, -- end of [18] + }, -- end of ["units"] + }, -- end of ["SA-10 SAM Battery"] +} -- end of templates diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 00000000..e8ff2462 --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "*.ejs": "html", + "xstring": "cpp", + "vector": "cpp", + "list": "cpp" + } +} \ No newline at end of file diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 781d4f5f..4759cb4f 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -36,6 +36,7 @@ + @@ -43,7 +44,7 @@ - + @@ -51,6 +52,7 @@ + @@ -58,7 +60,7 @@ - + @@ -183,7 +185,7 @@ NotUsing - include;..\..\third-party\lua\include;..\utils\include;..\shared\include;..\dcstools\include;..\logger\include;..\luatools\include + include;..\..\third-party\base64\include;..\..\third-party\lua\include;..\utils\include;..\shared\include;..\dcstools\include;..\logger\include;..\luatools\include stdcpp20 diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index d0cc64a4..00e7a412 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -39,12 +39,15 @@ Header Files - + Header Files Header Files + + Header Files + @@ -80,11 +83,14 @@ Source Files - + Source Files Source Files + + Source Files + \ No newline at end of file diff --git a/src/core/include/Unit.h b/src/core/include/Unit.h deleted file mode 100644 index 3b99897b..00000000 --- a/src/core/include/Unit.h +++ /dev/null @@ -1,119 +0,0 @@ -#pragma once -#include "framework.h" -#include "utils.h" -#include "dcstools.h" -#include "luatools.h" - -namespace State { - enum States { IDLE, REACH_DESTINATION, ATTACK, WINGMAN, FOLLOW, LAND, REFUEL, AWACS, EWR, TANKER, RUN_AWAY }; -}; - -class Unit -{ -public: - Unit(json::value json, int ID); - ~Unit(); - - void updateExportData(json::value json); - void updateMissionData(json::value json); - json::value json(); - - virtual void setState(int newState) { state = newState; }; - void resetTask(); - - void setPath(list path); - void setActiveDestination(Coords newActiveDestination) { activeDestination = newActiveDestination; } - void setAlive(bool newAlive) { alive = newAlive; } - void setTarget(int targetID); - void setIsLeader(bool newIsLeader); - void setIsWingman(bool newIsWingman); - void setLeader(Unit* newLeader) { leader = newLeader; } - void setWingmen(vector newWingmen) { wingmen = newWingmen; } - void setFormation(wstring newFormation) { formation = newFormation; } - void setFormationOffset(Offset formationOffset); - void setROE(wstring newROE); - void setReactionToThreat(wstring newReactionToThreat); - void landAt(Coords loc); - - int getID() { return ID; } - wstring getName() { return name; } - wstring getUnitName() { return unitName; } - wstring getGroupName() { return groupName; } - 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; } - double getLongitude() { return longitude; } - double getAltitude() { return altitude; } - double getHeading() { return heading; } - json::value getFlags() { return flags; } - Coords getActiveDestination() { return activeDestination; } - virtual wstring getCategory() { return L"No category"; }; - wstring getTarget(); - bool isTargetAlive(); - wstring getCurrentTask() { return currentTask; } - bool getAlive() { return alive; } - bool getIsLeader() { return isLeader; } - bool getIsWingman() { return isWingman; } - wstring getFormation() { return formation; } - - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; - virtual void setTargetSpeed(double newSpeed) { targetSpeed = newSpeed; } - virtual void setTargetAltitude(double newAltitude) { targetAltitude = newAltitude; } - virtual void changeSpeed(wstring change) {}; - virtual void changeAltitude(wstring change) {}; - - void resetActiveDestination(); - -protected: - int ID; - int state = State::IDLE; - bool hasTask = false; - 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(); - int targetID = NULL; - wstring currentTask = L""; - bool isLeader = false; - bool isWingman = false; - Offset formationOffset = Offset(NULL); - wstring formation = L""; - Unit* leader = nullptr; - wstring ROE = L""; - wstring reactionToThreat = L""; - vector wingmen; - double targetSpeed = 0; - double targetAltitude = 0; - double fuel = 0; - json::value ammo; - json::value targets; - - list activePath; - Coords activeDestination = Coords(0); - Coords oldPosition = Coords(0); // Used to approximate speed - - virtual void AIloop() = 0; - -private: - mutex mutexLock; -}; - - - - - - - - diff --git a/src/core/include/aircraft.h b/src/core/include/aircraft.h index 6d15e4f6..7fd3f615 100644 --- a/src/core/include/aircraft.h +++ b/src/core/include/aircraft.h @@ -10,12 +10,4 @@ public: virtual void changeSpeed(wstring change); virtual void changeAltitude(wstring change); - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; - virtual void setTargetSpeed(double newTargetSpeed); - virtual void setTargetAltitude(double newTargetAltitude); - -protected: - double targetSpeed = 300 / 1.94384; - double targetAltitude = 20000 * 0.3048; }; \ No newline at end of file diff --git a/src/core/include/airunit.h b/src/core/include/airunit.h index 550bbccf..cabd7b9e 100644 --- a/src/core/include/airunit.h +++ b/src/core/include/airunit.h @@ -12,19 +12,12 @@ class AirUnit : public Unit public: AirUnit(json::value json, int ID); - virtual wstring getCategory() = 0; - virtual void changeSpeed(wstring change) {}; - virtual void changeAltitude(wstring change) {}; - virtual void setTargetSpeed(double newTargetSpeed) {}; - virtual void setTargetAltitude(double newTargetAltitude) {}; + virtual void setState(int newState); + virtual wstring getCategory() = 0; + virtual void changeSpeed(wstring change) = 0; + virtual void changeAltitude(wstring change) = 0; + protected: virtual void AIloop(); - virtual void setState(int newState); - bool isDestinationReached(); - bool setActiveDestination(); - void createHoldingPattern(); - bool updateActivePath(bool looping); - void goToDestination(wstring enrouteTask = L"nil"); - void taskWingmen(); }; \ No newline at end of file diff --git a/src/core/include/Commands.h b/src/core/include/commands.h similarity index 58% rename from src/core/include/Commands.h rename to src/core/include/commands.h index 95ccefbc..d4e0bab3 100644 --- a/src/core/include/Commands.h +++ b/src/core/include/commands.h @@ -2,22 +2,19 @@ #include "framework.h" #include "luatools.h" #include "utils.h" +#include "logger.h" namespace CommandPriority { enum CommandPriorities { LOW, MEDIUM, HIGH }; }; -namespace CommandType { - enum CommandTypes { NO_TYPE, MOVE, SMOKE, SPAWN_AIR, SPAWN_GROUND, CLONE, FOLLOW, RESET_TASK, SET_OPTION, SET_COMMAND, SET_TASK }; -}; - namespace SetCommandType { enum SetCommandTypes { ROE = 0, REACTION_ON_THREAT = 1, RADAR_USING = 3, FLARE_USING = 4, - Formation = 5, + FORMATION = 5, RTB_ON_BINGO = 6, SILENCE = 7, RTB_ON_OUT_OF_AMMO = 10, @@ -28,6 +25,7 @@ namespace SetCommandType { PROHIBIT_AG = 17, MISSILE_ATTACK = 18, PROHIBIT_WP_PASS_REPORT = 19, + ENGAGE_AIR_WEAPONS = 20, OPTION_RADIO_USAGE_CONTACT = 21, OPTION_RADIO_USAGE_ENGAGE = 22, OPTION_RADIO_USAGE_KILL = 23, @@ -47,7 +45,7 @@ namespace ROE { } namespace ReactionToThreat { - enum ReactionToThreats { + enum ReactionsToThreat { NO_REACTION = 0, PASSIVE_DEFENCE = 1, EVADE_FIRE = 2, @@ -56,46 +54,73 @@ namespace ReactionToThreat { }; } +namespace RadarUse { + enum RadarUses { + NEVER = 0, + FOR_ATTACK_ONLY = 1, + FOR_SEARCH_IF_REQUIRED = 2, + FOR_CONTINUOUS_SEARCH = 3 + }; +} +namespace FlareUse { + enum FlareUses { + NEVER = 0, + AGAINST_FIRED_MISSILE = 1, + WHEN_FLYING_IN_SAM_WEZ = 2, + WHEN_FLYING_NEAR_ENEMIES = 3 + }; +} +namespace ECMUse { + enum ECMUses { + NEVER_USE = 0, + USE_IF_ONLY_LOCK_BY_RADAR = 1, + USE_IF_DETECTED_LOCK_BY_RADAR = 2, + ALWAYS_USE = 3 + }; +} /* Base command class */ class Command { public: int getPriority() { return priority; } - int getType() { return type; } virtual wstring getString(lua_State* L) = 0; + virtual int getLoad() = 0; protected: int priority = CommandPriority::LOW; - int type = CommandType::NO_TYPE; }; /* Simple low priority move command (from user click) */ class Move : public Command { public: - Move(int ID, Coords destination, double speed, double altitude, wstring unitCategory, wstring taskOptions): - ID(ID), + Move(wstring groupName, Coords destination, double speed, wstring speedType, double altitude, wstring altitudeType, wstring taskOptions, wstring category): + groupName(groupName), destination(destination), speed(speed), + speedType(speedType), altitude(altitude), - unitCategory(unitCategory), - taskOptions(taskOptions) + altitudeType(altitudeType), + taskOptions(taskOptions), + category(category) { priority = CommandPriority::HIGH; - type = CommandType::MOVE; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 5; } private: - const int ID; + const wstring groupName; const Coords destination; - const wstring unitCategory; const double speed; + const wstring speedType; const double altitude; + const wstring altitudeType; const wstring taskOptions; + const wstring category; }; /* Smoke command */ @@ -107,9 +132,9 @@ public: location(location) { priority = CommandPriority::LOW; - type = CommandType::SMOKE; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 5; } private: const wstring color; @@ -126,9 +151,9 @@ public: location(location) { priority = CommandPriority::LOW; - type = CommandType::SPAWN_GROUND; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 100; } private: const wstring coalition; @@ -148,10 +173,9 @@ public: airbaseName(airbaseName) { priority = CommandPriority::LOW; - type = CommandType::SPAWN_AIR; }; - virtual wstring getString(lua_State* L); + virtual int getLoad() { return 100; } private: const wstring coalition; @@ -170,9 +194,9 @@ public: location(location) { priority = CommandPriority::LOW; - type = CommandType::CLONE; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 100; } private: const int ID; @@ -183,33 +207,35 @@ private: class Delete : public Command { public: - Delete(int ID) : - ID(ID) + Delete(int ID, bool explosion) : + ID(ID), + explosion(explosion) { priority = CommandPriority::HIGH; - type = CommandType::CLONE; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 20; } private: const int ID; + const bool explosion; }; /* Follow command */ class SetTask : public Command { public: - SetTask(int ID, wstring task) : - ID(ID), + SetTask(wstring groupName, wstring task) : + groupName(groupName), task(task) { priority = CommandPriority::MEDIUM; - type = CommandType::FOLLOW; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring task; }; @@ -217,33 +243,33 @@ private: class ResetTask : public Command { public: - ResetTask(int ID) : - ID(ID) + ResetTask(wstring groupName) : + groupName(groupName) { priority = CommandPriority::HIGH; - type = CommandType::RESET_TASK; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; }; /* Set command */ class SetCommand : public Command { public: - SetCommand(int ID, wstring command) : - ID(ID), + SetCommand(wstring groupName, wstring command) : + groupName(groupName), command(command) { priority = CommandPriority::HIGH; - type = CommandType::RESET_TASK; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring command; }; @@ -251,18 +277,68 @@ private: class SetOption : public Command { public: - SetOption(int ID, int optionID, int optionValue) : - ID(ID), + SetOption(wstring groupName, int optionID, int optionValue) : + groupName(groupName), optionID(optionID), - optionValue(optionValue) + optionValue(optionValue), + optionBool(false), + isBoolean(false) + { + priority = CommandPriority::HIGH; + }; + + SetOption(wstring groupName, int optionID, bool optionBool) : + groupName(groupName), + optionID(optionID), + optionValue(0), + optionBool(optionBool), + isBoolean(true) { priority = CommandPriority::HIGH; - type = CommandType::RESET_TASK; }; virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const int optionID; const int optionValue; -}; \ No newline at end of file + const bool optionBool; + const bool isBoolean; +}; + +/* Set on off */ +class SetOnOff : public Command +{ +public: + SetOnOff(wstring groupName, bool onOff) : + groupName(groupName), + onOff(onOff) + { + priority = CommandPriority::HIGH; + }; + virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } + +private: + const wstring groupName; + const bool onOff; +}; + +/* Make a ground explosion */ +class Explosion : public Command +{ +public: + Explosion(int intensity, Coords location) : + location(location), + intensity(intensity) + { + priority = CommandPriority::MEDIUM; + }; + virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } + +private: + const Coords location; + const int intensity; +}; diff --git a/src/core/include/groundunit.h b/src/core/include/groundunit.h index 6edb1a3b..6b5930b0 100644 --- a/src/core/include/groundunit.h +++ b/src/core/include/groundunit.h @@ -7,13 +7,14 @@ class GroundUnit : public Unit { public: GroundUnit(json::value json, int ID); - virtual void AIloop(); - virtual wstring getCategory() { return L"GroundUnit"; }; + + virtual void setState(int newState); + virtual void changeSpeed(wstring change); - virtual void changeAltitude(wstring change) {}; - virtual double getTargetSpeed() { return targetSpeed; }; + virtual void setOnOff(bool newOnOff); + virtual void setFollowRoads(bool newFollowRoads); protected: - double targetSpeed = 10; + virtual void AIloop(); }; \ No newline at end of file diff --git a/src/core/include/helicopter.h b/src/core/include/helicopter.h index 50a0e9c2..097990b3 100644 --- a/src/core/include/helicopter.h +++ b/src/core/include/helicopter.h @@ -10,12 +10,4 @@ public: virtual void changeSpeed(wstring change); virtual void changeAltitude(wstring change); - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; - virtual void setTargetSpeed(double newTargetSpeed); - virtual void setTargetAltitude(double newTargetAltitude); - -protected: - double targetSpeed = 100 / 1.94384; - double targetAltitude = 5000 * 0.3048; }; \ No newline at end of file diff --git a/src/core/include/measure.h b/src/core/include/measure.h new file mode 100644 index 00000000..61ff410d --- /dev/null +++ b/src/core/include/measure.h @@ -0,0 +1,19 @@ +#pragma once +#include "framework.h" + +class Measure +{ +public: + Measure(json::value value, long long time): value(value), time(time) {}; + + void setValue(json::value newValue) { value = newValue; } + void setTime(long long newTime) { time = newTime; } + json::value getValue() { return value; } + long long getTime() { return time; } + +private: + json::value value; + long long time; + +}; + diff --git a/src/core/include/navyunit.h b/src/core/include/navyunit.h index d70d86d2..a27b5a31 100644 --- a/src/core/include/navyunit.h +++ b/src/core/include/navyunit.h @@ -9,9 +9,5 @@ public: virtual wstring getCategory() { return L"NavyUnit"; }; virtual void changeSpeed(wstring change); - virtual void changeAltitude(wstring change) {}; - virtual double getTargetSpeed() { return targetSpeed; }; -protected: - double targetSpeed = 10; }; \ No newline at end of file diff --git a/src/core/include/Scheduler.h b/src/core/include/scheduler.h similarity index 94% rename from src/core/include/Scheduler.h rename to src/core/include/scheduler.h index 08f015d7..d5ebfcf6 100644 --- a/src/core/include/Scheduler.h +++ b/src/core/include/scheduler.h @@ -12,9 +12,9 @@ public: void appendCommand(Command* command); void execute(lua_State* L); void handleRequest(wstring key, json::value value); - + private: list commands; - mutex mutexLock; + int load; }; diff --git a/src/core/include/scriptLoader.h b/src/core/include/scriptloader.h similarity index 100% rename from src/core/include/scriptLoader.h rename to src/core/include/scriptloader.h diff --git a/src/core/include/server.h b/src/core/include/server.h index 39db7a60..ec08d264 100644 --- a/src/core/include/server.h +++ b/src/core/include/server.h @@ -5,14 +5,16 @@ using namespace web::http; using namespace web::http::experimental::listener; -class UnitsFactory; +class UnitsManager; class Scheduler; class Server { public: Server(lua_State* L); - ~Server(); + + void start(lua_State* L); + void stop(lua_State* L); private: std::thread* serverThread; @@ -25,5 +27,7 @@ private: void task(); atomic runListener; + + wstring password = L""; }; diff --git a/src/core/include/unit.h b/src/core/include/unit.h new file mode 100644 index 00000000..817beb29 --- /dev/null +++ b/src/core/include/unit.h @@ -0,0 +1,262 @@ +#pragma once +#include "framework.h" +#include "utils.h" +#include "dcstools.h" +#include "luatools.h" +#include "measure.h" +#include "logger.h" + +#define TASK_CHECK_INIT_VALUE 10 + +namespace State +{ + enum States + { + NONE = 0, + IDLE, + REACH_DESTINATION, + ATTACK, + FOLLOW, + LAND, + REFUEL, + AWACS, + TANKER, + BOMB_POINT, + CARPET_BOMB, + BOMB_BUILDING, + FIRE_AT_AREA + }; +}; + +namespace Options { + struct TACAN + { + bool isOn = false; + int channel = 40; + wstring XY = L"X"; + wstring callsign = L"TKR"; + }; + + struct Radio + { + int frequency = 124000000; // MHz + int callsign = 1; + int callsignNumber = 1; + }; + + struct GeneralSettings + { + bool prohibitJettison = false; + bool prohibitAA = false; + bool prohibitAG = false; + bool prohibitAfterburner = false; + bool prohibitAirWpn = false; + }; +} + +class Unit +{ +public: + Unit(json::value json, int ID); + ~Unit(); + + /********** Public methods **********/ + void initialize(json::value json); + void setDefaults(bool force = false); + int getID() { return ID; } + void runAILoop(); + void updateExportData(json::value json); + void updateMissionData(json::value json); + json::value getData(long long time, bool getAll = false); + virtual wstring getCategory() { return L"No category"; }; + + /********** Base data **********/ + void setControlled(bool newControlled) { controlled = newControlled; addMeasure(L"controlled", json::value(newControlled)); } + void setName(wstring newName) { name = newName; addMeasure(L"name", json::value(newName));} + void setUnitName(wstring newUnitName) { unitName = newUnitName; addMeasure(L"unitName", json::value(newUnitName));} + void setGroupName(wstring newGroupName) { groupName = newGroupName; addMeasure(L"groupName", json::value(newGroupName));} + void setAlive(bool newAlive) { alive = newAlive; addMeasure(L"alive", json::value(newAlive));} + void setType(json::value newType) { type = newType; addMeasure(L"type", newType);} + void setCountry(int newCountry) { country = newCountry; addMeasure(L"country", json::value(newCountry));} + + bool getControlled() { return controlled; } + wstring getName() { return name; } + wstring getUnitName() { return unitName; } + wstring getGroupName() { return groupName; } + bool getAlive() { return alive; } + json::value getType() { return type; } + int getCountry() { return country; } + + /********** Flight data **********/ + void setLatitude(double newLatitude) {latitude = newLatitude; addMeasure(L"latitude", json::value(newLatitude));} + void setLongitude(double newLongitude) {longitude = newLongitude; addMeasure(L"longitude", json::value(newLongitude));} + void setAltitude(double newAltitude) {altitude = newAltitude; addMeasure(L"altitude", json::value(newAltitude));} + void setHeading(double newHeading) {heading = newHeading; addMeasure(L"heading", json::value(newHeading));} + void setSpeed(double newSpeed) {speed = newSpeed; addMeasure(L"speed", json::value(newSpeed));} + + double getLatitude() { return latitude; } + double getLongitude() { return longitude; } + double getAltitude() { return altitude; } + double getHeading() { return heading; } + double getSpeed() { return speed; } + + /********** Mission data **********/ + void setFuel(double newFuel) { fuel = newFuel; addMeasure(L"fuel", json::value(newFuel));} + void setAmmo(json::value newAmmo) { ammo = newAmmo; addMeasure(L"ammo", json::value(newAmmo));} + void setContacts(json::value newContacts) {contacts = newContacts; addMeasure(L"contacts", json::value(newContacts));} + void setHasTask(bool newHasTask); + void setCoalitionID(int newCoalitionID); + void setFlags(json::value newFlags) { flags = newFlags; addMeasure(L"flags", json::value(newFlags));} + + double getFuel() { return fuel; } + json::value getAmmo() { return ammo; } + json::value getTargets() { return contacts; } + bool getHasTask() { return hasTask; } + wstring getCoalition() { return coalition; } + int getCoalitionID(); + json::value getFlags() { return flags; } + + /********** Formation data **********/ + void setLeaderID(int newLeaderID) { leaderID = newLeaderID; addMeasure(L"leaderID", json::value(newLeaderID)); } + void setFormationOffset(Offset formationOffset); + + int getLeaderID() { return leaderID; } + Offset getFormationoffset() { return formationOffset; } + + /********** Task data **********/ + void setCurrentTask(wstring newCurrentTask) { currentTask = newCurrentTask; addMeasure(L"currentTask", json::value(newCurrentTask)); } + void setDesiredSpeed(double newDesiredSpeed); + void setDesiredAltitude(double newDesiredAltitude); + void setDesiredSpeedType(wstring newDesiredSpeedType); + void setDesiredAltitudeType(wstring newDesiredAltitudeType); + void setActiveDestination(Coords newActiveDestination) { activeDestination = newActiveDestination; addMeasure(L"activeDestination", json::value("")); } // TODO fix + void setActivePath(list newActivePath); + void setTargetID(int newTargetID) { targetID = newTargetID; addMeasure(L"targetID", json::value(newTargetID));} + void setTargetLocation(Coords newTargetLocation); + void setIsTanker(bool newIsTanker); + void setIsAWACS(bool newIsAWACS); + virtual void setOnOff(bool newOnOff) { onOff = newOnOff; addMeasure(L"onOff", json::value(newOnOff));}; + virtual void setFollowRoads(bool newFollowRoads) { followRoads = newFollowRoads; addMeasure(L"followRoads", json::value(newFollowRoads)); }; + + wstring getCurrentTask() { return currentTask; } + virtual double getDesiredSpeed() { return desiredSpeed; }; + virtual double getDesiredAltitude() { return desiredAltitude; }; + virtual wstring getDesiredSpeedType() { return desiredSpeedType; }; + virtual wstring getDesiredAltitudeType() { return desiredAltitudeType; }; + Coords getActiveDestination() { return activeDestination; } + list getActivePath() { return activePath; } + int getTargetID() { return targetID; } + Coords getTargetLocation() { return targetLocation; } + bool getIsTanker() { return isTanker; } + bool getIsAWACS() { return isAWACS; } + bool getOnOff() { return onOff; }; + bool getFollowRoads() { return followRoads; }; + + /********** Options data **********/ + void setROE(wstring newROE, bool force = false); + void setReactionToThreat(wstring newReactionToThreat, bool force = false); + void setEmissionsCountermeasures(wstring newEmissionsCountermeasures, bool force = false); + void setTACAN(Options::TACAN newTACAN, bool force = false); + void setRadio(Options::Radio newradio, bool force = false); + void setGeneralSettings(Options::GeneralSettings newGeneralSettings, bool force = false); + void setEPLRS(bool newEPLRS, bool force = false); + + wstring getROE() { return ROE; } + wstring getReactionToThreat() { return reactionToThreat; } + wstring getEmissionsCountermeasures() { return emissionsCountermeasures; }; + Options::TACAN getTACAN() { return TACAN; } + Options::Radio getRadio() { return radio; } + Options::GeneralSettings getGeneralSettings() { return generalSettings; } + bool getEPLRS() { return EPLRS; } + + /********** Control functions **********/ + void landAt(Coords loc); + virtual void changeSpeed(wstring change) {}; + virtual void changeAltitude(wstring change) {}; + void resetActiveDestination(); + virtual void setState(int newState) { state = newState; }; + void resetTask(); + void clearActivePath(); + void pushActivePathFront(Coords newActivePathFront); + void pushActivePathBack(Coords newActivePathBack); + void popActivePathFront(); + +protected: + int ID; + + map measures; + int taskCheckCounter = 0; + + /********** Base data **********/ + bool controlled = false; + wstring name = L"undefined"; + wstring unitName = L"undefined"; + wstring groupName = L"undefined"; + bool alive = true; + json::value type = json::value::null(); + int country = NULL; + + /********** Flight data **********/ + double latitude = NULL; + double longitude = NULL; + double altitude = NULL; + double speed = NULL; + double heading = NULL; + + /********** Mission data **********/ + double fuel = 0; + double initialFuel = 0; // Used internally to detect refueling completed + json::value ammo = json::value::null(); + json::value contacts = json::value::null(); + bool hasTask = false; + wstring coalition = L""; + json::value flags = json::value::null(); + + /********** Formation data **********/ + int leaderID = NULL; + Offset formationOffset = Offset(NULL); + + /********** Task data **********/ + wstring currentTask = L""; + double desiredSpeed = 0; + double desiredAltitude = 0; + wstring desiredSpeedType = L"GS"; + wstring desiredAltitudeType = L"AGL"; + list activePath; + Coords activeDestination = Coords(NULL); + int targetID = NULL; + Coords targetLocation = Coords(NULL); + bool isTanker = false; + bool isAWACS = false; + bool onOff = true; + bool followRoads = false; + + /********** Options data **********/ + wstring ROE = L"Designated"; + wstring reactionToThreat = L"Evade"; + wstring emissionsCountermeasures = L"Defend"; + Options::TACAN TACAN; + Options::Radio radio; + Options::GeneralSettings generalSettings; + bool EPLRS = false; + + /********** State machine **********/ + int state = State::NONE; + + /********** Other **********/ + Coords oldPosition = Coords(0); // Used to approximate speed + + /********** Functions **********/ + wstring getTargetName(); + wstring getLeaderName(); + bool isTargetAlive(); + bool isLeaderAlive(); + virtual void AIloop() = 0; + void addMeasure(wstring key, json::value value); + bool isDestinationReached(double threshold); + bool setActiveDestination(); + bool updateActivePath(bool looping); + void goToDestination(wstring enrouteTask = L"nil"); + bool checkTaskFailed(); + void resetTaskFailedCounter(); +}; diff --git a/src/core/include/unitsFactory.h b/src/core/include/unitsFactory.h deleted file mode 100644 index 07b0d354..00000000 --- a/src/core/include/unitsFactory.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include "framework.h" -#include "dcstools.h" - -class Unit; - -class UnitsFactory -{ -public: - UnitsFactory(lua_State* L); - ~UnitsFactory(); - - Unit* getUnit(int ID); - void updateExportData(lua_State* L); - void updateMissionData(json::value missionData); - void updateAnswer(json::value& answer); - void deleteUnit(int ID); - -private: - map units; - json::value missionDB; - -}; - diff --git a/src/core/include/unitsmanager.h b/src/core/include/unitsmanager.h new file mode 100644 index 00000000..871c9e74 --- /dev/null +++ b/src/core/include/unitsmanager.h @@ -0,0 +1,31 @@ +#pragma once +#include "framework.h" +#include "dcstools.h" + +class Unit; + +class UnitsManager +{ +public: + UnitsManager(lua_State* L); + ~UnitsManager(); + + Unit* getUnit(int ID); + bool isUnitInGroup(Unit* unit); + bool isUnitGroupLeader(Unit* unit); + Unit* getGroupLeader(int ID); + Unit* getGroupLeader(Unit* unit); + vector getGroupMembers(wstring groupName); + void updateExportData(lua_State* L); + void updateMissionData(json::value missionData); + void runAILoop(); + void getData(json::value& answer, long long time); + void deleteUnit(int ID, bool explosion); + void acquireControl(int ID); + +private: + map units; + json::value missionDB; + +}; + diff --git a/src/core/src/Scheduler.cpp b/src/core/src/Scheduler.cpp deleted file mode 100644 index 254137e1..00000000 --- a/src/core/src/Scheduler.cpp +++ /dev/null @@ -1,263 +0,0 @@ -#include "scheduler.h" -#include "logger.h" -#include "dcstools.h" -#include "unitsFactory.h" -#include "utils.h" -#include "unit.h" - -extern UnitsFactory* unitsFactory; - -Scheduler::Scheduler(lua_State* L) -{ - LogInfo(L, "Units Factory constructor called successfully"); -} - -Scheduler::~Scheduler() -{ - -} - -void Scheduler::appendCommand(Command* command) -{ - commands.push_back(command); -} - -void Scheduler::execute(lua_State* L) -{ - /* Lock for thread safety */ - lock_guard guard(mutexLock); - int priority = CommandPriority::HIGH; - while (priority >= CommandPriority::LOW) - { - for (auto command : commands) - { - if (command->getPriority() == priority) - { - wstring commandString = L"Olympus.protectedCall(" + command->getString(L) + L")"; - if (dostring_in(L, "server", to_string(commandString))) - { - log(L"Error executing command " + commandString); - } - { - log(L"Command " + commandString + L" executed succesfully"); - } - commands.remove(command); - return; - } - } - priority--; - } -} - -void Scheduler::handleRequest(wstring key, json::value value) -{ - /* Lock for thread safety */ - lock_guard guard(mutexLock); - Command* command = nullptr; - - log(L"Received request with ID: " + key); - if (key.compare(L"setPath") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - { - wstring unitName = unit->getUnitName(); - json::value path = value[L"path"]; - list newPath; - for (auto const& e : path.as_object()) - { - wstring WP = e.first; - double lat = path[WP][L"lat"].as_double(); - double lng = path[WP][L"lng"].as_double(); - log(unitName + L" set path destination " + WP + L" (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); - Coords dest; dest.lat = lat; dest.lng = lng; - newPath.push_back(dest); - } - - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - { - unit->setPath(newPath); - unit->setState(State::REACH_DESTINATION); - log(unitName + L" new path set successfully"); - } - else - log(unitName + L" not found, request will be discarded"); - } - } - else if (key.compare(L"smoke") == 0) - { - wstring color = value[L"color"].as_string(); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - log(L"Adding " + color + L" smoke at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); - Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new Smoke(color, loc)); - } - else if (key.compare(L"spawnGround") == 0) - { - wstring coalition = value[L"coalition"].as_string(); - wstring type = value[L"type"].as_string(); - double lat = value[L"location"][L"lat"].as_double(); - 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 SpawnGroundUnit(coalition, type, loc)); - } - else if (key.compare(L"spawnAir") == 0) - { - wstring coalition = value[L"coalition"].as_string(); - wstring type = value[L"type"].as_string(); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; - wstring payloadName = value[L"payloadName"].as_string(); - 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 SpawnAircraft(coalition, type, loc, payloadName, airbaseName)); - } - else if (key.compare(L"attackUnit") == 0) - { - int ID = value[L"ID"].as_integer(); - int targetID = value[L"targetID"].as_integer(); - - Unit* unit = unitsFactory->getUnit(ID); - Unit* target = unitsFactory->getUnit(targetID); - - wstring unitName; - wstring targetName; - - if (unit != nullptr) - unitName = unit->getUnitName(); - else - return; - - if (target != nullptr) - targetName = target->getUnitName(); - else - return; - - log(L"Unit " + unitName + L" attacking unit " + targetName); - unit->setTarget(targetID); - unit->setState(State::ATTACK); - } - else if (key.compare(L"stopAttack") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - unit->setState(State::REACH_DESTINATION); - else - return; - } - else if (key.compare(L"changeSpeed") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - unit->changeSpeed(value[L"change"].as_string()); - } - else if (key.compare(L"changeAltitude") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - unit->changeAltitude(value[L"change"].as_string()); - } - else if (key.compare(L"setSpeed") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - unit->setTargetSpeed(value[L"speed"].as_double()); - } - else if (key.compare(L"setAltitude") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - if (unit != nullptr) - unit->setTargetAltitude(value[L"altitude"].as_double()); - } - else if (key.compare(L"cloneUnit") == 0) - { - int ID = value[L"ID"].as_integer(); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; - command = dynamic_cast(new Clone(ID, loc)); - 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); - bool isLeader = value[L"isLeader"].as_bool(); - if (isLeader) - { - 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) - wingmen.push_back(wingman); - } - unit->setFormation(L"Line abreast"); - unit->setIsLeader(true); - unit->setWingmen(wingmen); - log(L"Setting " + unit->getName() + L" as formation leader"); - } - } - else { - unit->setIsLeader(false); - } - } - 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 if (key.compare(L"setROE") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - wstring ROE = value[L"ROE"].as_string(); - unit->setROE(ROE); - } - else if (key.compare(L"setReactionToThreat") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - wstring reactionToThreat = value[L"reactionToThreat"].as_string(); - unit->setReactionToThreat(reactionToThreat); - } - else if (key.compare(L"landAt") == 0) - { - int ID = value[L"ID"].as_integer(); - Unit* unit = unitsFactory->getUnit(ID); - double lat = value[L"location"][L"lat"].as_double(); - double lng = value[L"location"][L"lng"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; - unit->landAt(loc); - } - else if (key.compare(L"deleteUnit") == 0) - { - int ID = value[L"ID"].as_integer(); - unitsFactory->deleteUnit(ID); - } - else - { - log(L"Unknown command: " + key); - } - - if (command != nullptr) - { - appendCommand(command); - } -} - diff --git a/src/core/src/Unit.cpp b/src/core/src/Unit.cpp deleted file mode 100644 index 8f8f502d..00000000 --- a/src/core/src/Unit.cpp +++ /dev/null @@ -1,270 +0,0 @@ -#include "unit.h" -#include "utils.h" -#include "logger.h" -#include "commands.h" -#include "scheduler.h" -#include "defines.h" -#include "unitsFactory.h" - -#include -using namespace GeographicLib; - -extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; - -Unit::Unit(json::value json, int ID) : - ID(ID) -{ - log("Creating unit with ID: " + to_string(ID)); -} - -Unit::~Unit() -{ - -} - -void Unit::updateExportData(json::value json) -{ - /* Lock for thread safety */ - lock_guard guard(mutexLock); - - /* Compute speed (loGetWorldObjects does not provide speed, we compute it for better performance instead of relying on many lua calls) */ - if (oldPosition != NULL) - { - double dist = 0; - Geodesic::WGS84().Inverse(latitude, longitude, oldPosition.lat, oldPosition.lng, dist); - speed = speed * 0.95 + (dist / UPDATE_TIME_INTERVAL) * 0.05; - } - oldPosition = Coords(latitude, longitude, altitude); - - /* Update all the internal fields from the input json file */ - if (json.has_string_field(L"Name")) - name = json[L"Name"].as_string(); - if (json.has_string_field(L"UnitName")) - unitName = json[L"UnitName"].as_string(); - if (json.has_string_field(L"GroupName")) - groupName = json[L"GroupName"].as_string(); - if (json.has_object_field(L"Type")) - type = json[L"Type"]; - if (json.has_number_field(L"Country")) - country = json[L"Country"].as_number().to_int32(); - if (json.has_number_field(L"CoalitionID")) - coalitionID = json[L"CoalitionID"].as_number().to_int32(); - if (json.has_object_field(L"LatLongAlt")) - { - latitude = json[L"LatLongAlt"][L"Lat"].as_number().to_double(); - longitude = json[L"LatLongAlt"][L"Long"].as_number().to_double(); - altitude = json[L"LatLongAlt"][L"Alt"].as_number().to_double(); - } - if (json.has_number_field(L"Heading")) - heading = json[L"Heading"].as_number().to_double(); - if (json.has_object_field(L"Flags")) - flags = json[L"Flags"]; - - /* All units which contain the name "Olympus" are automatically under AI control */ - /* TODO: I don't really like using this method */ - if (unitName.find(L"Olympus") != wstring::npos) - { - AI = true; - } - - /* If the unit is alive and it is not a human, run the AI Loop that performs the requested commands and instructions (moving, attacking, etc) */ - if (AI && alive && flags[L"Human"].as_bool() == false) - { - AIloop(); - } -} - -void Unit::updateMissionData(json::value json) -{ - /* Lock for thread safety */ - lock_guard guard(mutexLock); - - if (json.has_number_field(L"fuel")) - fuel = json[L"fuel"].as_number().to_int32(); - if (json.has_object_field(L"ammo")) - ammo = json[L"ammo"]; - if (json.has_object_field(L"targets")) - targets = json[L"targets"]; - if (json.has_boolean_field(L"hasTask")) - hasTask = json[L"hasTask"].as_bool(); -} - -json::value Unit::json() -{ - /* Lock for thread safety */ - lock_guard guard(mutexLock); - - auto json = json::value::object(); - - json[L"alive"] = alive; - json[L"name"] = json::value::string(name); - json[L"unitName"] = json::value::string(unitName); - json[L"groupName"] = json::value::string(groupName); - json[L"type"] = type; - json[L"country"] = country; - json[L"coalitionID"] = coalitionID; - json[L"latitude"] = latitude; - json[L"longitude"] = longitude; - json[L"altitude"] = altitude; - json[L"speed"] = speed; - json[L"heading"] = heading; - json[L"flags"] = flags; - json[L"category"] = json::value::string(getCategory()); - json[L"currentTask"] = json::value::string(getCurrentTask()); - json[L"isLeader"] = isLeader; - json[L"isWingman"] = isWingman; - json[L"formation"] = json::value::string(formation); - json[L"fuel"] = fuel; - json[L"ammo"] = ammo; - json[L"targets"] = targets; - json[L"targetSpeed"] = getTargetSpeed(); - json[L"targetAltitude"] = getTargetAltitude(); - json[L"hasTask"] = hasTask; - json[L"ROE"] = json::value::string(ROE); - json[L"reactionToThreat"] = json::value::string(reactionToThreat); - - int i = 0; - for (auto itr = wingmen.begin(); itr != wingmen.end(); itr++) - json[L"wingmenIDs"][i++] = (*itr)->getID(); - - if (leader != nullptr) - json[L"leaderID"] = leader->getID(); - - /* Send the active path as a json object */ - if (activePath.size() > 0) { - auto path = json::value::object(); - int count = 1; - for (auto& destination : activePath) - { - auto json = json::value::object(); - json[L"lat"] = destination.lat; - json[L"lng"] = destination.lng; - json[L"alt"] = destination.alt; - path[to_wstring(count++)] = json; - } - json[L"activePath"] = path; - } - - return json; -} - -void Unit::setPath(list path) -{ - if (state != State::WINGMAN && state != State::FOLLOW) - { - activePath = path; - resetActiveDestination(); - } -} - -void Unit::setTarget(int newTargetID) -{ - targetID = newTargetID; -} - -wstring Unit::getTarget() -{ - if (isTargetAlive()) - { - Unit* target = unitsFactory->getUnit(targetID); - if (target != nullptr) - return target->getUnitName(); - } - return L""; -} - -bool Unit::isTargetAlive() -{ - if (targetID == NULL) - return false; - - Unit* target = unitsFactory->getUnit(targetID); - if (target != nullptr) - return target->alive; - else - return false; -} - -/* 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() -{ - activeDestination = Coords(NULL); -} - -void Unit::resetTask() -{ - Command* command = dynamic_cast(new ResetTask(ID)); - scheduler->appendCommand(command); -} - -void Unit::setIsLeader(bool newIsLeader) { - isLeader = newIsLeader; - if (!isLeader) { - for (auto wingman : wingmen) - { - wingman->setFormation(L""); - wingman->setIsWingman(false); - wingman->setLeader(nullptr); - } - } -} - -void Unit::setIsWingman(bool newIsWingman) -{ - isWingman = newIsWingman; - if (isWingman) - setState(State::WINGMAN); - else - setState(State::IDLE); -} - -void Unit::setFormationOffset(Offset newFormationOffset) -{ - formationOffset = newFormationOffset; - resetTask(); -} - -void Unit::setROE(wstring newROE) { - ROE = newROE; - int ROEEnum; - if (newROE.compare(L"Free") == 0) - ROEEnum = ROE::WEAPON_FREE; - else if (newROE.compare(L"Designated free") == 0) - ROEEnum = ROE::OPEN_FIRE_WEAPON_FREE; - else if (newROE.compare(L"Designated") == 0) - ROEEnum = ROE::OPEN_FIRE; - else if (newROE.compare(L"Return") == 0) - ROEEnum = ROE::RETURN_FIRE; - else if (newROE.compare(L"Hold") == 0) - ROEEnum = ROE::WEAPON_HOLD; - else - return; - Command* command = dynamic_cast(new SetOption(ID, SetCommandType::ROE, ROEEnum)); - scheduler->appendCommand(command); -} - -void Unit::setReactionToThreat(wstring newReactionToThreat) { - reactionToThreat = newReactionToThreat; - int reactionToThreatEnum; - if (newReactionToThreat.compare(L"None") == 0) - reactionToThreatEnum = ReactionToThreat::NO_REACTION; - else if (newReactionToThreat.compare(L"Passive") == 0) - reactionToThreatEnum = ReactionToThreat::PASSIVE_DEFENCE; - else if (newReactionToThreat.compare(L"Evade") == 0) - reactionToThreatEnum = ReactionToThreat::EVADE_FIRE; - else if (newReactionToThreat.compare(L"Escape") == 0) - reactionToThreatEnum = ReactionToThreat::BYPASS_AND_ESCAPE; - else if (newReactionToThreat.compare(L"Abort") == 0) - reactionToThreatEnum = ReactionToThreat::ALLOW_ABORT_MISSION; - else - return; - Command* command = dynamic_cast(new SetOption(ID, SetCommandType::REACTION_ON_THREAT, reactionToThreatEnum)); - scheduler->appendCommand(command); -} - -void Unit::landAt(Coords loc) { - activePath.clear(); - activePath.push_back(loc); - setState(State::LAND); -} \ No newline at end of file diff --git a/src/core/src/aircraft.cpp b/src/core/src/aircraft.cpp index b50d8abf..1284f903 100644 --- a/src/core/src/aircraft.cpp +++ b/src/core/src/aircraft.cpp @@ -4,65 +4,66 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Aircraft */ Aircraft::Aircraft(json::value json, int ID) : AirUnit(json, ID) { log("New Aircraft created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); + + double desiredSpeed = knotsToMs(300); + double desiredAltitude = ftToM(20000); + setDesiredSpeed(desiredSpeed); + setDesiredAltitude(desiredAltitude); }; void Aircraft::changeSpeed(wstring change) { if (change.compare(L"stop") == 0) - { setState(State::IDLE); - } else if (change.compare(L"slow") == 0) - targetSpeed -= 25 / 1.94384; + setDesiredSpeed(getDesiredSpeed() - knotsToMs(25)); else if (change.compare(L"fast") == 0) - targetSpeed += 25 / 1.94384; + setDesiredSpeed(getDesiredSpeed() + knotsToMs(25)); - if (targetSpeed < 50 / 1.94384) - targetSpeed = 50 / 1.94384; + if (getDesiredSpeed() < knotsToMs(50)) + setDesiredSpeed(knotsToMs(50)); - goToDestination(); /* Send the command to reach the destination */ + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ } void Aircraft::changeAltitude(wstring change) { if (change.compare(L"descend") == 0) { - if (targetAltitude > 5000) - targetAltitude -= 2500 / 3.28084; - else if (targetAltitude > 0) - targetAltitude -= 500 / 3.28084; + if (getDesiredAltitude() > 5000) + setDesiredAltitude(getDesiredAltitude() - ftToM(2500)); + else if (getDesiredAltitude() > 0) + setDesiredAltitude(getDesiredAltitude() - ftToM(500)); } else if (change.compare(L"climb") == 0) { - if (targetAltitude > 5000) - targetAltitude += 2500 / 3.28084; - else if (targetAltitude >= 0) - targetAltitude += 500 / 3.28084; + if (getDesiredAltitude() > 5000) + setDesiredAltitude(getDesiredAltitude() + ftToM(2500)); + else if (getDesiredAltitude() >= 0) + setDesiredAltitude(getDesiredAltitude() + ftToM(500)); } - if (targetAltitude < 0) - targetAltitude = 0; - goToDestination(); /* Send the command to reach the destination */ -} + if (getDesiredAltitude() < 0) + setDesiredAltitude(0); -void Aircraft::setTargetSpeed(double newTargetSpeed) { - targetSpeed = newTargetSpeed; - goToDestination(); -} - -void Aircraft::setTargetAltitude(double newTargetAltitude) { - targetAltitude = newTargetAltitude; - goToDestination(); + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ } \ No newline at end of file diff --git a/src/core/src/airunit.cpp b/src/core/src/airunit.cpp index cb7ac8db..06c0808a 100644 --- a/src/core/src/airunit.cpp +++ b/src/core/src/airunit.cpp @@ -4,24 +4,24 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Air unit */ AirUnit::AirUnit(json::value json, int ID) : Unit(json, ID) { - + }; void AirUnit::setState(int newState) { - if (state != newState) - { + /************ Perform any action required when LEAVING a state ************/ + if (newState != state) { switch (state) { case State::IDLE: { break; @@ -30,175 +30,98 @@ void AirUnit::setState(int newState) break; } case State::ATTACK: { - setTarget(NULL); + setTargetID(NULL); break; } case State::FOLLOW: { - break; - } - case State::WINGMAN: { - if (isWingman) - return; + setLeaderID(NULL); break; } case State::LAND: { break; } - default: + case State::REFUEL: { break; } - - switch (newState) { - case State::IDLE: { - resetActiveDestination(); - break; - } - case State::REACH_DESTINATION: { - resetActiveDestination(); - break; - } - case State::ATTACK: { - if (isTargetAlive()) { - Unit* target = unitsFactory->getUnit(targetID); - Coords targetPosition = Coords(target->getLatitude(), target->getLongitude(), 0); - activePath.clear(); - activePath.push_front(targetPosition); - resetActiveDestination(); - } - break; - } - case State::FOLLOW: { - resetActiveDestination(); - break; - } - case State::WINGMAN: { - resetActiveDestination(); - break; - } - case State::LAND: { - resetActiveDestination(); + case State::BOMB_POINT: + case State::CARPET_BOMB: + case State::BOMB_BUILDING: { + setTargetLocation(Coords(NULL)); break; } default: break; } - - resetTask(); - - log(unitName + L" setting state from " + to_wstring(state) + L" to " + to_wstring(newState)); - state = newState; } -} -bool AirUnit::isDestinationReached() -{ - if (activeDestination != NULL) - { - double dist = 0; - Geodesic::WGS84().Inverse(latitude, longitude, activeDestination.lat, activeDestination.lng, dist); - if (dist < AIR_DEST_DIST_THR) - { - log(unitName + L" destination reached"); - return true; - } - else { - return false; + /************ Perform any action required when ENTERING a state ************/ + switch (newState) { + case State::IDLE: { + clearActivePath(); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Idle")); + break; + } + case State::REACH_DESTINATION: { + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Reach destination")); + break; + } + case State::ATTACK: { + if (isTargetAlive()) { + Unit* target = unitsManager->getUnit(targetID); + Coords targetPosition = Coords(target->getLatitude(), target->getLongitude(), 0); + clearActivePath(); + pushActivePathFront(targetPosition); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Attack")); } + break; + } + case State::FOLLOW: { + clearActivePath(); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Follow")); + break; + } + case State::LAND: { + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Land")); + break; + } + case State::REFUEL: { + initialFuel = fuel; + clearActivePath(); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Refuel")); + break; + } + case State::BOMB_POINT: { + addMeasure(L"currentState", json::value(L"Bombing point")); + clearActivePath(); + resetActiveDestination(); + break; + } + case State::CARPET_BOMB: { + addMeasure(L"currentState", json::value(L"Carpet bombing")); + clearActivePath(); + resetActiveDestination(); + break; + } + case State::BOMB_BUILDING: { + addMeasure(L"currentState", json::value(L"Bombing building")); + clearActivePath(); + resetActiveDestination(); + break; + } + default: + break; } - else - return true; -} -bool AirUnit::setActiveDestination() -{ - if (activePath.size() > 0) - { - activeDestination = activePath.front(); - log(unitName + L" active destination set to queue front"); - return true; - } - else - { - activeDestination = Coords(0); - log(unitName + L" active destination set to NULL"); - return false; - } -} + resetTask(); -void AirUnit::createHoldingPattern() -{ - /* Air units must ALWAYS have a destination or they will RTB and become uncontrollable */ - activePath.clear(); - Coords point1; - Coords point2; - Coords point3; - Geodesic::WGS84().Direct(latitude, longitude, 45, 10000, point1.lat, point1.lng); - Geodesic::WGS84().Direct(point1.lat, point1.lng, 135, 10000, point2.lat, point2.lng); - Geodesic::WGS84().Direct(point2.lat, point2.lng, 225, 10000, point3.lat, point3.lng); - activePath.push_back(point1); - activePath.push_back(point2); - activePath.push_back(point3); - activePath.push_back(Coords(latitude, longitude)); - log(unitName + L" holding pattern created"); -} - -bool AirUnit::updateActivePath(bool looping) -{ - if (activePath.size() > 0) - { - /* Push the next destination in the queue to the front */ - if (looping) - activePath.push_back(activePath.front()); - activePath.pop_front(); - log(unitName + L" active path front popped"); - return true; - } - else { - return false; - } -} - -void AirUnit::goToDestination(wstring enrouteTask) -{ - if (activeDestination != NULL) - { - Command* command = dynamic_cast(new Move(ID, activeDestination, getTargetSpeed(), getTargetAltitude(), getCategory(), enrouteTask)); - scheduler->appendCommand(command); - hasTask = true; - } - else - log(unitName + L" error, no active destination!"); -} - -void AirUnit::taskWingmen() -{ - switch (state) { - case State::IDLE: - case State::REACH_DESTINATION: - case State::ATTACK:{ - int idx = 1; - for (auto const& wingman : wingmen) - { - if (!wingman->getIsWingman()) - { - wingman->setIsWingman(true); - wingman->setLeader(this); - } - - if (wingman->getFormation().compare(formation) != 0) - { - wingman->resetTask(); - wingman->setFormation(formation); - if (formation.compare(L"Line abreast") == 0) - wingman->setFormationOffset(Offset(0 * idx, 0 * idx, 1852 * idx)); - idx++; - } - } - break; - } - default: - break; - } + log(unitName + L" setting state from " + to_wstring(state) + L" to " + to_wstring(newState)); + state = newState; } void AirUnit::AIloop() @@ -206,52 +129,65 @@ void AirUnit::AIloop() /* State machine */ switch (state) { case State::IDLE: { - wstring enrouteTask = L"nil"; currentTask = L"Idle"; - if (activeDestination == NULL || !hasTask) + if (!getHasTask()) { - createHoldingPattern(); - setActiveDestination(); - goToDestination(enrouteTask); + std::wostringstream taskSS; + if (isTanker) { + taskSS << "{ [1] = { id = 'Tanker' }, [2] = { id = 'Orbit', pattern = 'Race-Track', altitude = " << desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << desiredAltitudeType << "' } }"; + } + else if (isAWACS) { + taskSS << "{ [1] = { id = 'AWACS' }, [2] = { id = 'Orbit', pattern = 'Circle', altitude = " << desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << desiredAltitudeType << "' } }"; + } + else { + taskSS << "{ id = 'Orbit', pattern = 'Circle', altitude = " << desiredAltitude << ", speed = " << desiredSpeed << ", altitudeType = '" << desiredAltitudeType << "' }"; + } + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); } - else { - if (isDestinationReached() && updateActivePath(true) && setActiveDestination()) - goToDestination(enrouteTask); - } - - if (isLeader) - taskWingmen(); break; } case State::REACH_DESTINATION: { - wstring enrouteTask = L"nil"; - currentTask = L"Reaching destination"; - - if (activeDestination == NULL || !hasTask) + wstring enrouteTask = L""; + bool looping = false; + + if (isTanker) { - setActiveDestination(); - goToDestination(enrouteTask); + enrouteTask = L"{ id = 'Tanker' }"; + currentTask = L"Tanker"; + } + else if (isAWACS) + { + enrouteTask = L"{ id = 'AWACS' }"; + currentTask = L"AWACS"; + } + else + { + enrouteTask = L"nil"; + currentTask = L"Reaching destination"; + } + + if (activeDestination == NULL || !getHasTask()) + { + if (!setActiveDestination()) + setState(State::IDLE); + else + goToDestination(enrouteTask); } else { - if (isDestinationReached()) { - if (updateActivePath(false) && setActiveDestination()) + if (isDestinationReached(AIR_DEST_DIST_THR)) { + if (updateActivePath(looping) && setActiveDestination()) goToDestination(enrouteTask); - else { + else setState(State::IDLE); - break; - } } - } - - if (isLeader) - taskWingmen(); - + } break; } - case State::LAND: { - wstring enrouteTask = L"{" "id = 'land' }"; + wstring enrouteTask = L"{ id = 'Land' }"; currentTask = L"Landing"; if (activeDestination == NULL) @@ -259,10 +195,6 @@ void AirUnit::AIloop() setActiveDestination(); goToDestination(enrouteTask); } - - if (isLeader) - taskWingmen(); - break; } case State::ATTACK: { @@ -280,45 +212,30 @@ void AirUnit::AIloop() << "targetID = " << targetID << "," << "}"; wstring enrouteTask = enrouteTaskSS.str(); - currentTask = L"Attacking " + getTarget(); + currentTask = L"Attacking " + getTargetName(); - if (activeDestination == NULL || !hasTask) + if (!getHasTask()) { setActiveDestination(); goToDestination(enrouteTask); } - else { - if (isDestinationReached()) { - if (updateActivePath(false) && setActiveDestination()) - goToDestination(enrouteTask); - else { - setState(State::IDLE); - break; - } - } - } - - if (isLeader) - taskWingmen(); break; } case State::FOLLOW: { - /* TODO */ - setState(State::IDLE); - break; - } - case State::WINGMAN: { - /* In the WINGMAN state, the unit relinquishes control to the leader */ - activePath.clear(); + clearActivePath(); activeDestination = Coords(NULL); - if (leader == nullptr || !leader->getAlive()) - { - this->setFormation(L""); - this->setIsWingman(false); + + /* If the leader is not alive (either not set or was destroyed) go back to IDLE */ + if (!isLeaderAlive()) { + setState(State::IDLE); break; } - if (!hasTask) { + + currentTask = L"Following " + getTargetName(); + + Unit* leader = unitsManager->getUnit(leaderID); + if (!getHasTask()) { if (leader != nullptr && leader->getAlive() && formationOffset != NULL) { std::wostringstream taskSS; @@ -331,14 +248,69 @@ void AirUnit::AIloop() << "z = " << formationOffset.z << "}," << "}"; - Command* command = dynamic_cast(new SetTask(ID, taskSS.str())); + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); - hasTask = true; + setHasTask(true); } } break; } + case State::REFUEL: { + currentTask = L"Refueling"; + + if (!getHasTask()) { + if (fuel <= initialFuel) { + std::wostringstream taskSS; + taskSS << "{" + << "id = 'Refuel'" + << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + else { + setState(State::IDLE); + } + } + } + case State::BOMB_POINT: { + currentTask = L"Bombing point"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'Bombing', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + } + case State::CARPET_BOMB: { + currentTask = L"Carpet bombing"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'CarpetBombing', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + break; + } + case State::BOMB_BUILDING: { + currentTask = L"Bombing building"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'AttackMapObject', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + break; + } default: break; } -} + + addMeasure(L"currentTask", json::value(currentTask)); +} \ No newline at end of file diff --git a/src/core/src/Commands.cpp b/src/core/src/commands.cpp similarity index 59% rename from src/core/src/Commands.cpp rename to src/core/src/commands.cpp index ef5a7535..87f33218 100644 --- a/src/core/src/Commands.cpp +++ b/src/core/src/commands.cpp @@ -1,19 +1,26 @@ #include "commands.h" #include "logger.h" #include "dcstools.h" +#include "unit.h" +#include "unitsmanager.h" + +extern UnitsManager* unitsManager; /* Move command */ wstring Move::getString(lua_State* L) { + std::wostringstream commandSS; commandSS.precision(10); - commandSS << "Olympus.move, " - << ID << ", " - << destination.lat << ", " - << destination.lng << ", " - << altitude << ", " - << speed << ", " - << "\"" << unitCategory << "\"" << ", " + commandSS << "Olympus.move, " + << "\"" << groupName << "\"" << ", " + << destination.lat << ", " + << destination.lng << ", " + << altitude << ", " + << "\"" << altitudeType << "\"" << ", " + << speed << ", " + << "\"" << speedType << "\"" << ", " + << "\"" << category << "\"" << ", " << taskOptions; return commandSS.str(); } @@ -50,7 +57,7 @@ wstring SpawnAircraft::getString(lua_State* L) optionsSS.precision(10); optionsSS << "{" << "payloadName = \"" << payloadName << "\", " - << "airbaseName = \"" << airbaseName << "\"," + << "airbaseName = \"" << airbaseName << "\", " << "}"; std::wostringstream commandSS; @@ -59,7 +66,8 @@ wstring SpawnAircraft::getString(lua_State* L) << "\"" << coalition << "\"" << ", " << "\"" << unitType << "\"" << ", " << location.lat << ", " - << location.lng << "," + << location.lng << ", " + << location.alt << ", " << optionsSS.str(); return commandSS.str(); } @@ -67,13 +75,22 @@ wstring SpawnAircraft::getString(lua_State* L) /* Clone unit command */ wstring Clone::getString(lua_State* L) { - std::wostringstream commandSS; - commandSS.precision(10); - commandSS << "Olympus.clone, " - << ID << ", " - << location.lat << ", " - << location.lng; - return commandSS.str(); + Unit* unit = unitsManager->getUnit(ID); + if (unit != nullptr) + { + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.clone, " + << ID << ", " + << location.lat << ", " + << location.lng << ", " + << "\"" << unit->getCategory() << "\""; + return commandSS.str(); + } + else + { + return L""; + } } /* Delete unit command */ @@ -82,7 +99,8 @@ wstring Delete::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.delete, " - << ID; + << ID << ", " + << (explosion ? "true" : "false"); return commandSS.str(); } @@ -92,7 +110,7 @@ wstring SetTask::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.setTask, " - << ID << "," + << "\"" << groupName << "\"" << ", " << task; return commandSS.str(); @@ -104,7 +122,7 @@ wstring ResetTask::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.resetTask, " - << ID; + << "\"" << groupName << "\""; return commandSS.str(); } @@ -115,7 +133,7 @@ wstring SetCommand::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.setCommand, " - << ID << "," + << "\"" << groupName << "\"" << ", " << command; return commandSS.str(); @@ -126,10 +144,42 @@ wstring SetOption::getString(lua_State* L) { std::wostringstream commandSS; commandSS.precision(10); - commandSS << "Olympus.setOption, " - << ID << "," - << optionID << "," - << optionValue; + if (!isBoolean) { + commandSS << "Olympus.setOption, " + << "\"" << groupName << "\"" << ", " + << optionID << ", " + << optionValue; + } else { + commandSS << "Olympus.setOption, " + << "\"" << groupName << "\"" << ", " + << optionID << ", " + << (optionBool? "true": "false"); + } + return commandSS.str(); +} + +/* Set onOff command */ +wstring SetOnOff::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + + commandSS << "Olympus.setOnOff, " + << "\"" << groupName << "\"" << ", " + << (onOff ? "true" : "false"); + + return commandSS.str(); +} + +/* Explosion command */ +wstring Explosion::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.explosion, " + << intensity << ", " + << location.lat << ", " + << location.lng; return commandSS.str(); } \ No newline at end of file diff --git a/src/core/src/core.cpp b/src/core/src/core.cpp index 24849509..38e490f2 100644 --- a/src/core/src/core.cpp +++ b/src/core/src/core.cpp @@ -1,26 +1,34 @@ #include "dcstools.h" #include "logger.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include "server.h" #include "scheduler.h" #include "scriptLoader.h" #include "luatools.h" auto before = std::chrono::system_clock::now(); -UnitsFactory* unitsFactory = nullptr; +UnitsManager* unitsManager = nullptr; Server* server = nullptr; Scheduler* scheduler = nullptr; -json::value airbasesData; -json::value bullseyeData; - +json::value airbases; +json::value bullseyes; +json::value mission; +mutex mutexLock; +bool initialized = false; +string sessionHash; /* Called when DCS simulation stops. All singleton instances are deleted. */ extern "C" DllExport int coreDeinit(lua_State* L) { + if (!initialized) + return (0); + log("Olympus coreDeinit called successfully"); - delete unitsFactory; + server->stop(L); + + delete unitsManager; delete server; delete scheduler; @@ -32,49 +40,66 @@ extern "C" DllExport int coreDeinit(lua_State* L) /* Called when DCS simulation starts. All singletons are instantiated, and the custom Lua functions are registered in the Lua state. */ extern "C" DllExport int coreInit(lua_State* L) { - unitsFactory = new UnitsFactory(L); + sessionHash = random_string(16); + unitsManager = new UnitsManager(L); server = new Server(L); scheduler = new Scheduler(L); registerLuaFunctions(L); + server->start(L); + + initialized = true; return(0); } extern "C" DllExport int coreFrame(lua_State* L) { + if (!initialized) + return (0); + + /* Lock for thread safety */ + lock_guard guard(mutexLock); + const std::chrono::duration duration = std::chrono::system_clock::now() - before; - // TODO make intervals editable + /* TODO make intervals editable */ if (duration.count() > UPDATE_TIME_INTERVAL) { - if (unitsFactory != nullptr) + if (unitsManager != nullptr) { - unitsFactory->updateExportData(L); - } - - // TODO allow for different intervals - if (scheduler != nullptr) - { - scheduler->execute(L); + unitsManager->updateExportData(L); + unitsManager->runAILoop(); } before = std::chrono::system_clock::now(); } + + if (scheduler != nullptr) + scheduler->execute(L); + return(0); } extern "C" DllExport int coreMissionData(lua_State * L) { + if (!initialized) + return (0); + + /* Lock for thread safety */ + lock_guard guard(mutexLock); + lua_getglobal(L, "Olympus"); lua_getfield(L, -1, "missionData"); json::value missionData = luaTableToJSON(L, -1); if (missionData.has_object_field(L"unitsData")) - unitsFactory->updateMissionData(missionData[L"unitsData"]); + unitsManager->updateMissionData(missionData[L"unitsData"]); if (missionData.has_object_field(L"airbases")) - airbasesData = missionData[L"airbases"]; - if (missionData.has_object_field(L"bullseye")) - bullseyeData = missionData[L"bullseye"]; + airbases = missionData[L"airbases"]; + if (missionData.has_object_field(L"bullseyes")) + bullseyes = missionData[L"bullseyes"]; + if (missionData.has_object_field(L"mission")) + mission = missionData[L"mission"]; return(0); } diff --git a/src/core/src/groundunit.cpp b/src/core/src/groundunit.cpp index 463843d0..58b8fd50 100644 --- a/src/core/src/groundunit.cpp +++ b/src/core/src/groundunit.cpp @@ -4,68 +4,147 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Ground unit */ GroundUnit::GroundUnit(json::value json, int ID) : Unit(json, ID) { log("New Ground Unit created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); + + double desiredSpeed = 10; + setDesiredSpeed(desiredSpeed); }; +void GroundUnit::setState(int newState) +{ + /************ Perform any action required when LEAVING a state ************/ + if (newState != state) { + switch (state) { + case State::IDLE: { + break; + } + case State::REACH_DESTINATION: { + break; + } + case State::FIRE_AT_AREA: { + setTargetLocation(Coords(NULL)); + break; + } + default: + break; + } + } + + /************ Perform any action required when ENTERING a state ************/ + switch (newState) { + case State::IDLE: { + clearActivePath(); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Idle")); + break; + } + case State::REACH_DESTINATION: { + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Reach destination")); + break; + } + case State::FIRE_AT_AREA: { + addMeasure(L"currentState", json::value(L"Firing at area")); + clearActivePath(); + resetActiveDestination(); + break; + } + default: + break; + } + + resetTask(); + + log(unitName + L" setting state from " + to_wstring(state) + L" to " + to_wstring(newState)); + state = newState; +} + void GroundUnit::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()) + switch (state) { + case State::IDLE: { + currentTask = L"Idle"; + if (getHasTask()) + resetTask(); + break; + } + case State::REACH_DESTINATION: { + wstring enrouteTask = L""; + bool looping = false; + + std::wostringstream taskSS; + taskSS << "{ id = 'FollowRoads', value = " << (getFollowRoads() ? "true" : "false") << " }"; + enrouteTask = taskSS.str(); + + if (activeDestination == NULL || !getHasTask()) { - activeDestination = activePath.front(); - Command* command = dynamic_cast(new Move(ID, activeDestination, getTargetSpeed(), getTargetAltitude(), getCategory(), L"nil")); + if (!setActiveDestination()) + setState(State::IDLE); + else + goToDestination(enrouteTask); + } + else { + if (isDestinationReached(GROUND_DEST_DIST_THR)) { + if (updateActivePath(looping) && setActiveDestination()) + goToDestination(enrouteTask); + else + setState(State::IDLE); + } + } + break; + } + case State::FIRE_AT_AREA: { + currentTask = L"Firing at area"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'FireAtPoint', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << ", radius = 1000}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); + setHasTask(true); } } - else - { - if (activeDestination != NULL) - { - log(unitName + L" no more points in active path"); - activeDestination = Coords(0); // Set the active path to NULL - currentTask = L"Idle"; - } + default: + break; } - /* Ground unit AI Loop */ - if (activeDestination != NULL) - { - double newDist = 0; - Geodesic::WGS84().Inverse(latitude, longitude, activeDestination.lat, activeDestination.lng, newDist); - if (newDist < GROUND_DEST_DIST_THR) - { - /* Destination reached */ - activePath.pop_front(); - log(unitName + L" destination reached"); - } - } + addMeasure(L"currentTask", json::value(currentTask)); } void GroundUnit::changeSpeed(wstring change) { if (change.compare(L"stop") == 0) - { - - } + setState(State::IDLE); else if (change.compare(L"slow") == 0) - { - - } + setDesiredSpeed(getDesiredSpeed() - knotsToMs(5)); else if (change.compare(L"fast") == 0) - { + setDesiredSpeed(getDesiredSpeed() + knotsToMs(5)); - } + if (getDesiredSpeed() < 0) + setDesiredSpeed(0); +} + +void GroundUnit::setOnOff(bool newOnOff) +{ + Unit::setOnOff(newOnOff); + Command* command = dynamic_cast(new SetOnOff(groupName, onOff)); + scheduler->appendCommand(command); +} + +void GroundUnit::setFollowRoads(bool newFollowRoads) +{ + Unit::setFollowRoads(newFollowRoads); + resetActiveDestination(); /* Reset active destination to apply option*/ } \ No newline at end of file diff --git a/src/core/src/helicopter.cpp b/src/core/src/helicopter.cpp index 7350e659..d728f4c5 100644 --- a/src/core/src/helicopter.cpp +++ b/src/core/src/helicopter.cpp @@ -4,18 +4,24 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Helicopter */ Helicopter::Helicopter(json::value json, int ID) : AirUnit(json, ID) { log("New Helicopter created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); + + double desiredSpeed = knotsToMs(100); + double desiredAltitude = ftToM(5000); + setDesiredSpeed(desiredSpeed); + setDesiredAltitude(desiredAltitude); }; void Helicopter::changeSpeed(wstring change) @@ -23,14 +29,14 @@ void Helicopter::changeSpeed(wstring change) if (change.compare(L"stop") == 0) { /* Air units can't hold a position, so we can only set them to hold. At the moment, this will erase any other command. TODO: helicopters should be able to hover in place */ - activePath.clear(); + clearActivePath(); } else if (change.compare(L"slow") == 0) - targetSpeed -= 10 / 1.94384; + desiredSpeed -= knotsToMs(10); else if (change.compare(L"fast") == 0) - targetSpeed += 10 / 1.94384; - if (targetSpeed < 0) - targetSpeed = 0; + desiredSpeed += knotsToMs(10); + if (desiredSpeed < 0) + desiredSpeed = 0; goToDestination(); /* Send the command to reach the destination */ } @@ -39,31 +45,20 @@ void Helicopter::changeAltitude(wstring change) { if (change.compare(L"descend") == 0) { - if (targetAltitude > 100) - targetAltitude -= 100 / 3.28084; - else if (targetAltitude > 0) - targetAltitude -= 10 / 3.28084; + if (desiredAltitude > 100) + desiredAltitude -= ftToM(100); + else if (desiredAltitude > 0) + desiredAltitude -= ftToM(10); } else if (change.compare(L"climb") == 0) { - if (targetAltitude > 100) - targetAltitude += 100 / 3.28084; - else if (targetAltitude >= 0) - targetAltitude += 10 / 3.28084; + if (desiredAltitude > 100) + desiredAltitude += ftToM(100); + else if (desiredAltitude >= 0) + desiredAltitude += ftToM(10); } - if (targetAltitude < 0) - targetAltitude = 0; + if (desiredAltitude < 0) + desiredAltitude = 0; goToDestination(); /* Send the command to reach the destination */ } - - -void Helicopter::setTargetSpeed(double newTargetSpeed) { - targetSpeed = newTargetSpeed; - goToDestination(); -} - -void Helicopter::setTargetAltitude(double newTargetAltitude) { - targetAltitude = newTargetAltitude; - goToDestination(); -} \ No newline at end of file diff --git a/src/core/src/measure.cpp b/src/core/src/measure.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/core/src/navyunit.cpp b/src/core/src/navyunit.cpp index e9491d43..dc96f7a1 100644 --- a/src/core/src/navyunit.cpp +++ b/src/core/src/navyunit.cpp @@ -4,18 +4,22 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Navy Unit */ NavyUnit::NavyUnit(json::value json, int ID) : Unit(json, ID) { log("New Navy Unit created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); + + double desiredSpeed = 10; + setDesiredSpeed(desiredSpeed); }; void NavyUnit::AIloop() diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp new file mode 100644 index 00000000..e7a7de4e --- /dev/null +++ b/src/core/src/scheduler.cpp @@ -0,0 +1,393 @@ +#include "scheduler.h" +#include "logger.h" +#include "dcstools.h" +#include "unitsManager.h" +#include "utils.h" +#include "unit.h" + +extern UnitsManager* unitsManager; + +Scheduler::Scheduler(lua_State* L): + load(0) +{ + LogInfo(L, "Scheduler constructor called successfully"); +} + +Scheduler::~Scheduler() +{ + +} + +void Scheduler::appendCommand(Command* command) +{ + commands.push_back(command); +} + +void Scheduler::execute(lua_State* L) +{ + /* Decrease the active computation load. New commands can be sent only if the load has reached 0. + This is needed to avoid server lag. */ + if (load > 0) { + load--; + return; + } + + int priority = CommandPriority::HIGH; + while (priority >= CommandPriority::LOW) + { + for (auto command : commands) + { + if (command->getPriority() == priority) + { + wstring commandString = L"Olympus.protectedCall(" + command->getString(L) + L")"; + if (dostring_in(L, "server", to_string(commandString))) + log(L"Error executing command " + commandString); + load = command->getLoad(); + commands.remove(command); + return; + } + } + priority--; + } +} + +void Scheduler::handleRequest(wstring key, json::value value) +{ + Command* command = nullptr; + + log(L"Received request with ID: " + key); + if (key.compare(L"setPath") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + { + wstring unitName = unit->getUnitName(); + json::value path = value[L"path"]; + list newPath; + for (int i = 1; i <= path.as_object().size(); i++) + { + wstring WP = to_wstring(i); + double lat = path[WP][L"lat"].as_double(); + double lng = path[WP][L"lng"].as_double(); + log(unitName + L" set path destination " + WP + L" (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); + Coords dest; dest.lat = lat; dest.lng = lng; + newPath.push_back(dest); + } + + unit->setActivePath(newPath); + unit->setState(State::REACH_DESTINATION); + log(unitName + L" new path set successfully"); + } + } + else if (key.compare(L"smoke") == 0) + { + wstring color = value[L"color"].as_string(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + log(L"Adding " + color + L" smoke at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); + Coords loc; loc.lat = lat; loc.lng = lng; + command = dynamic_cast(new Smoke(color, loc)); + } + else if (key.compare(L"spawnGround") == 0) + { + wstring coalition = value[L"coalition"].as_string(); + wstring type = value[L"type"].as_string(); + double lat = value[L"location"][L"lat"].as_double(); + 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 SpawnGroundUnit(coalition, type, loc)); + } + else if (key.compare(L"spawnAir") == 0) + { + wstring coalition = value[L"coalition"].as_string(); + wstring type = value[L"type"].as_string(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + double altitude = value[L"altitude"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; loc.alt = altitude; + wstring payloadName = value[L"payloadName"].as_string(); + 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 SpawnAircraft(coalition, type, loc, payloadName, airbaseName)); + } + else if (key.compare(L"attackUnit") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + int targetID = value[L"targetID"].as_integer(); + + Unit* unit = unitsManager->getGroupLeader(ID); + Unit* target = unitsManager->getUnit(targetID); + + wstring unitName; + wstring targetName; + + if (unit != nullptr) + unitName = unit->getUnitName(); + else + return; + + if (target != nullptr) + targetName = target->getUnitName(); + else + return; + + log(L"Unit " + unitName + L" attacking unit " + targetName); + unit->setTargetID(targetID); + unit->setState(State::ATTACK); + } + else if (key.compare(L"followUnit") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + int leaderID = value[L"targetID"].as_integer(); + int offsetX = value[L"offsetX"].as_integer(); + int offsetY = value[L"offsetY"].as_integer(); + int offsetZ = value[L"offsetZ"].as_integer(); + + Unit* unit = unitsManager->getGroupLeader(ID); + Unit* leader = unitsManager->getUnit(leaderID); + + wstring unitName; + wstring leaderName; + + if (unit != nullptr) + unitName = unit->getUnitName(); + else + return; + + if (leader != nullptr) + leaderName = leader->getUnitName(); + else + return; + + log(L"Unit " + unitName + L" following unit " + leaderName); + unit->setFormationOffset(Offset(offsetX, offsetY, offsetZ)); + unit->setLeaderID(leaderID); + unit->setState(State::FOLLOW); + } + else if (key.compare(L"changeSpeed") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->changeSpeed(value[L"change"].as_string()); + } + else if (key.compare(L"changeAltitude") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->changeAltitude(value[L"change"].as_string()); + } + else if (key.compare(L"setSpeed") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setDesiredSpeed(value[L"speed"].as_double()); + } + else if (key.compare(L"setSpeedType") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setDesiredSpeedType(value[L"speedType"].as_string()); + } + else if (key.compare(L"setAltitude") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setDesiredAltitude(value[L"altitude"].as_double()); + } + else if (key.compare(L"setAltitudeType") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setDesiredAltitudeType(value[L"altitudeType"].as_string()); + } + else if (key.compare(L"cloneUnit") == 0) + { + int ID = value[L"ID"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + command = dynamic_cast(new Clone(ID, loc)); + log(L"Cloning unit " + to_wstring(ID)); + } + else if (key.compare(L"setROE") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + wstring ROE = value[L"ROE"].as_string(); + unit->setROE(ROE); + } + else if (key.compare(L"setReactionToThreat") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + wstring reactionToThreat = value[L"reactionToThreat"].as_string(); + unit->setReactionToThreat(reactionToThreat); + } + else if (key.compare(L"setEmissionsCountermeasures") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + wstring emissionsCountermeasures = value[L"emissionsCountermeasures"].as_string(); + unit->setEmissionsCountermeasures(emissionsCountermeasures); + } + else if (key.compare(L"landAt") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + unit->landAt(loc); + } + else if (key.compare(L"deleteUnit") == 0) + { + int ID = value[L"ID"].as_integer(); + bool explosion = value[L"explosion"].as_bool(); + unitsManager->deleteUnit(ID, explosion); + } + else if (key.compare(L"refuel") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::REFUEL); + } + else if (key.compare(L"setAdvancedOptions") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + { + /* Advanced tasking */ + unit->setIsTanker(value[L"isTanker"].as_bool()); + unit->setIsAWACS(value[L"isAWACS"].as_bool()); + + /* TACAN Options */ + auto TACAN = value[L"TACAN"]; + unit->setTACAN({ TACAN[L"isOn"].as_bool(), + TACAN[L"channel"].as_number().to_int32(), + TACAN[L"XY"].as_string(), + TACAN[L"callsign"].as_string() + }); + + /* Radio Options */ + auto radio = value[L"radio"]; + unit->setRadio({ radio[L"frequency"].as_number().to_int32(), + radio[L"callsign"].as_number().to_int32(), + radio[L"callsignNumber"].as_number().to_int32() + }); + + /* General Settings */ + auto generalSettings = value[L"generalSettings"]; + unit->setGeneralSettings({ generalSettings[L"prohibitJettison"].as_bool(), + generalSettings[L"prohibitAA"].as_bool(), + generalSettings[L"prohibitAG"].as_bool(), + generalSettings[L"prohibitAfterburner"].as_bool(), + generalSettings[L"prohibitAirWpn"].as_bool(), + }); + + unit->resetActiveDestination(); + } + } + else if (key.compare(L"setFollowRoads") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + bool followRoads = value[L"followRoads"].as_bool(); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setFollowRoads(followRoads); + } + else if (key.compare(L"setOnOff") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + bool onOff = value[L"onOff"].as_bool(); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setOnOff(onOff); + } + else if (key.compare(L"explosion") == 0) + { + int intensity = value[L"intensity"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + log(L"Adding " + to_wstring(intensity) + L" explosion at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); + Coords loc; loc.lat = lat; loc.lng = lng; + command = dynamic_cast(new Explosion(intensity, loc)); + } + else if (key.compare(L"bombPoint") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::BOMB_POINT); + unit->setTargetLocation(loc); + } + else if (key.compare(L"carpetBomb") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::CARPET_BOMB); + unit->setTargetLocation(loc); + } + else if (key.compare(L"bombBuilding") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::BOMB_BUILDING); + unit->setTargetLocation(loc); + } + else if (key.compare(L"fireAtArea") == 0) + { + int ID = value[L"ID"].as_integer(); + unitsManager->acquireControl(ID); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::FIRE_AT_AREA); + unit->setTargetLocation(loc); + } + else + { + log(L"Unknown command: " + key); + } + + if (command != nullptr) + { + appendCommand(command); + } +} + diff --git a/src/core/src/scriptLoader.cpp b/src/core/src/scriptloader.cpp similarity index 91% rename from src/core/src/scriptLoader.cpp rename to src/core/src/scriptloader.cpp index 2cf5a134..0509463e 100644 --- a/src/core/src/scriptLoader.cpp +++ b/src/core/src/scriptloader.cpp @@ -47,7 +47,8 @@ void registerLuaFunctions(lua_State* L) return; } - executeLuaScript(L, modLocation + "\\Scripts\\mist_4_4_90.lua"); + executeLuaScript(L, modLocation + "\\Scripts\\mist.lua"); executeLuaScript(L, modLocation + "\\Scripts\\OlympusCommand.lua"); executeLuaScript(L, modLocation + "\\Scripts\\unitPayloads.lua"); -} \ No newline at end of file + executeLuaScript(L, modLocation + "\\Scripts\\templates.lua"); +} diff --git a/src/core/src/server.cpp b/src/core/src/server.cpp index f70ff8c5..a2b01978 100644 --- a/src/core/src/server.cpp +++ b/src/core/src/server.cpp @@ -1,16 +1,24 @@ #include "server.h" #include "logger.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsManager.h" #include "scheduler.h" #include "luatools.h" #include #include +#include "base64.hpp" -extern UnitsFactory* unitsFactory; +#include +using namespace std::chrono; +using namespace base64; + +extern UnitsManager* unitsManager; extern Scheduler* scheduler; -extern json::value airbasesData; -extern json::value bullseyeData; +extern json::value airbases; +extern json::value bullseyes; +extern json::value mission; +extern mutex mutexLock; +extern string sessionHash; void handle_eptr(std::exception_ptr eptr) { @@ -25,79 +33,139 @@ void handle_eptr(std::exception_ptr eptr) } Server::Server(lua_State* L): + serverThread(nullptr), runListener(true) { - LogInfo(L, "Starting RESTServer"); + +} + +void Server::start(lua_State* L) +{ + log("Starting RESTServer"); serverThread = new thread(&Server::task, this); } -Server::~Server() +void Server::stop(lua_State* L) { + log("Stopping RESTServer"); runListener = false; + if (serverThread != nullptr) + serverThread->join(); } void Server::handle_options(http_request request) { http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); request.reply(response); } void Server::handle_get(http_request request) { - http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); + /* Lock for thread safety */ + lock_guard guard(mutexLock); - auto answer = json::value::object(); - std::exception_ptr eptr; - try { - unitsFactory->updateAnswer(answer); - answer[L"airbases"] = airbasesData; - answer[L"bullseye"] = bullseyeData; - response.set_body(answer); + http_response response(status_codes::OK); + string authorization = to_base64("admin:" + to_string(password)); + if (password == L"" || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second == L"Basic " + to_wstring(authorization))) + { + std::exception_ptr eptr; + try { + auto answer = json::value::object(); + auto path = uri::split_path(uri::decode(request.relative_uri().path())); + + if (path.size() > 0) + { + if (path[0] == UNITS_URI) + { + map query = request.relative_uri().split_query(request.relative_uri().query()); + long long time = 0; + if (query.find(L"time") != query.end()) + { + try { + time = stoll((*(query.find(L"time"))).second); + } + catch (const std::exception& e) { + time = 0; + } + } + unitsManager->getData(answer, time); + } + else if (path[0] == LOGS_URI) + { + auto logs = json::value::object(); + getLogsJSON(logs, 100); // By reference, for thread safety. Get the last 100 log entries + answer[L"logs"] = logs; + } + else if (path[0] == AIRBASES_URI) + answer[L"airbases"] = airbases; + else if (path[0] == BULLSEYE_URI) + answer[L"bullseyes"] = bullseyes; + else if (path[0] == MISSION_URI) + answer[L"mission"] = mission; + + milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); + answer[L"time"] = json::value::string(to_wstring(ms.count())); + answer[L"sessionHash"] = json::value::string(to_wstring(sessionHash)); + } + + response.set_body(answer); + } + catch (...) { + eptr = std::current_exception(); // capture + } + handle_eptr(eptr); } - catch (...) { - eptr = std::current_exception(); // capture + else { + response = status_codes::Unauthorized; } - handle_eptr(eptr); + + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Origin"), U("*")); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); request.reply(response); } void Server::handle_request(http_request request, function action) { - auto answer = json::value::object(); - - request.extract_json().then([&answer, &action](pplx::task task) - { - try - { - auto const& jvalue = task.get(); - - if (!jvalue.is_null()) - { - action(jvalue, answer); - } - } - catch (http_exception const& e) - { - log(e.what()); - } - }).wait(); - http_response response(status_codes::OK); - response.headers().add(U("Allow"), U("GET, POST, PUT, OPTIONS")); + string authorization = to_base64("admin:" + to_string(password)); + if (password == L"" || (request.headers().has(L"Authorization") && request.headers().find(L"Authorization")->second == L"Basic " + to_wstring(authorization))) + { + auto answer = json::value::object(); + request.extract_json().then([&answer, &action](pplx::task task) + { + try + { + auto const& jvalue = task.get(); + + if (!jvalue.is_null()) + { + action(jvalue, answer); + } + } + catch (http_exception const& e) + { + log(e.what()); + } + }).wait(); + response.set_body(answer); + } + else { + response = status_codes::Unauthorized; + } + + response.headers().add(U("Allow"), U("GET, PUT, OPTIONS")); response.headers().add(U("Access-Control-Allow-Origin"), U("*")); - response.headers().add(U("Access-Control-Allow-Methods"), U("GET, POST, PUT, OPTIONS")); - response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type")); - response.set_body(answer); + response.headers().add(U("Access-Control-Allow-Methods"), U("GET, PUT, OPTIONS")); + response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type, Authorization")); + request.reply(response); } @@ -107,6 +175,9 @@ void Server::handle_put(http_request request) request, [](json::value const& jvalue, json::value& answer) { + /* Lock for thread safety */ + lock_guard guard(mutexLock); + for (auto const& e : jvalue.as_object()) { auto key = e.first; @@ -125,7 +196,41 @@ void Server::handle_put(http_request request) void Server::task() { - http_listener listener(REST_ADDRESS); + wstring address = wstring(REST_ADDRESS); + wstring modLocation; + char* buf = nullptr; + size_t sz = 0; + if (_dupenv_s(&buf, &sz, "DCSOLYMPUS_PATH") == 0 && buf != nullptr) + { + std::ifstream ifstream(string(buf) + "\\olympus.json"); + std::stringstream ss; + ss << ifstream.rdbuf(); + std::error_code errorCode; + json::value config = json::value::parse(to_wstring(ss.str()), errorCode); + if (config.is_object() && config.has_object_field(L"server") && + config[L"server"].has_string_field(L"address") && config[L"server"].has_number_field(L"port")) + { + address = L"http://" + config[L"server"][L"address"].as_string() + L":" + to_wstring(config[L"server"][L"port"].as_number().to_int32()); + log(L"Starting server on " + address); + } + else + log(L"Error reading configuration file. Starting server on " + address); + + if (config.is_object() && config.has_object_field(L"authentication") && + config[L"authentication"].has_string_field(L"password")) + { + password = config[L"authentication"][L"password"].as_string(); + } + else + log(L"Error reading configuration file. No password set."); + free(buf); + } + else + { + log(L"DCSOLYMPUS_PATH environment variable is missing, starting server on " + address); + } + + http_listener listener(address + L"/" + wstring(REST_URI)); std::function handle_options = std::bind(&Server::handle_options, this, std::placeholders::_1); std::function handle_get = std::bind(&Server::handle_get, this, std::placeholders::_1); @@ -143,7 +248,10 @@ void Server::task() while (runListener); - listener.close(); + listener.close() + .then([&listener]() {log("RESTServer stopping connections"); }) + .wait(); + log("RESTServer stopped listening"); } catch (exception const& e) diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp new file mode 100644 index 00000000..4b18efc7 --- /dev/null +++ b/src/core/src/unit.cpp @@ -0,0 +1,742 @@ +#include "unit.h" +#include "utils.h" +#include "logger.h" +#include "commands.h" +#include "scheduler.h" +#include "defines.h" +#include "unitsmanager.h" + +#include +using namespace std::chrono; + +#include +using namespace GeographicLib; + +extern Scheduler* scheduler; +extern UnitsManager* unitsManager; + +// TODO: Make dedicated file +bool operator==(const Options::TACAN& lhs, const Options::TACAN& rhs) +{ + return lhs.isOn == rhs.isOn && lhs.channel == rhs.channel && lhs.XY == rhs.XY && lhs.callsign == rhs.callsign; +} + +bool operator==(const Options::Radio& lhs, const Options::Radio& rhs) +{ + return lhs.frequency == rhs.frequency && lhs.callsign == rhs.callsign && lhs.callsignNumber == rhs.callsignNumber; +} + +bool operator==(const Options::GeneralSettings& lhs, const Options::GeneralSettings& rhs) +{ + return lhs.prohibitAA == rhs.prohibitAA && lhs.prohibitAfterburner == rhs.prohibitAfterburner && lhs.prohibitAG == rhs.prohibitAG && + lhs.prohibitAirWpn == rhs.prohibitAirWpn && lhs.prohibitJettison == rhs.prohibitJettison; +} + +Unit::Unit(json::value json, int ID) : + ID(ID) +{ + log("Creating unit with ID: " + to_string(ID)); +} + +Unit::~Unit() +{ + +} + +void Unit::initialize(json::value json) +{ + updateExportData(json); + setDefaults(); +} + +void Unit::setDefaults(bool force) +{ + const bool isUnitControlledByOlympus = getControlled(); + const bool isUnitAlive = getAlive(); + const bool isUnitLeader = unitsManager->isUnitGroupLeader(this); + const bool isUnitLeaderOfAGroupWithOtherUnits = unitsManager->isUnitInGroup(this) && unitsManager->isUnitGroupLeader(this); + const bool isUnitHuman = getFlags()[L"Human"].as_bool(); + if (isUnitControlledByOlympus && (isUnitAlive || isUnitLeaderOfAGroupWithOtherUnits) && isUnitLeader && !isUnitHuman) { + /* Set the default IDLE state */ + setState(State::IDLE); + + /* Set desired altitude to be equal to current altitude so the unit does not climb/descend after spawn */ + setDesiredAltitude(altitude); + + /* Set the default options (these are all defaults so will only affect the export data, no DCS command will be sent) */ + setROE(L"Designated", force); + setReactionToThreat(L"Evade", force); + setEmissionsCountermeasures(L"Defend", force); + setTACAN(TACAN, force); + setRadio(radio, force); + setEPLRS(EPLRS, force); + setGeneralSettings(generalSettings, force); + setOnOff(onOff); + setFollowRoads(followRoads); + } +} + +void Unit::addMeasure(wstring key, json::value value) +{ + milliseconds ms = duration_cast(system_clock::now().time_since_epoch()); + if (measures.find(key) == measures.end()) + measures[key] = new Measure(value, ms.count()); + else + { + if (measures[key]->getValue() != value) + { + measures[key]->setValue(value); + measures[key]->setTime(ms.count()); + } + } +} + +void Unit::runAILoop() { + /* If the unit is alive and it is not a human, run the AI Loop that performs the requested commands and instructions (moving, attacking, etc) */ + const bool isUnitControlledByOlympus = getControlled(); + const bool isUnitAlive = getAlive(); + const bool isUnitLeader = unitsManager->isUnitGroupLeader(this); + const bool isUnitLeaderOfAGroupWithOtherUnits = unitsManager->isUnitInGroup(this) && unitsManager->isUnitGroupLeader(this); + const bool isUnitHuman = getFlags()[L"Human"].as_bool(); + + // Keep running the AI loop even if the unit is dead if it is the leader of a group which has other members in it + if (isUnitControlledByOlympus && (isUnitAlive || isUnitLeaderOfAGroupWithOtherUnits) && isUnitLeader && !isUnitHuman) + { + if (checkTaskFailed() && state != State::IDLE && State::LAND) + setState(State::IDLE); + + AIloop(); + } +} + +void Unit::updateExportData(json::value json) +{ + /* Compute speed (loGetWorldObjects does not provide speed, we compute it for better performance instead of relying on many lua calls) */ + if (oldPosition != NULL) + { + double dist = 0; + Geodesic::WGS84().Inverse(latitude, longitude, oldPosition.lat, oldPosition.lng, dist); + setSpeed(getSpeed() * 0.95 + (dist / UPDATE_TIME_INTERVAL) * 0.05); + } + oldPosition = Coords(latitude, longitude, altitude); + + if (json.has_string_field(L"Name")) + setName(json[L"Name"].as_string()); + if (json.has_string_field(L"UnitName")) + setUnitName(json[L"UnitName"].as_string()); + if (json.has_string_field(L"GroupName")) + setGroupName(json[L"GroupName"].as_string()); + if (json.has_object_field(L"Type")) + setType(json[L"Type"]); + if (json.has_number_field(L"Country")) + setCountry(json[L"Country"].as_number().to_int32()); + if (json.has_number_field(L"CoalitionID")) + setCoalitionID(json[L"CoalitionID"].as_number().to_int32()); + if (json.has_object_field(L"LatLongAlt")) + { + setLatitude(json[L"LatLongAlt"][L"Lat"].as_number().to_double()); + setLongitude(json[L"LatLongAlt"][L"Long"].as_number().to_double()); + setAltitude(json[L"LatLongAlt"][L"Alt"].as_number().to_double()); + } + if (json.has_number_field(L"Heading")) + setHeading(json[L"Heading"].as_number().to_double()); + if (json.has_object_field(L"Flags")) + setFlags(json[L"Flags"]); + + /* All units which contain the name "Olympus" are automatically under AI control */ + if (getUnitName().find(L"Olympus") != wstring::npos) + setControlled(true); +} + +void Unit::updateMissionData(json::value json) +{ + if (json.has_number_field(L"fuel")) + setFuel(int(json[L"fuel"].as_number().to_double() * 100)); + if (json.has_object_field(L"ammo")) + setAmmo(json[L"ammo"]); + if (json.has_object_field(L"contacts")) + setContacts(json[L"contacts"]); + if (json.has_boolean_field(L"hasTask")) + setHasTask(json[L"hasTask"].as_bool()); +} + +json::value Unit::getData(long long time, bool sendAll) +{ + auto json = json::value::object(); + + /* If the unit is in a group, task & option data is given by the group leader */ + if (unitsManager->isUnitInGroup(this) && !unitsManager->isUnitGroupLeader(this)) + json = unitsManager->getGroupLeader(this)->getData(time, true); + + /********** Base data **********/ + json[L"baseData"] = json::value::object(); + for (auto key : { L"controlled", L"name", L"unitName", L"groupName", L"alive", L"category"}) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"baseData"][key] = measures[key]->getValue(); + } + if (json[L"baseData"].size() == 0) + json.erase(L"baseData"); + + if (alive || sendAll) { + /********** Flight data **********/ + json[L"flightData"] = json::value::object(); + for (auto key : { L"latitude", L"longitude", L"altitude", L"speed", L"heading" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"flightData"][key] = measures[key]->getValue(); + } + if (json[L"flightData"].size() == 0) + json.erase(L"flightData"); + + /********** Mission data **********/ + json[L"missionData"] = json::value::object(); + for (auto key : { L"fuel", L"ammo", L"contacts", L"hasTask", L"coalition", L"flags" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"missionData"][key] = measures[key]->getValue(); + } + if (json[L"missionData"].size() == 0) + json.erase(L"missionData"); + + /********** Formation data **********/ + json[L"formationData"] = json::value::object(); + for (auto key : { L"leaderID" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"formationData"][key] = measures[key]->getValue(); + } + if (json[L"formationData"].size() == 0) + json.erase(L"formationData"); + + /* If the unit is in a group, task & option data is given by the group leader */ + if (unitsManager->isUnitGroupLeader(this)) { + /********** Task data **********/ + json[L"taskData"] = json::value::object(); + for (auto key : { L"currentState", L"currentTask", L"desiredSpeed", L"desiredAltitude", L"desiredSpeedType", L"desiredAltitudeType", L"activePath", L"isTanker", L"isAWACS", L"onOff", L"followRoads", L"targetID", L"targetLocation" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"taskData"][key] = measures[key]->getValue(); + } + if (json[L"taskData"].size() == 0) + json.erase(L"taskData"); + + /********** Options data **********/ + json[L"optionsData"] = json::value::object(); + for (auto key : { L"ROE", L"reactionToThreat", L"emissionsCountermeasures", L"TACAN", L"radio", L"generalSettings" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"optionsData"][key] = measures[key]->getValue(); + } + if (json[L"optionsData"].size() == 0) + json.erase(L"optionsData"); + } + } + + return json; +} + +void Unit::setActivePath(list newPath) +{ + activePath = newPath; + resetActiveDestination(); + + auto path = json::value::object(); + if (activePath.size() > 0) { + int count = 1; + for (auto& destination : activePath) + { + auto json = json::value::object(); + json[L"lat"] = destination.lat; + json[L"lng"] = destination.lng; + json[L"alt"] = destination.alt; + path[to_wstring(count++)] = json; + } + } + addMeasure(L"activePath", path); +} + +void Unit::clearActivePath() +{ + list newPath; + setActivePath(newPath); +} + +void Unit::pushActivePathFront(Coords newActivePathFront) +{ + list path = activePath; + path.push_front(newActivePathFront); + setActivePath(path); +} + +void Unit::pushActivePathBack(Coords newActivePathBack) +{ + list path = activePath; + path.push_back(newActivePathBack); + setActivePath(path); +} + +void Unit::popActivePathFront() +{ + list path = activePath; + path.pop_front(); + setActivePath(path); +} + +void Unit::setCoalitionID(int newCoalitionID) +{ + if (newCoalitionID == 0) + coalition = L"neutral"; + else if (newCoalitionID == 1) + coalition = L"red"; + else + coalition = L"blue"; + addMeasure(L"coalition", json::value(coalition)); +} + +int Unit::getCoalitionID() +{ + if (coalition == L"neutral") + return 0; + else if (coalition == L"red") + return 1; + else + return 2; +} + +wstring Unit::getTargetName() +{ + if (isTargetAlive()) + { + Unit* target = unitsManager->getUnit(targetID); + if (target != nullptr) + return target->getUnitName(); + } + return L""; +} + +bool Unit::isTargetAlive() +{ + if (targetID == NULL) + return false; + + Unit* target = unitsManager->getUnit(targetID); + if (target != nullptr) + return target->alive; + else + return false; +} + +wstring Unit::getLeaderName() +{ + if (isLeaderAlive()) + { + Unit* leader = unitsManager->getUnit(leaderID); + if (leader != nullptr) + return leader->getUnitName(); + } + return L""; +} + +bool Unit::isLeaderAlive() +{ + if (leaderID == NULL) + return false; + + Unit* leader = unitsManager->getUnit(leaderID); + if (leader != nullptr) + return leader->alive; + else + return false; +} + +void Unit::resetActiveDestination() +{ + activeDestination = Coords(NULL); +} + +void Unit::resetTask() +{ + Command* command = dynamic_cast(new ResetTask(groupName)); + scheduler->appendCommand(command); + setHasTask(false); + resetTaskFailedCounter(); +} + +void Unit::setFormationOffset(Offset newFormationOffset) +{ + formationOffset = newFormationOffset; + resetTask(); +} + +void Unit::setROE(wstring newROE, bool force) { + addMeasure(L"ROE", json::value(newROE)); + + if (ROE != newROE || force) { + ROE = newROE; + + int ROEEnum; + if (ROE.compare(L"Free") == 0) + ROEEnum = ROE::WEAPON_FREE; + else if (ROE.compare(L"Designated free") == 0) + ROEEnum = ROE::OPEN_FIRE_WEAPON_FREE; + else if (ROE.compare(L"Designated") == 0) + ROEEnum = ROE::OPEN_FIRE; + else if (ROE.compare(L"Return") == 0) + ROEEnum = ROE::RETURN_FIRE; + else if (ROE.compare(L"Hold") == 0) + ROEEnum = ROE::WEAPON_HOLD; + else + return; + + Command* command = dynamic_cast(new SetOption(groupName, SetCommandType::ROE, ROEEnum)); + scheduler->appendCommand(command); + } +} + +void Unit::setReactionToThreat(wstring newReactionToThreat, bool force) { + addMeasure(L"reactionToThreat", json::value(newReactionToThreat)); + + if (reactionToThreat != newReactionToThreat || force) { + reactionToThreat = newReactionToThreat; + + int reactionToThreatEnum; + if (reactionToThreat.compare(L"None") == 0) + reactionToThreatEnum = ReactionToThreat::NO_REACTION; + else if (reactionToThreat.compare(L"Passive") == 0) + reactionToThreatEnum = ReactionToThreat::PASSIVE_DEFENCE; + else if (reactionToThreat.compare(L"Evade") == 0) + reactionToThreatEnum = ReactionToThreat::EVADE_FIRE; + else if (reactionToThreat.compare(L"Escape") == 0) + reactionToThreatEnum = ReactionToThreat::BYPASS_AND_ESCAPE; + else if (reactionToThreat.compare(L"Abort") == 0) + reactionToThreatEnum = ReactionToThreat::ALLOW_ABORT_MISSION; + else + return; + + Command* command = dynamic_cast(new SetOption(groupName, SetCommandType::REACTION_ON_THREAT, reactionToThreatEnum)); + scheduler->appendCommand(command); + } +} + +void Unit::setEmissionsCountermeasures(wstring newEmissionsCountermeasures, bool force) { + addMeasure(L"emissionsCountermeasures", json::value(newEmissionsCountermeasures)); + + if (emissionsCountermeasures != newEmissionsCountermeasures || force) { + emissionsCountermeasures = newEmissionsCountermeasures; + + int radarEnum; + int flareEnum; + int ECMEnum; + if (emissionsCountermeasures.compare(L"Silent") == 0) + { + radarEnum = RadarUse::NEVER; + flareEnum = FlareUse::NEVER; + ECMEnum = ECMUse::NEVER_USE; + } + else if (emissionsCountermeasures.compare(L"Attack") == 0) + { + radarEnum = RadarUse::FOR_ATTACK_ONLY; + flareEnum = FlareUse::AGAINST_FIRED_MISSILE; + ECMEnum = ECMUse::USE_IF_ONLY_LOCK_BY_RADAR; + } + else if (emissionsCountermeasures.compare(L"Defend") == 0) + { + radarEnum = RadarUse::FOR_SEARCH_IF_REQUIRED; + flareEnum = FlareUse::WHEN_FLYING_IN_SAM_WEZ; + ECMEnum = ECMUse::USE_IF_DETECTED_LOCK_BY_RADAR; + } + else if (emissionsCountermeasures.compare(L"Free") == 0) + { + radarEnum = RadarUse::FOR_CONTINUOUS_SEARCH; + flareEnum = FlareUse::WHEN_FLYING_NEAR_ENEMIES; + ECMEnum = ECMUse::ALWAYS_USE; + } + else + return; + + Command* command; + + command = dynamic_cast(new SetOption(groupName, SetCommandType::RADAR_USING, radarEnum)); + scheduler->appendCommand(command); + + command = dynamic_cast(new SetOption(groupName, SetCommandType::FLARE_USING, flareEnum)); + scheduler->appendCommand(command); + + command = dynamic_cast(new SetOption(groupName, SetCommandType::ECM_USING, ECMEnum)); + scheduler->appendCommand(command); + } +} + +void Unit::landAt(Coords loc) { + clearActivePath(); + pushActivePathBack(loc); + setState(State::LAND); +} + +void Unit::setIsTanker(bool newIsTanker) { + isTanker = newIsTanker; + resetTask(); + addMeasure(L"isTanker", json::value(newIsTanker)); +} + +void Unit::setIsAWACS(bool newIsAWACS) { + isAWACS = newIsAWACS; + resetTask(); + addMeasure(L"isAWACS", json::value(newIsAWACS)); + setEPLRS(isAWACS); +} + +void Unit::setTACAN(Options::TACAN newTACAN, bool force) { + auto json = json::value(); + json[L"isOn"] = json::value(newTACAN.isOn); + json[L"channel"] = json::value(newTACAN.channel); + json[L"XY"] = json::value(newTACAN.XY); + json[L"callsign"] = json::value(newTACAN.callsign); + addMeasure(L"TACAN", json); + + if (TACAN != newTACAN || force) + { + TACAN = newTACAN; + if (TACAN.isOn) { + std::wostringstream commandSS; + commandSS << "{" + << "id = 'ActivateBeacon'," + << "params = {" + << "type = " << ((TACAN.XY.compare(L"X") == 0) ? 4 : 5) << "," + << "system = 3," + << "name = \"Olympus_TACAN\"," + << "callsign = \"" << TACAN.callsign << "\", " + << "frequency = " << TACANChannelToFrequency(TACAN.channel, TACAN.XY) << "," + << "}" + << "}"; + Command* command = dynamic_cast(new SetCommand(groupName, commandSS.str())); + scheduler->appendCommand(command); + } + else { + std::wostringstream commandSS; + commandSS << "{" + << "id = 'DeactivateBeacon'," + << "params = {" + << "}" + << "}"; + Command* command = dynamic_cast(new SetCommand(groupName, commandSS.str())); + scheduler->appendCommand(command); + } + } +} + +void Unit::setRadio(Options::Radio newRadio, bool force) { + + auto json = json::value(); + json[L"frequency"] = json::value(newRadio.frequency); + json[L"callsign"] = json::value(newRadio.callsign); + json[L"callsignNumber"] = json::value(newRadio.callsignNumber); + addMeasure(L"radio", json); + + if (radio != newRadio || force) + { + radio = newRadio; + + std::wostringstream commandSS; + Command* command; + + commandSS << "{" + << "id = 'SetFrequency'," + << "params = {" + << "modulation = 0," // TODO Allow selection + << "frequency = " << radio.frequency << "," + << "}" + << "}"; + command = dynamic_cast(new SetCommand(groupName, commandSS.str())); + scheduler->appendCommand(command); + + // Clear the stringstream + commandSS.str(wstring()); + + commandSS << "{" + << "id = 'SetCallsign'," + << "params = {" + << "callname = " << radio.callsign << "," + << "number = " << radio.callsignNumber << "," + << "}" + << "}"; + command = dynamic_cast(new SetCommand(groupName, commandSS.str())); + scheduler->appendCommand(command); + } +} + +void Unit::setEPLRS(bool newEPLRS, bool force) +{ + //addMeasure(L"EPLRS", json::value(newEPLRS)); + // + //if (EPLRS != newEPLRS || force) { + // EPLRS = newEPLRS; + // + // std::wostringstream commandSS; + // commandSS << "{" + // << "id = 'EPLRS'," + // << "params = {" + // << "value = " << (EPLRS ? "true" : "false") << ", " + // << "}" + // << "}"; + // Command* command = dynamic_cast(new SetCommand(ID, commandSS.str())); + // scheduler->appendCommand(command); + //} +} + +void Unit::setGeneralSettings(Options::GeneralSettings newGeneralSettings, bool force) { + + auto json = json::value(); + json[L"prohibitJettison"] = json::value(newGeneralSettings.prohibitJettison); + json[L"prohibitAA"] = json::value(newGeneralSettings.prohibitAA); + json[L"prohibitAG"] = json::value(newGeneralSettings.prohibitAG); + json[L"prohibitAfterburner"] = json::value(newGeneralSettings.prohibitAfterburner); + json[L"prohibitAirWpn"] = json::value(newGeneralSettings.prohibitAirWpn); + addMeasure(L"generalSettings", json); + + if (generalSettings != newGeneralSettings) + { + generalSettings = newGeneralSettings; + + Command* command; + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AA, generalSettings.prohibitAA)); + scheduler->appendCommand(command); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AG, generalSettings.prohibitAG)); + scheduler->appendCommand(command); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_JETT, generalSettings.prohibitJettison)); + scheduler->appendCommand(command); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AB, generalSettings.prohibitAfterburner)); + scheduler->appendCommand(command); + command = dynamic_cast(new SetOption(groupName, SetCommandType::ENGAGE_AIR_WEAPONS, !generalSettings.prohibitAirWpn)); + scheduler->appendCommand(command); + } +} + +void Unit::setDesiredSpeed(double newDesiredSpeed) { + desiredSpeed = newDesiredSpeed; + addMeasure(L"desiredSpeed", json::value(newDesiredSpeed)); + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ +} + +void Unit::setDesiredAltitude(double newDesiredAltitude) { + desiredAltitude = newDesiredAltitude; + addMeasure(L"desiredAltitude", json::value(newDesiredAltitude)); + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ +} + +void Unit::setDesiredSpeedType(wstring newDesiredSpeedType) { + desiredSpeedType = newDesiredSpeedType; + addMeasure(L"desiredSpeedType", json::value(newDesiredSpeedType)); + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ +} + +void Unit::setDesiredAltitudeType(wstring newDesiredAltitudeType) { + desiredAltitudeType = newDesiredAltitudeType; + addMeasure(L"desiredAltitudeType", json::value(newDesiredAltitudeType)); + if (state == State::IDLE) + resetTask(); + else + goToDestination(); /* Send the command to reach the destination */ +} + +void Unit::goToDestination(wstring enrouteTask) +{ + if (activeDestination != NULL) + { + Command* command = dynamic_cast(new Move(groupName, activeDestination, getDesiredSpeed(), getDesiredSpeedType(), getDesiredAltitude(), getDesiredAltitudeType(), enrouteTask, getCategory())); + scheduler->appendCommand(command); + setHasTask(true); + } +} + +bool Unit::isDestinationReached(double threshold) +{ + if (activeDestination != NULL) + { + /* Check if any unit in the group has reached the point */ + for (auto const& p: unitsManager->getGroupMembers(groupName)) + { + double dist = 0; + Geodesic::WGS84().Inverse(p->getLatitude(), p->getLongitude(), activeDestination.lat, activeDestination.lng, dist); + if (dist < threshold) + { + log(unitName + L" destination reached"); + return true; + } + else { + return false; + } + } + } + else + return true; +} + +bool Unit::setActiveDestination() +{ + if (activePath.size() > 0) + { + activeDestination = activePath.front(); + log(unitName + L" active destination set to queue front"); + return true; + } + else + { + activeDestination = Coords(0); + log(unitName + L" active destination set to NULL"); + return false; + } +} + +bool Unit::updateActivePath(bool looping) +{ + if (activePath.size() > 0) + { + /* Push the next destination in the queue to the front */ + if (looping) + pushActivePathBack(activePath.front()); + popActivePathFront(); + log(unitName + L" active path front popped"); + return true; + } + else { + return false; + } +} + +void Unit::setTargetLocation(Coords newTargetLocation) { + targetLocation = newTargetLocation; + auto json = json::value(); + json[L"latitude"] = json::value(newTargetLocation.lat); + json[L"longitude"] = json::value(newTargetLocation.lng); + addMeasure(L"targetLocation", json::value(json)); +} + +bool Unit::checkTaskFailed() { + if (getHasTask()) + return false; + else { + if (taskCheckCounter > 0) + taskCheckCounter--; + return taskCheckCounter == 0; + } +} + +void Unit::resetTaskFailedCounter() { + taskCheckCounter = TASK_CHECK_INIT_VALUE; +} + +void Unit::setHasTask(bool newHasTask) { + hasTask = newHasTask; + addMeasure(L"hasTask", json::value(newHasTask)); +} \ No newline at end of file diff --git a/src/core/src/unitsFactory.cpp b/src/core/src/unitsFactory.cpp deleted file mode 100644 index 6a3104bf..00000000 --- a/src/core/src/unitsFactory.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "framework.h" -#include "unitsFactory.h" -#include "logger.h" -#include "unit.h" -#include "aircraft.h" -#include "helicopter.h" -#include "groundunit.h" -#include "navyunit.h" -#include "weapon.h" -#include "commands.h" -#include "scheduler.h" - -extern Scheduler* scheduler; - -UnitsFactory::UnitsFactory(lua_State* L) -{ - LogInfo(L, "Units Factory constructor called successfully"); -} - -UnitsFactory::~UnitsFactory() -{ - -} - -Unit* UnitsFactory::getUnit(int ID) -{ - if (units.find(ID) == units.end()) { - return nullptr; - } - else { - return units[ID]; - } -} - -void UnitsFactory::updateExportData(lua_State* L) -{ - map unitJSONs = getAllUnits(L); - - /* Update all units, create them if needed TODO: move code to get constructor in dedicated function */ - for (auto const& p : unitJSONs) - { - int ID = p.first; - if (units.count(ID) == 0) - { - json::value type = static_cast(p.second)[L"Type"]; - if (type.has_number_field(L"level1")) - { - if (type[L"level1"].as_number().to_int32() == 1) - { - if (type[L"level2"].as_number().to_int32() == 1) - { - units[ID] = dynamic_cast(new Aircraft(p.second, ID)); - } - else if (type[L"level2"].as_number().to_int32() == 2) - { - units[ID] = dynamic_cast(new Helicopter(p.second, ID)); - } - } - else if (type[L"level1"].as_number().to_int32() == 2) - { - units[ID] = dynamic_cast(new GroundUnit(p.second, ID)); - } - else if (type[L"level1"].as_number().to_int32() == 3) - { - units[ID] = dynamic_cast(new NavyUnit(p.second, ID)); - } - else if (type[L"level1"].as_number().to_int32() == 4) - { - if (type[L"level2"].as_number().to_int32() == 4) - { - units[ID] = dynamic_cast(new Missile(p.second, ID)); - } - else if (type[L"level2"].as_number().to_int32() == 5) - { - units[ID] = dynamic_cast(new Bomb(p.second, ID)); - } - } - } - } - /* Update the unit if present*/ - if (units.count(ID) != 0) - { - units[ID]->updateExportData(p.second); - } - } - - /* Set the units that are not present in the JSON as dead (probably have been destroyed) */ - for (auto const& unit : units) - { - if (unitJSONs.find(unit.first) == unitJSONs.end()) - { - unit.second->setAlive(false); - } - } -} - -void UnitsFactory::updateMissionData(json::value missionData) -{ - /* Update all units */ - for (auto const& p : units) - { - int ID = p.first; - if (missionData.has_field(to_wstring(ID))) - { - p.second->updateMissionData(missionData[to_wstring(ID)]); - } - } -} - -void UnitsFactory::updateAnswer(json::value& answer) -{ - // TODO THREAT SAFEY! - auto unitsJson = json::value::object(); - - for (auto const& p : units) - { - unitsJson[to_wstring(p.first)] = p.second->json(); - } - - answer[L"units"] = unitsJson; -} - -void UnitsFactory::deleteUnit(int ID) -{ - if (getUnit(ID) != nullptr) - { - Command* command = dynamic_cast(new Delete(ID)); - scheduler->appendCommand(command); - } -} - diff --git a/src/core/src/unitsmanager.cpp b/src/core/src/unitsmanager.cpp new file mode 100644 index 00000000..2c683858 --- /dev/null +++ b/src/core/src/unitsmanager.cpp @@ -0,0 +1,201 @@ +#include "framework.h" +#include "unitsManager.h" +#include "logger.h" +#include "unit.h" +#include "aircraft.h" +#include "helicopter.h" +#include "groundunit.h" +#include "navyunit.h" +#include "weapon.h" +#include "commands.h" +#include "scheduler.h" + +extern Scheduler* scheduler; + +UnitsManager::UnitsManager(lua_State* L) +{ + LogInfo(L, "Units Factory constructor called successfully"); +} + +UnitsManager::~UnitsManager() +{ + +} + +Unit* UnitsManager::getUnit(int ID) +{ + if (units.find(ID) == units.end()) { + return nullptr; + } + else { + return units[ID]; + } +} + +bool UnitsManager::isUnitInGroup(Unit* unit) +{ + if (unit != nullptr) { + wstring groupName = unit->getGroupName(); + for (auto const& p : units) + { + if (p.second->getGroupName().compare(groupName) == 0 && p.second != unit) + return true; + } + } + return false; +} + +bool UnitsManager::isUnitGroupLeader(Unit* unit) +{ + if (unit != nullptr) + return unit == getGroupLeader(unit); + else + return false; +} + +// The group leader is the unit with the lowest ID that is part of the group. This is different from DCS's concept of leader, which will change if the leader is destroyed +Unit* UnitsManager::getGroupLeader(Unit* unit) +{ + if (unit != nullptr) { + wstring groupName = unit->getGroupName(); + + /* Get the unit IDs in order */ + std::vector keys; + for (auto const& p : units) + keys.push_back(p.first); + sort(keys.begin(), keys.end()); + + /* Find the first unit that has the same groupName */ + for (auto const& tempID : keys) + { + Unit* tempUnit = getUnit(tempID); + if (tempUnit != nullptr && tempUnit->getGroupName().compare(groupName) == 0) + return tempUnit; + } + } + return nullptr; +} + +vector UnitsManager::getGroupMembers(wstring groupName) +{ + vector members; + for (auto const& p : units) + { + if (p.second->getGroupName().compare(groupName) == 0) + members.push_back(p.second); + } + return members; +} + +Unit* UnitsManager::getGroupLeader(int ID) +{ + Unit* unit = getUnit(ID); + return getGroupLeader(unit); +} + +void UnitsManager::updateExportData(lua_State* L) +{ + map unitJSONs = getAllUnits(L); + + /* Update all units, create them if needed TODO: move code to get constructor in dedicated function */ + for (auto const& p : unitJSONs) + { + int ID = p.first; + if (units.count(ID) == 0) + { + json::value type = static_cast(p.second)[L"Type"]; + if (type.has_number_field(L"level1")) + { + if (type[L"level1"].as_number().to_int32() == 1) + { + if (type[L"level2"].as_number().to_int32() == 1) + units[ID] = dynamic_cast(new Aircraft(p.second, ID)); + else if (type[L"level2"].as_number().to_int32() == 2) + units[ID] = dynamic_cast(new Helicopter(p.second, ID)); + } + else if (type[L"level1"].as_number().to_int32() == 2) + units[ID] = dynamic_cast(new GroundUnit(p.second, ID)); + else if (type[L"level1"].as_number().to_int32() == 3) + units[ID] = dynamic_cast(new NavyUnit(p.second, ID)); + else if (type[L"level1"].as_number().to_int32() == 4) + { + if (type[L"level2"].as_number().to_int32() == 4) + units[ID] = dynamic_cast(new Missile(p.second, ID)); + else if (type[L"level2"].as_number().to_int32() == 5) + units[ID] = dynamic_cast(new Bomb(p.second, ID)); + } + } + /* Initialize the unit if creation was successfull */ + if (units.count(ID) != 0) + units[ID]->initialize(p.second); + } + else { + /* Update the unit if present*/ + if (units.count(ID) != 0) + units[ID]->updateExportData(p.second); + } + } + + /* Set the units that are not present in the JSON as dead (probably have been destroyed) */ + for (auto const& unit : units) + { + unit.second->setAlive(unitJSONs.find(unit.first) != unitJSONs.end()); + } +} + +void UnitsManager::updateMissionData(json::value missionData) +{ + /* Update all units */ + for (auto const& p : units) + { + int ID = p.first; + if (missionData.has_field(to_wstring(ID))) + { + p.second->updateMissionData(missionData[to_wstring(ID)]); + } + } +} + +void UnitsManager::runAILoop() { + /* Run the AI Loop on all units */ + for (auto const& unit : units) + { + unit.second->runAILoop(); + } +} + +void UnitsManager::getData(json::value& answer, long long time) +{ + auto unitsJson = json::value::object(); + for (auto const& p : units) + { + auto unitJson = p.second->getData(time); + if (unitJson.size() > 0) + unitsJson[to_wstring(p.first)] = p.second->getData(time); + } + answer[L"units"] = unitsJson; +} + +void UnitsManager::deleteUnit(int ID, bool explosion) +{ + if (getUnit(ID) != nullptr) + { + Command* command = dynamic_cast(new Delete(ID, explosion)); + scheduler->appendCommand(command); + } +} + +void UnitsManager::acquireControl(int ID) { + Unit* unit = getUnit(ID); + if (unit != nullptr) { + for (auto const& groupMember : getGroupMembers(unit->getGroupName())) { + if (!groupMember->getControlled()) { + groupMember->setControlled(true); + groupMember->setState(State::IDLE); + groupMember->setDefaults(true); + } + } + } + +} + diff --git a/src/core/src/weapon.cpp b/src/core/src/weapon.cpp index 4025bb82..7705b710 100644 --- a/src/core/src/weapon.cpp +++ b/src/core/src/weapon.cpp @@ -4,13 +4,13 @@ #include "commands.h" #include "scheduler.h" #include "defines.h" -#include "unitsFactory.h" +#include "unitsmanager.h" #include using namespace GeographicLib; extern Scheduler* scheduler; -extern UnitsFactory* unitsFactory; +extern UnitsManager* unitsManager; /* Weapon */ Weapon::Weapon(json::value json, int ID) : Unit(json, ID) @@ -22,10 +22,12 @@ Weapon::Weapon(json::value json, int ID) : Unit(json, ID) Missile::Missile(json::value json, int ID) : Weapon(json, ID) { log("New Missile created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); }; /* Bomb */ Bomb::Bomb(json::value json, int ID) : Weapon(json, ID) { log("New Bomb created with ID: " + to_string(ID)); + addMeasure(L"category", json::value(getCategory())); }; \ No newline at end of file diff --git a/src/dcstools/include/dcstools.h b/src/dcstools/include/dcstools.h index b4866db4..db8ff519 100644 --- a/src/dcstools/include/dcstools.h +++ b/src/dcstools/include/dcstools.h @@ -8,4 +8,5 @@ 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); +int DllExport TACANChannelToFrequency(int channel, wstring XY); diff --git a/src/dcstools/src/dcstools.cpp b/src/dcstools/src/dcstools.cpp index 3995f9b5..64184a98 100644 --- a/src/dcstools/src/dcstools.cpp +++ b/src/dcstools/src/dcstools.cpp @@ -94,7 +94,6 @@ exit: return units; } - int dostring_in(lua_State* L, string target, string command) { lua_getglobal(L, "net"); @@ -102,4 +101,10 @@ int dostring_in(lua_State* L, string target, string command) lua_pushstring(L, target.c_str()); lua_pushstring(L, command.c_str()); return lua_pcall(L, 2, 0, 0); +} + +int TACANChannelToFrequency(int channel, wstring XY) +{ + int basef = (XY == L"X" && channel > 63) || (XY == L"Y" && channel < 64) ? 1087: 961; + return (basef + channel) * 1000000; } \ No newline at end of file diff --git a/src/logger/include/interface.h b/src/logger/include/interface.h index f28155c5..f5dbe800 100644 --- a/src/logger/include/interface.h +++ b/src/logger/include/interface.h @@ -3,3 +3,4 @@ void DllExport log(const std::string& sMessage); void DllExport log(const std::wstring& sMessage); +void DllExport getLogsJSON(json::value& json, int logsNumber = NULL); diff --git a/src/logger/include/logger.h b/src/logger/include/logger.h index 1db3336d..20e17d71 100644 --- a/src/logger/include/logger.h +++ b/src/logger/include/logger.h @@ -5,10 +5,12 @@ class Logger { public: - void Log(const string& sMessage); - void Log(const wstring& sMessage); + void log(const string& sMessage); + void log(const wstring& sMessage); + void toJSON(json::value& json, int logsNumber = NULL); static Logger* GetLogger(); + private: Logger(); Logger(const Logger&) {}; // copy constructor is private @@ -17,6 +19,9 @@ private: static const string m_sFileName; static Logger* m_pThis; static ofstream m_Logfile; + static std::list m_logs; + + mutex mutexLock; void Open(); void Close(); diff --git a/src/logger/src/interface.cpp b/src/logger/src/interface.cpp index 4589e508..77c743b5 100644 --- a/src/logger/src/interface.cpp +++ b/src/logger/src/interface.cpp @@ -6,10 +6,15 @@ void log(const string& message) { - LOGGER->Log(message); + LOGGER->log(message); } void log(const wstring& message) { - LOGGER->Log(message); + LOGGER->log(message); +} + +void getLogsJSON(json::value& json, int logsNumber) +{ + LOGGER->toJSON(json, logsNumber); } \ No newline at end of file diff --git a/src/logger/src/logger.cpp b/src/logger/src/logger.cpp index ef8d0981..4c9810d3 100644 --- a/src/logger/src/logger.cpp +++ b/src/logger/src/logger.cpp @@ -5,6 +5,7 @@ const string Logger::m_sFileName = LOG_NAME; Logger* Logger::m_pThis = NULL; ofstream Logger::m_Logfile; +std::list Logger::m_logs; Logger::Logger() { @@ -15,8 +16,7 @@ Logger* Logger::GetLogger() if (m_pThis == NULL) { m_pThis = new Logger(); std::filesystem::path dirPath = std::filesystem::temp_directory_path(); - m_Logfile.open((dirPath.string() + m_sFileName).c_str(), ios::out | ios::app); - m_pThis->Log("**************************************************"); + m_Logfile.open((dirPath.string() + m_sFileName).c_str(), ios::out); } return m_pThis; } @@ -32,18 +32,34 @@ void Logger::Close() m_Logfile.close(); } -void Logger::Log(const string& message) +void Logger::toJSON(json::value& json, int logsNumber) { + lock_guard guard(mutexLock); + int i = 0; + for (auto itr = m_logs.end(); itr != m_logs.begin(); --itr) + { + json[to_wstring(m_logs.size() - 1 - i)] = json::value::string(to_wstring(*itr)); + if (logsNumber != 0 && i > logsNumber) + break; + } +} + +void Logger::log(const string& message) +{ + lock_guard guard(mutexLock); Open(); m_Logfile << CurrentDateTime() << ":\t"; m_Logfile << message << "\n"; + m_logs.push_back(CurrentDateTime() + ": " + message); Close(); } -void Logger::Log(const wstring& message) +void Logger::log(const wstring& message) { + lock_guard guard(mutexLock); Open(); m_Logfile << CurrentDateTime() << ":\t"; m_Logfile << to_string(message) << "\n"; + m_logs.push_back(CurrentDateTime() + ": " + to_string(message)); Close(); } diff --git a/src/Olympus.sln b/src/olympus.sln similarity index 100% rename from src/Olympus.sln rename to src/olympus.sln diff --git a/src/Olympus/Olympus.filters b/src/olympus/olympus.filters similarity index 100% rename from src/Olympus/Olympus.filters rename to src/olympus/olympus.filters diff --git a/src/Olympus/Olympus.vcxproj b/src/olympus/olympus.vcxproj similarity index 100% rename from src/Olympus/Olympus.vcxproj rename to src/olympus/olympus.vcxproj diff --git a/src/Olympus/Olympus.vcxproj.filters b/src/olympus/olympus.vcxproj.filters similarity index 100% rename from src/Olympus/Olympus.vcxproj.filters rename to src/olympus/olympus.vcxproj.filters diff --git a/src/Olympus/src/olympus.cpp b/src/olympus/src/olympus.cpp similarity index 100% rename from src/Olympus/src/olympus.cpp rename to src/olympus/src/olympus.cpp diff --git a/src/shared/include/defines.h b/src/shared/include/defines.h index ae844c49..9289e51d 100644 --- a/src/shared/include/defines.h +++ b/src/shared/include/defines.h @@ -1,7 +1,13 @@ #pragma once -#define VERSION "v0.0.1" +#define VERSION "v0.2.1" #define LOG_NAME "Olympus_log.txt" -#define REST_ADDRESS L"http://localhost:30000/restdemo" +#define REST_ADDRESS L"http://localhost:30000" +#define REST_URI L"olympus" +#define UNITS_URI L"units" +#define LOGS_URI L"logs" +#define AIRBASES_URI L"airbases" +#define BULLSEYE_URI L"bullseyes" +#define MISSION_URI L"mission" #define UPDATE_TIME_INTERVAL 0.25 \ No newline at end of file diff --git a/src/utils/include/Utils.h b/src/utils/include/utils.h similarity index 79% rename from src/utils/include/Utils.h rename to src/utils/include/utils.h index 852c033f..4534a8de 100644 --- a/src/utils/include/Utils.h +++ b/src/utils/include/utils.h @@ -17,6 +17,7 @@ struct Offset { const DllExport std::string CurrentDateTime(); std::wstring DllExport to_wstring(const std::string& str); std::string DllExport to_string(const std::wstring& wstr); +std::string DllExport random_string(size_t length); bool DllExport operator== (const Coords& a, const Coords& b); bool DllExport operator!= (const Coords& a, const Coords& b); @@ -27,3 +28,9 @@ bool DllExport operator== (const Offset& a, const Offset& b); bool DllExport operator!= (const Offset& a, const Offset& b); bool DllExport operator== (const Offset& a, const int& b); bool DllExport operator!= (const Offset& a, const int& b); + +double DllExport knotsToMs(const double knots); +double DllExport msToKnots(const double ms); +double DllExport ftToM(const double ft); +double DllExport mToFt(const double m); + diff --git a/src/utils/src/Utils.cpp b/src/utils/src/utils.cpp similarity index 73% rename from src/utils/src/Utils.cpp rename to src/utils/src/utils.cpp index d471028b..9d91fbe7 100644 --- a/src/utils/src/Utils.cpp +++ b/src/utils/src/utils.cpp @@ -38,6 +38,22 @@ std::string to_string(const std::wstring& wstr) return result; } +std::string random_string(size_t length) +{ + auto randchar = []() -> char + { + const char charset[] = + "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + const size_t max_index = (sizeof(charset) - 1); + return charset[rand() % max_index]; + }; + std::string str(length, 0); + std::generate_n(str.begin(), length, randchar); + return str; +} + bool operator== (const Coords& a, const Coords& b) { return a.lat == b.lat && a.lng == b.lng && a.alt == b.alt; } bool operator!= (const Coords& a, const Coords& b) { return !(a == b); } bool operator== (const Coords& a, const int& b) { return a.lat == b && a.lng == b && a.alt == b; } @@ -47,3 +63,20 @@ bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == bool operator!= (const Offset& a, const Offset& b) { return !(a == b); } bool operator== (const Offset& a, const int& b) { return a.x == b && a.y == b && a.z == b; } bool operator!= (const Offset& a, const int& b) { return !(a == b); } + + +double knotsToMs(const double knots) { + return knots / 1.94384; +} + +double msToKnots(const double ms) { + return ms * 1.94384; +} + +double ftToM(const double ft) { + return ft * 0.3048; +} + +double mToFt(const double m) { + return m / 0.3048; +} \ No newline at end of file diff --git a/third-party/base64/include/base64.hpp b/third-party/base64/include/base64.hpp new file mode 100644 index 00000000..adcf1c04 --- /dev/null +++ b/third-party/base64/include/base64.hpp @@ -0,0 +1,81 @@ +#ifndef BASE_64_HPP +#define BASE_64_HPP + +#include +#include + +namespace base64 { + +inline std::string get_base64_chars() { + static std::string base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + return base64_chars; +} + +inline std::string to_base64(std::string const &data) { + int counter = 0; + uint32_t bit_stream = 0; + const std::string base64_chars = get_base64_chars(); + std::string encoded; + int offset = 0; + for (unsigned char c : data) { + auto num_val = static_cast(c); + offset = 16 - counter % 3 * 8; + bit_stream += num_val << offset; + if (offset == 16) { + encoded += base64_chars.at(bit_stream >> 18 & 0x3f); + } + if (offset == 8) { + encoded += base64_chars.at(bit_stream >> 12 & 0x3f); + } + if (offset == 0 && counter != 3) { + encoded += base64_chars.at(bit_stream >> 6 & 0x3f); + encoded += base64_chars.at(bit_stream & 0x3f); + bit_stream = 0; + } + counter++; + } + if (offset == 16) { + encoded += base64_chars.at(bit_stream >> 12 & 0x3f); + encoded += "=="; + } + if (offset == 8) { + encoded += base64_chars.at(bit_stream >> 6 & 0x3f); + encoded += '='; + } + return encoded; +} + +inline std::string from_base64(std::string const &data) { + int counter = 0; + uint32_t bit_stream = 0; + std::string decoded; + int offset = 0; + const std::string base64_chars = get_base64_chars(); + for (unsigned char c : data) { + auto num_val = base64_chars.find(c); + if (num_val != std::string::npos) { + offset = 18 - counter % 4 * 6; + bit_stream += num_val << offset; + if (offset == 12) { + decoded += static_cast(bit_stream >> 16 & 0xff); + } + if (offset == 6) { + decoded += static_cast(bit_stream >> 8 & 0xff); + } + if (offset == 0 && counter != 4) { + decoded += static_cast(bit_stream & 0xff); + bit_stream = 0; + } + } else if (c != '=') { + return std::string(); + } + counter++; + } + return decoded; +} + +} + +#endif // BASE_64_HPP \ No newline at end of file