diff --git a/client/TODO.txt b/client/TODO.txt deleted file mode 100644 index d4f06408..00000000 --- a/client/TODO.txt +++ /dev/null @@ -1,7 +0,0 @@ -RTB -tanker -scenario dropdown -explosion -wrong name for ground units -improve map zIndex -human symbol if user diff --git a/client/app.js b/client/app.js index 7c2ad473..8fbd41c1 100644 --- a/client/app.js +++ b/client/app.js @@ -3,6 +3,7 @@ var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var fs = require('fs'); +var basicAuth = require('express-basic-auth') var atcRouter = require('./routes/api/atc'); var indexRouter = require('./routes/index'); @@ -26,7 +27,8 @@ app.set('view engine', 'ejs'); let rawdata = fs.readFileSync('../olympus.json'); let config = JSON.parse(rawdata); -app.get('/config', (req, res) => res.send(config)); +if (config["server"] != undefined) + app.get('/config', (req, res) => res.send(config["server"])); module.exports = app; @@ -38,3 +40,8 @@ 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/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 index c30f7f7b..2c113e9f 100644 --- a/client/demo.js +++ b/client/demo.js @@ -2,7 +2,7 @@ const DEMO_UNIT_DATA = { ["1"]:{ baseData: { - AI: true, + AI: false, name: "KC-135", unitName: "Olympus 1-1", groupName: "Group 1", @@ -18,7 +18,7 @@ const DEMO_UNIT_DATA = { }, missionData: { fuel: 50, - flags: {human: true}, + flags: {Human: false}, ammo: [ { count: 4, @@ -47,6 +47,7 @@ const DEMO_UNIT_DATA = { }, taskData: { currentTask: "Holding", + currentState: "Idle", activePath: undefined, targetSpeed: 400, targetAltitude: 3000, @@ -67,7 +68,7 @@ const DEMO_UNIT_DATA = { }, ["2"]:{ baseData: { - AI: false, + AI: true, name: "KC-135", unitName: "Olympus 1-2", groupName: "Group 1", diff --git a/client/package-lock.json b/client/package-lock.json index e7858b9a..55970983 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "DCSOlympus", - "version": "v0.2.0-alpha", + "version": "v0.2.1-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "DCSOlympus", - "version": "v0.2.0-alpha", + "version": "v0.2.1-alpha", "dependencies": { "@types/geojson": "^7946.0.10", "@types/leaflet": "^1.9.0", @@ -31,6 +31,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", @@ -3283,6 +3284,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", @@ -8148,6 +8158,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", diff --git a/client/package.json b/client/package.json index 40823578..fce35cdf 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,7 @@ "name": "DCSOlympus", "node-main": "./bin/www", "main": "http://localhost:3000", - "version": "v0.2.0-alpha", + "version": "v0.2.1-alpha", "private": true, "scripts": { "copy": "copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet.css", @@ -33,6 +33,7 @@ "browserify": "^17.0.0", "concurrently": "^7.6.0", "esmify": "^2.1.1", + "express-basic-auth": "^1.2.1", "nodemon": "^2.0.20", "sortablejs": "^1.15.0", "tsify": "^5.0.4", 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/icon-temporary.png b/client/public/images/icon-temporary.png new file mode 100644 index 00000000..712221a0 Binary files /dev/null and b/client/public/images/icon-temporary.png differ diff --git a/client/public/stylesheets/contextmenus.css b/client/public/stylesheets/contextmenus.css index 606f197f..573bba4b 100644 --- a/client/public/stylesheets/contextmenus.css +++ b/client/public/stylesheets/contextmenus.css @@ -70,8 +70,6 @@ align-self: stretch; } - - #aircraft-spawn-menu .ol-select.is-open .ol-select-options { max-height: 300px; } @@ -131,6 +129,16 @@ background-color: var(--primary-red) } +[data-active-coalition="neutral"].toggle-fill, +[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); @@ -141,6 +149,21 @@ 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"].toggle-fill::after { + transform: translateX(0); +} +[data-active-coalition="red"].toggle-fill::after { + transform: translateX(var(--height)); +} +[data-active-coalition="neutral"].toggle-fill::after { + transform: translateX(calc(var(--height) / 2)); +} [data-active-coalition="blue"]#active-coalition-label::after { content: "Create blue unit"; @@ -148,6 +171,9 @@ [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 { display: flex; @@ -167,6 +193,7 @@ #unit-image { width: 100px; + height: 100px; filter: invert(100%); margin-top: 10px; margin-bottom: 10px; @@ -329,4 +356,39 @@ row-gap: 5px; width: 180px; z-index: 1000; -} \ No newline at end of file +} + +.toggle { + --width: 40px; + --height: calc(var(--width) / 2); + --border-radius: calc(var(--height) / 2); + + display: inline-block; + cursor: pointer; +} + +.toggle-input { + display: none; +} + +.toggle-fill { + position: relative; + width: var(--width); + height: var(--height); + border-radius: var(--border-radius); + transition: background-color 0.2s; +} + +.toggle-fill::after { + content: ""; + position: absolute; + top: 2; + left: 2; + height: calc(var(--height) - 4px); + width: calc(var(--height) - 4px); + background-color: #ffffff; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); + border-radius: var(--border-radius); + transition: transform 0.2s; +} + diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index ff8e8a54..5c60d1bb 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -16,12 +16,13 @@ #olympus-toolbar-summary { background-image: url("/images/icon-round.png"); - background-position: 25px 20px; + background-position: 20px 22px; background-repeat: no-repeat; - background-size: 36px 36px; + background-size: 45px 45px; display: flex; flex-direction: column; - text-indent: 44px; + text-indent: 60px; + padding: 20px; } dl.ol-data-grid { diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 93a6080d..15a5578c 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -16,24 +16,22 @@ box-sizing: border-box; } - -html { - font-family: 'Open Sans', sans-serif; +html * { + font-family: 'Open Sans', sans-serif !important; } body { - display:grid; - margin: 0; - padding: 0; + display: grid; + margin: 0; + padding: 0; } html, body { - height: 100%; - width: 100%; + height: 100%; + width: 100%; } - a { text-decoration: none; } @@ -42,7 +40,6 @@ a:hover { text-decoration: underline; } - button { background-color: var(--background-steel); border: 1px solid var(--background-steel); @@ -67,26 +64,40 @@ form { padding: 0; } -form > div { +form>div { margin: 20px 0; } - .pill { - background-color: var( --background-dark-steel ); + 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: 10px; + width: var(--border-radius-md); } - + .ol-scrollable::-webkit-scrollbar-track { background-color: transparent; - border-radius: 100px; + border-top-right-radius: 10px; + border-bottom-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 { @@ -96,7 +107,6 @@ form > div { margin-top: 10px; } - .ol-panel { background-color: var(--background-steel); border-radius: 15px; @@ -124,6 +134,14 @@ form > div { width: 100%; } +.ol-ellipsed { + display: inline-block; + width: calc(100%); + overflow: hidden; + text-overflow: ellipsis; + text-align: left; +} + .ol-select { position: relative; color: var(--nav-text); @@ -138,6 +156,7 @@ form > div { text-align: center; white-space: nowrap; width: 100%; + min-width: 0; } .ol-select:not(.ol-select-image)>.ol-select-value { @@ -145,14 +164,14 @@ form > div { background-color: var(--background-grey); border-radius: var(--border-radius-sm); padding: 1em 30px 1em 20px; - width: 100%; + width: calc(100%); overflow: hidden; text-overflow: ellipsis; } .ol-select.narrow:not(.ol-select-image)>.ol-select-value { opacity: .9; - padding:4px 30px 4px 15px; + padding: 4px 30px 4px 15px; } .ol-select:not(.ol-select-image)>.ol-select-value svg { @@ -166,55 +185,51 @@ form > div { } .ol-select>.ol-select-options { - border-radius: var( --border-radius-md ); + border-radius: var(--border-radius-md); overflow: hidden; position: absolute; max-height: 0; - translate: 0 -2px; z-index: 1000; } +.ol-select-options.scrollbar-visible { + border-top-right-radius: 0px !important; + border-bottom-right-radius: 0px !important; +} + .ol-select.ol-select-image>.ol-select-options { position: absolute; } - -.ol-select.is-open > .ol-select-options { +.ol-select.is-open>.ol-select-options { max-height: 382px; overflow: visible; overflow-y: auto; - padding: 8px 0; min-width: 100%; - z-index:9999; + z-index: 9999; + translate: 0px 5px; } - -.ol-select.is-open[data-position="top"] > .ol-select-options { - top:0; - translate:0 -100%; +.ol-select.is-open[data-position="top"]>.ol-select-options { + top: 0; + translate: 0 -100%; } - - -.ol-select>.ol-select-options > div { +.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: 4px 25px; + padding: 2px 10px; width: 100%; } .ol-select>.ol-select-options>div:first-of-type { - border-top-left-radius: var(--border-radius-md); - border-top-right-radius: var(--border-radius-md); - padding-top: 16px; + padding-top: 12px; } .ol-select>.ol-select-options>div:last-of-type { - border-bottom-left-radius: var(--border-radius-md); - border-bottom-right-radius: var(--border-radius-md); - padding-bottom: 16px; + padding-bottom: 12px; } .ol-select>.ol-select-options div hr { @@ -223,25 +238,29 @@ form > div { width: 100%; } -.ol-select>.ol-select-options > div a, .ol-select>.ol-select-options > div button { +.ol-select>.ol-select-options>div a, +.ol-select>.ol-select-options>div button { background-color: transparent; border: none; border-radius: 0; color: white; - display:block; - font-size: 14px; + display: block; + font-size: 12px; font-weight: normal; padding: 6px 2px; text-align: left; white-space: nowrap; width: 100%; + padding: 5px; + border-radius: var(--border-radius-sm); } -.ol-select>.ol-select-options > div a:hover, .ol-select>.ol-select-options > div button:hover { - text-decoration: underline; +.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; @@ -275,32 +294,30 @@ form > div { max-width: 16px; } - .ol-panel-board { display: flex; flex-direction: row; justify-content: space-evenly; } -.ol-panel-board > .panel-section { +.ol-panel-board>.panel-section { border-right: 1px solid #555; - margin:10px 0; - padding:0 30px; + margin: 10px 0; + padding: 0 30px; } -.ol-panel-board > .panel-section:first-child { - padding-left:20px; +.ol-panel-board>.panel-section:first-child { + padding-left: 20px; } -.ol-panel-board > .panel-section:last-child { - padding-right:20px; +.ol-panel-board>.panel-section:last-child { + padding-right: 20px; } -.ol-panel-board > .panel-section:last-of-type { +.ol-panel-board>.panel-section:last-of-type { border-right-width: 0; } - h1, h2, h3, @@ -335,7 +352,6 @@ button.ol-button-warning { color: var(--primary-red); } - nav.ol-panel { column-gap: 20px; display: flex; @@ -347,7 +363,6 @@ nav.ol-panel> :last-child { margin-right: 5px; } - .ol-panel .ol-group { border-radius: var(--border-radius-sm); column-gap: 10px; @@ -367,7 +382,6 @@ nav.ol-panel> :last-child { flex-wrap: wrap; } - .ol-panel .ol-group-button-toggle { align-items: center; column-gap: 15px; @@ -381,7 +395,7 @@ nav.ol-panel> :last-child { background-position: 5px 50%; background-repeat: no-repeat; border: 0; - display:flex; + display: flex; justify-items: left; text-indent: 5px; } @@ -392,38 +406,36 @@ nav.ol-panel> :last-child { content: ""; filter: invert(100%); -webkit-filter: invert(100%); - height:16px; - width:16px; + height: 16px; + width: 16px; } - .ol-panel .ol-group-button-toggle button.off::before { background-image: url("/images/icons/square-regular.svg"); } - - - .highlight-primary { background-color: var(--secondary-light-grey); } -.highlight-coalition, .highlight-neutral { - background-color: var(--primary-grey); +.highlight-coalition, +.highlight-neutral { + background-color: var(--primary-neutral); color: var(--secondary-gunmetal-grey) } -.highlight-coalition[data-coalition="blue"], .highlight-bluefor { +.highlight-coalition[data-coalition="blue"], +.highlight-bluefor { background-color: var(--primary-blue); color: white; } -.highlight-coalition[data-coalition="red"], .highlight-redfor { +.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); @@ -445,7 +457,7 @@ nav.ol-panel> :last-child { } .accent-neutral { - color: var(--primary-grey); + color: var(--primary-neutral); font-weight: var(--font-weight-bolder); } @@ -464,7 +476,6 @@ nav.ol-panel> :last-child { flex-direction: column; } - .slider-container { width: 100%; } @@ -529,23 +540,20 @@ nav.ol-panel> :last-child { width: fit-content; height: fit-content; text-align: center; - color: var(--primary-grey); + color: var(--primary-neutral); font-size: 12px; z-index: 2000; font-weight: var(--font-weight-bolder); } - .ol-sortable .handle { - background-image: url( "/images/icons/grip-lines-solid.svg" ); - cursor:ns-resize; - filter:invert(); - height:12px; - width:12px; + background-image: url("/images/icons/grip-lines-solid.svg"); + cursor: ns-resize; + filter: invert(); + height: 12px; + width: 12px; } - - #unit-selection { display: flex; flex-direction: column; @@ -621,223 +629,264 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit { #atc-navbar-control { align-items: center; - display:flex; + display: flex; flex-direction: column; } #atc-navbar-control button { - background:#ffffff20; - border-radius: var( --border-radius-sm ); - padding:4px; + background: #ffffff20; + border-radius: var(--border-radius-sm); + padding: 4px; } - -.toggle { - --width: 40px; - --height: calc(var(--width) / 2); - --border-radius: calc(var(--height) / 2); - - display: inline-block; - cursor: pointer; -} - -.toggle-input { - display: none; -} - -.toggle-fill { - position: relative; - width: var(--width); - height: var(--height); - border-radius: var(--border-radius); - transition: background-color 0.2s; -} - -.toggle-fill::after { - content: ""; - position: absolute; - top: 2; - left: 2; - height: calc(var(--height) - 4px); - width: calc(var(--height) - 4px); - background-color: #ffffff; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); - border-radius: var(--border-radius); - transition: transform 0.2s; -} - -.toggle-input:checked ~ .toggle-fill::after { - transform: translateX(var(--height)); -} - - #roe-buttons-container button { - background-color:transparent; - border:1px solid var( --accent-light-blue ); + background-color: transparent; + border: 1px solid var(--accent-light-blue); } -#roe-buttons-container button.selected, #reaction-to-threat-buttons-container button.selected { +#roe-buttons-container button.selected, +#reaction-to-threat-buttons-container button.selected { background-color: white; border-color: white; } -#roe-buttons-container button::before, #reaction-to-threat-buttons-container button::before { - background-position:center; +#roe-buttons-container button::before, +#reaction-to-threat-buttons-container button::before { + background-position: center; background-repeat: no-repeat; content: ""; - display:block; - height:24px; - width:24px; + display: block; + height: 24px; + width: 24px; } - - - #roe-buttons-container button[title="Hold"]::before { - background-image: url( "/themes/olympus/images/icons_roe_stop_light.svg"); + background-image: url("/themes/olympus/images/icons_roe_stop_light.svg"); } #roe-buttons-container button[title="Hold"].selected::before { - background-image: url( "/themes/olympus/images/icons_roe_stop_dark.svg"); + background-image: url("/themes/olympus/images/icons_roe_stop_dark.svg"); } /**/ - #roe-buttons-container button[title="Return"]::before { - background-image: url( "/themes/olympus/images/icons_roe_defend_light.svg"); + background-image: url("/themes/olympus/images/icons_roe_defend_light.svg"); } #roe-buttons-container button[title="Return"].selected::before { - background-image: url( "/themes/olympus/images/icons_roe_defend_dark.svg"); + background-image: url("/themes/olympus/images/icons_roe_defend_dark.svg"); } /**/ - #roe-buttons-container button[title="Designated"]::before { - background-image: url( "/themes/olympus/images/icons_roe_target_light.svg"); + background-image: url("/themes/olympus/images/icons_roe_target_light.svg"); } #roe-buttons-container button[title="Designated"].selected::before { - background-image: url( "/themes/olympus/images/icons_roe_target_dark.svg"); + background-image: url("/themes/olympus/images/icons_roe_target_dark.svg"); } /**/ - #roe-buttons-container button[title="Free"]::before { - background-image: url( "/themes/olympus/images/icons_roe_free_light.svg"); + background-image: url("/themes/olympus/images/icons_roe_free_light.svg"); } #roe-buttons-container button[title="Free"].selected::before { - background-image: url( "/themes/olympus/images/icons_roe_free_dark.svg"); + background-image: url("/themes/olympus/images/icons_roe_free_dark.svg"); } - /****************************************************************************************/ - - #reaction-to-threat-buttons-container button[title="None"]::before { - background-image: url( "/themes/olympus/images/icons_threat_nothing_light.svg"); + background-image: url("/themes/olympus/images/icons_threat_nothing_light.svg"); } #reaction-to-threat-buttons-container button[title="None"].selected::before { - background-image: url( "/themes/olympus/images/icons_threat_nothing_dark.svg"); + background-image: url("/themes/olympus/images/icons_threat_nothing_dark.svg"); } - /**/ - - #reaction-to-threat-buttons-container button[title="Passive"]::before { - background-image: url( "/themes/olympus/images/icons_threat_cms_light.svg"); + background-image: url("/themes/olympus/images/icons_threat_cms_light.svg"); } #reaction-to-threat-buttons-container button[title="Passive"].selected::before { - background-image: url( "/themes/olympus/images/icons_threat_cms_dark.svg"); + background-image: url("/themes/olympus/images/icons_threat_cms_dark.svg"); } - /**/ - - #reaction-to-threat-buttons-container button[title="Evade"]::before { - background-image: url( "/themes/olympus/images/icons_threat_defend_light.svg"); + background-image: url("/themes/olympus/images/icons_threat_defend_light.svg"); } #reaction-to-threat-buttons-container button[title="Evade"].selected::before { - background-image: url( "/themes/olympus/images/icons_threat_defend_dark.svg"); + background-image: url("/themes/olympus/images/icons_threat_defend_dark.svg"); } - /****************************************************************************************/ - - #splash-screen { - background-image: url( "/images/splash/splash_pic_ship.png" ); - background-position:100% 50%; - background-size:320px; - border-radius: var( --border-radius-lg ); - display:none; + background-image: url("/images/splash/splash_pic_ship.png"); + background-position: 100% 50%; + background-size: 60%; + border-radius: var(--border-radius-lg); overflow: hidden; - width:700px; + width: 1200px; + z-index: 99999; } #splash-content { - background-color: var( --background-steel ); + background-color: var(--background-steel); display: flex; flex-direction: column; - padding:20px; - position:relative; - row-gap:10px; - width:55%; - z-index:10; + padding: 30px; + position: relative; + row-gap: 10px; + width: 50%; + z-index: 10; } #splash-content::after { - background-color: var( --background-steel ); + background-color: var(--background-steel); content: ""; display: block; - height:250px; + height: 800px; position: absolute; - right:0; - top:0; + right: 0; + top: 0; transform: rotate(-23deg); transform-origin: top right; - width:200px; + width: 200px; z-index: -1; } #splash-content #app-summary { - background-image: url( "/images/olympus-500x500.png" ); + background-image: url("/images/olympus-500x500.png"); background-position: 0 50%; background-repeat: no-repeat; - background-size:75px 75px; + background-size: 75px 75px; content: ""; - display:flex; + display: flex; flex-direction: column; justify-content: space-between; min-height: 75px; text-indent: 85px; } -#splash-content #app-summary > * { - height:fit-content; +#splash-content #app-summary>* { + height: fit-content; line-height: 25px; white-space: nowrap; - width:fit-content; + width: fit-content; + padding: 2px; } #splash-content .app-version { - font-size:11px; + font-size: 11px; } -#splash-content #legal-stuff h4 { - text-transform:uppercase; +#splash-content #legal-stuff h5 { + text-transform: uppercase; } #splash-content #legal-stuff p { - font-size:10px; + font-size: 10px; + color:#FFF7; + width: 120%; +} + +#splash-content.ol-dialog-content { + margin: 0px; } .feature-splashScreen #splash-screen { - display:flex; + display: flex; +} + +#gray-out { + position: fixed; + height: 100%; + width: 100%; + left: 0px; + top: 0px; + z-index: 9999; + background-color: #000A; +} + +#authentication-form { + display: flex; + align-items: end; + column-gap: 10px; + margin: 10px 0px; + flex-direction: row; +} + +#authentication-form>div { + display: flex; + align-items: start; + row-gap: 4px; + flex-direction: column; +} + +#authentication-form>div>input { + height: 35px; + border-radius: var(--border-radius-sm); + border: 0px solid transparent; + width: 200px; +} + +#splash-content a { + color: #FFFB; + font-weight: bold; +} + +#connection-status { + margin-bottom: 5px; +} + +#connection-status[data-status="connecting"]::before { + content: "Connecting..."; + animation: blinker 1s linear infinite; +} + +#connection-status[data-status="failed"]::before { + content: "Incorrect username/password!"; + color: var(--primary-red); +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + +#hotgroup-panel { + position: absolute; + bottom: 40px; + left: 50%; + translate: -50%; + z-index: 9999; + display: flex; + column-gap: 10px; +} + +#hotgroup-panel>div { + width: 50px; + height: 50px; + background-color: var(--background-steel); + border-radius: var(--border-radius-sm); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + border: 0px solid transparent; +} + +#hotgroup-panel>div:hover { + cursor: pointer; + border: 2px solid white; +} + +.hotgroup-selector>.unit-hotgroup { + display: flex; + translate: 0% -300%; } \ No newline at end of file diff --git a/client/public/stylesheets/unitcontrolpanel.css b/client/public/stylesheets/unitcontrolpanel.css index ba50dc42..85e5bf05 100644 --- a/client/public/stylesheets/unitcontrolpanel.css +++ b/client/public/stylesheets/unitcontrolpanel.css @@ -1,5 +1,5 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { - display:block !important; + display: block !important; } @@ -13,96 +13,96 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { } #unit-control-panel h3 { - margin-bottom:8px; + margin-bottom: 8px; } #unit-control-panel #selected-units-container { align-items: left; - border-radius: var( --border-radius-md ); - display:flex; + border-radius: var(--border-radius-md); + display: flex; flex-direction: column; max-height: 136px; - overflow-y:auto; + overflow-y: auto; row-gap: 4px; } #unit-control-panel #selected-units-container button { align-items: center; - border-radius: var( --border-radius-lg ); - display:flex; + border-radius: var(--border-radius-lg); + display: flex; font-size: 11px; - height:30px; - padding:8px 0; + height: 30px; + padding: 8px 0; position: relative; - width:90%; + width: 100%; } #unit-control-panel #selected-units-container button::before { - background-color: var( --primary-grey ); - border-radius: var( --border-radius-md ); - content: attr( data-short-label ); - margin:0 5px; - padding:4px 6px; + background-color: var( --primary-neutral ); + border-radius: 999px; + content: attr(data-short-label); + margin: 0 5px; + padding: 4px 6px; white-space: nowrap; - width:fit-content; + width: 30px; + text-overflow: ellipsis; + overflow: hidden; } - - #unit-control-panel #selected-units-container button[data-coalition="blue"]::before { - background-color: var( --accent-light-blue ); + background-color: var(--accent-light-blue); } #unit-control-panel #selected-units-container button[data-coalition="red"]::before { - background-color: var( --accent-light-red ); - color:var(--secondary-red-outline) + background-color: var(--accent-light-red); + color: var(--secondary-red-outline) } #unit-control-panel #selected-units-container button::after { - border-radius: var( --border-radius-sm ); - content: attr( data-callsign ); + border-radius: var(--border-radius-sm); + content: attr(data-callsign); display: block; overflow: hidden; - padding:4px; - padding-left:0; + padding: 4px; + padding-left: 0; text-align: left; text-overflow: ellipsis; white-space: nowrap; - width:fit-content; + width: fit-content; } #unit-control-panel #selected-units-container button:hover::after { overflow: visible; - text-overflow:initial; + text-overflow: initial; } #unit-control-panel #selected-units-container button:hover::after { - background-color: var( --background-grey ); + background-color: var(--background-grey); } #unit-control-panel #selected-units-container button[data-coalition="blue"]:hover::after { - background-color: var( --primary-blue ); + background-color: var(--primary-blue); } #unit-control-panel #selected-units-container button[data-coalition="red"]:hover::after { - background-color: var( --primary-red ); + background-color: var(--primary-red); } #unit-control-panel h4 { - margin-bottom:8px; + margin-bottom: 8px; } #unit-control-panel #threat, #unit-control-panel #roe { - margin-top:12px; + margin-top: 12px; } #advanced-settings-dialog { width: 400px; } -#advanced-settings-dialog > .ol-dialog-content { +#advanced-settings-dialog>.ol-dialog-content { margin-top: 10px; margin-bottom: 10px; display: flex; @@ -111,6 +111,6 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { row-gap: 10px; } -#advanced-settings-dialog > .ol-dialog-content > .ol-group { +#advanced-settings-dialog>.ol-dialog-content>.ol-group { justify-content: space-between; } \ No newline at end of file diff --git a/client/public/stylesheets/units.css b/client/public/stylesheets/units.css index 0685381f..b48389d6 100644 --- a/client/public/stylesheets/units.css +++ b/client/public/stylesheets/units.css @@ -1,79 +1,80 @@ :root { - /* Generic marker settings */ - --unit-centre-x: calc( var( --unit-width ) / 2 ); - --unit-centre-y: calc( var( --unit-height ) / 2 ); - - --unit-hotgroup-height: 10px; - --unit-hotgroup-width: var( --unit-hotgroup-height ); - - - /* Air units' marker settings */ - --unit-aircraft-label-x: calc( var( --unit-centre-x ) - ( var( --unit-aircraft-width ) / 2 ) + ( var( --unit-stroke-width ) / 2 ) ); - --unit-aircraft-label-y: calc( var( --unit-centre-y ) - ( var( --unit-aircraft-height ) / 2 ) + ( var( --unit-stroke-width ) / 2 ) ); + /* Generic marker settings */ + --unit-centre-x: calc(var(--unit-width) / 2); + --unit-centre-y: calc(var(--unit-height) / 2); + + --unit-hotgroup-height: 15px; + --unit-hotgroup-width: var(--unit-hotgroup-height); + + + /* Air units' marker settings */ + --unit-aircraft-label-x: calc(var(--unit-centre-x) - (var(--unit-aircraft-width) / 2) + (var(--unit-stroke-width) / 2)); + --unit-aircraft-label-y: calc(var(--unit-centre-y) - (var(--unit-aircraft-height) / 2) + (var(--unit-stroke-width) / 2)); } [data-object|="unit"] { align-items: center; - cursor:pointer; - display:flex; + cursor: pointer; + display: flex; justify-content: center; - position:relative; + position: relative; + height: 100%; + width: 100%; } -[data-object|="unit"] .unit-selected-spotlight { - background-color: var( --unit-spotlight-fill ); +.unit-selected-spotlight { + background-color: var(--unit-spotlight-fill); border-radius: 50%; - display:none; - padding: var( --unit-spotlight-radius ); + display: none; + padding: var(--unit-spotlight-radius); position: absolute; - z-index:1; + z-index: 1; } - -[data-object|="unit"] .unit-vvi { +.unit-vvi { align-self: center; - background:var( --secondary-gunmetal-grey ); - display:flex; + background: var(--secondary-gunmetal-grey); + display: flex; justify-self: center; transform-origin: bottom; - translate:0 -50%; - padding-bottom: calc( ( var( --unit-aircraft-width ) / 2 ) + var( --unit-stroke-width ) ); - position:absolute; - width: var( --unit-aircraft-vvi-width ); + translate: 0 -50%; + padding-bottom: calc((var(--unit-aircraft-width) / 2) + var(--unit-stroke-width)); + position: absolute; + width: var(--unit-aircraft-vvi-width); z-index: 3; } +.unit-marker-border { + border-radius: var(--border-radius-sm); + display: none; + height: calc(var(--unit-aircraft-height) + (var(--unit-label-border-width) * 2)); + position: absolute; + width: calc(var(--unit-aircraft-width) + (var(--unit-label-border-width) * 2)); + z-index: 2; +} -[data-object|="unit"] .unit-hotgroup { +.unit-hotgroup { align-content: center; - background-color: black; - border-radius: var( --border-radius-xs ); - display:none; - height: var( --unit-hotgroup-height ); + background-color: var(--background-steel); + border-radius: var(--border-radius-xs); + display: none; + height: var(--unit-hotgroup-height); justify-content: center; - position:absolute; - transform: rotate( -45deg ); - translate:0 -275%; - width: var( --unit-hotgroup-width ); + position: absolute; + transform: rotate(-45deg); + translate: 0 -200%; + width: var(--unit-hotgroup-width); z-index: 5; } -[data-object|="unit"] .unit-hotgroup-id { +.unit-hotgroup-id { background-color: transparent; - color:white; + color: white; font-size: 9px; font-weight: bolder; - transform:rotate( 45deg ); -} - -[data-object|="unit"] .unit-marker-border { - border-radius: var( --border-radius-sm ); - display:none; - height: calc( var( --unit-aircraft-height ) + ( var( --unit-label-border-width ) * 2 ) ); - position:absolute; - width: calc( var( --unit-aircraft-width ) + ( var( --unit-label-border-width ) * 2 ) ); - z-index:2; + transform: rotate(45deg); + translate: -1px 1px; } @@ -85,249 +86,241 @@ background-color: transparent; background-repeat: no-repeat; background-size: cover; - position:absolute; + position: absolute; transform-origin: center; - z-index:3; + z-index: 3; } - - /* Air */ [data-object|="unit-aircraft"] .unit-marker { - background-image: var( --unit-aircraft-marker-neutral-url ); - height: var( --unit-aircraft-marker-height ); - width: var( --unit-aircraft-marker-width ); + background-image: var(--unit-aircraft-marker-neutral-url); + height: var(--unit-aircraft-marker-height); + width: var(--unit-aircraft-marker-width); } -[data-object|="unit-aircraft"]:hover .unit-marker { - background-image: var( --unit-aircraft-marker-neutral-hover-url ); +[data-object|="unit-aircraft"][data-is-highlighted] .unit-marker { + background-image: var(--unit-aircraft-marker-neutral-hover-url); } [data-object|="unit-aircraft"][data-is-selected] .unit-marker { - background-image: var( --unit-aircraft-marker-neutral-selected-url ); + background-image: var(--unit-aircraft-marker-neutral-selected-url); } - [data-object|="unit-aircraft"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-aircraft-marker-blue-url ); + background-image: var(--unit-aircraft-marker-blue-url); } -[data-object|="unit-aircraft"][data-coalition="blue"]:hover .unit-marker { - background-image: var( --unit-aircraft-marker-blue-hover-url ); +[data-object|="unit-aircraft"][data-coalition="blue"][data-is-highlighted] .unit-marker { + background-image: var(--unit-aircraft-marker-blue-hover-url); } [data-object|="unit-aircraft"][data-coalition="blue"][data-is-selected] .unit-marker { - background-image: var( --unit-aircraft-marker-blue-selected-url ); + background-image: var(--unit-aircraft-marker-blue-selected-url); } [data-object|="unit-aircraft"][data-coalition="red"] .unit-marker { - background-image: var( --unit-aircraft-marker-red-url ); + background-image: var(--unit-aircraft-marker-red-url); } -[data-object|="unit-aircraft"][data-coalition="red"]:hover .unit-marker { - background-image: var( --unit-aircraft-marker-red-hover-url ); +[data-object|="unit-aircraft"][data-coalition="red"][data-is-highlighted] .unit-marker { + background-image: var(--unit-aircraft-marker-red-hover-url); } [data-object|="unit-aircraft"][data-coalition="red"][data-is-selected] .unit-marker { - background-image: var( --unit-aircraft-marker-red-selected-url ); + background-image: var(--unit-aircraft-marker-red-selected-url); } - - - /* Ground vehicles (not SAMs) */ [data-object|="unit-groundunit"] .unit-marker { - background-image: var( --unit-groundunit-marker-neutral-url ); - height: var( --unit-groundunit-marker-height ); - width: var( --unit-groundunit-marker-width ); + background-image: var(--unit-groundunit-marker-neutral-url); + height: var(--unit-groundunit-marker-height); + width: var(--unit-groundunit-marker-width); } -[data-object|="unit-groundunit"]:hover .unit-marker { - background-image: var( --unit-groundunit-marker-neutral-hover-url ); +[data-object|="unit-groundunit"][data-is-highlighted] .unit-marker { + background-image: var(--unit-groundunit-marker-neutral-hover-url); } [data-object|="unit-groundunit"][data-is-selected] .unit-marker { - background-image: var( --unit-groundunit-marker-neutral-selected-url ); + background-image: var(--unit-groundunit-marker-neutral-selected-url); } [data-object|="unit-groundunit"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-groundunit-marker-blue-url ); + background-image: var(--unit-groundunit-marker-blue-url); } -[data-object|="unit-groundunit"][data-coalition="blue"]:hover .unit-marker { - background-image: var( --unit-groundunit-marker-blue-hover-url ); +[data-object|="unit-groundunit"][data-coalition="blue"][data-is-highlighted] .unit-marker { + background-image: var(--unit-groundunit-marker-blue-hover-url); } [data-object|="unit-groundunit"][data-coalition="blue"][data-is-selected] .unit-marker { - background-image: var( --unit-groundunit-marker-blue-selected-url ); + background-image: var(--unit-groundunit-marker-blue-selected-url); } [data-object|="unit-groundunit"][data-coalition="red"] .unit-marker { - background-image: var( --unit-groundunit-marker-red-url ); + background-image: var(--unit-groundunit-marker-red-url); } -[data-object|="unit-groundunit"][data-coalition="red"]:hover .unit-marker { - background-image: var( --unit-groundunit-marker-red-hover-url ); +[data-object|="unit-groundunit"][data-coalition="red"][data-is-highlighted] .unit-marker { + background-image: var(--unit-groundunit-marker-red-hover-url); } [data-object|="unit-groundunit"][data-coalition="red"][data-is-selected] .unit-marker { - background-image: var( --unit-groundunit-marker-red-selected-url ); + background-image: var(--unit-groundunit-marker-red-selected-url); } /* SAMs */ [data-object|="unit-sam"] .unit-selected-spotlight { - translate:0 2px; + translate: 0 2px; } [data-object|="unit-sam"] .unit-marker { - background-image: var( --unit-sam-marker-neutral-url ); - height: var( --unit-sam-marker-height ); - width: var( --unit-sam-marker-width ); + background-image: var(--unit-sam-marker-neutral-url); + height: var(--unit-sam-marker-height); + width: var(--unit-sam-marker-width); } -[data-object|="unit-sam"]:hover .unit-marker { - background-image: var( --unit-sam-marker-neutral-hover-url ); +[data-object|="unit-sam"][data-is-highlighted] .unit-marker { + background-image: var(--unit-sam-marker-neutral-hover-url); } [data-object|="unit-sam"][data-is-selected] .unit-marker { - background-image: var( --unit-sam-marker-neutral-selected-url ); + background-image: var(--unit-sam-marker-neutral-selected-url); } [data-object|="unit-sam"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-sam-marker-blue-url ); + background-image: var(--unit-sam-marker-blue-url); } -[data-object|="unit-sam"][data-coalition="blue"]:hover .unit-marker { - background-image: var( --unit-sam-marker-blue-hover-url ); +[data-object|="unit-sam"][data-coalition="blue"][data-is-highlighted] .unit-marker { + background-image: var(--unit-sam-marker-blue-hover-url); } [data-object|="unit-sam"][data-coalition="blue"][data-is-selected] .unit-marker { - background-image: var( --unit-sam-marker-blue-selected-url ); + background-image: var(--unit-sam-marker-blue-selected-url); } [data-object|="unit-sam"][data-coalition="red"] .unit-marker { - background-image: var( --unit-sam-marker-red-url ); + background-image: var(--unit-sam-marker-red-url); } -[data-object|="unit-sam"][data-coalition="red"]:hover .unit-marker { - background-image: var( --unit-sam-marker-red-hover-url ); +[data-object|="unit-sam"][data-coalition="red"][data-is-highlighted] .unit-marker { + background-image: var(--unit-sam-marker-red-hover-url); } [data-object|="unit-sam"][data-coalition="red"][data-is-selected] .unit-marker { - background-image: var( --unit-sam-marker-red-selected-url ); + background-image: var(--unit-sam-marker-red-selected-url); } /* navyunit */ [data-object|="unit-navyunit"] .unit-selected-spotlight { - translate:0 -2px; + translate: 0 -2px; } [data-object|="unit-navyunit"] .unit-marker { - background-image: var( --unit-navyunit-marker-neutral-url ); - height: var( --unit-navyunit-marker-height ); - width: var( --unit-navyunit-marker-width ); + background-image: var(--unit-navyunit-marker-neutral-url); + height: var(--unit-navyunit-marker-height); + width: var(--unit-navyunit-marker-width); } -[data-object|="unit-navyunit"]:hover .unit-marker { - background-image: var( --unit-navyunit-marker-neutral-hover-url ); +[data-object|="unit-navyunit"][data-is-highlighted] .unit-marker { + background-image: var(--unit-navyunit-marker-neutral-hover-url); } [data-object|="unit-navyunit"][data-is-selected] .unit-marker { - background-image: var( --unit-navyunit-marker-neutral-selected-url ); + background-image: var(--unit-navyunit-marker-neutral-selected-url); } [data-object|="unit-navyunit"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-navyunit-marker-blue-url ); + background-image: var(--unit-navyunit-marker-blue-url); } -[data-object|="unit-navyunit"][data-coalition="blue"]:hover .unit-marker { - background-image: var( --unit-navyunit-marker-blue-hover-url ); +[data-object|="unit-navyunit"][data-coalition="blue"][data-is-highlighted] .unit-marker { + background-image: var(--unit-navyunit-marker-blue-hover-url); } [data-object|="unit-navyunit"][data-coalition="blue"][data-is-selected] .unit-marker { - background-image: var( --unit-navyunit-marker-blue-selected-url ); + background-image: var(--unit-navyunit-marker-blue-selected-url); } [data-object|="unit-navyunit"][data-coalition="red"] .unit-marker { - background-image: var( --unit-navyunit-marker-red-url ); + background-image: var(--unit-navyunit-marker-red-url); } -[data-object|="unit-navyunit"][data-coalition="red"]:hover .unit-marker { - background-image: var( --unit-navyunit-marker-red-hover-url ); +[data-object|="unit-navyunit"][data-coalition="red"][data-is-highlighted] .unit-marker { + background-image: var(--unit-navyunit-marker-red-hover-url); } [data-object|="unit-navyunit"][data-coalition="red"][data-is-selected] .unit-marker { - background-image: var( --unit-navyunit-marker-red-selected-url ); + background-image: var(--unit-navyunit-marker-red-selected-url); } /* Building */ - [data-object|="unit-building"] .unit-marker { - background-image: var( --unit-building-marker-neutral-url ); - height: var( --unit-building-marker-height ); - width: var( --unit-building-marker-width ); + background-image: var(--unit-building-marker-neutral-url); + height: var(--unit-building-marker-height); + width: var(--unit-building-marker-width); } [data-object|="unit-building"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-building-marker-blue-url ); + background-image: var(--unit-building-marker-blue-url); } [data-object|="unit-building"][data-coalition="red"] .unit-marker { - background-image: var( --unit-building-marker-red-url ); + background-image: var(--unit-building-marker-red-url); } - - /* Weapons */ -[data-object|="unit-missile"], [data-object|="unit-bomb"] { +[data-object|="unit-missile"], +[data-object|="unit-bomb"] { cursor: default; } [data-object|="unit-missile"] .unit-marker { - background-image: var( --unit-missile-marker-neutral-url ); - height: var( --unit-missile-marker-height ); - width: var( --unit-missile-marker-width ); + background-image: var(--unit-missile-marker-neutral-url); + height: var(--unit-missile-marker-height); + width: var(--unit-missile-marker-width); } [data-object|="unit-missile"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-missile-marker-blue-url ); + background-image: var(--unit-missile-marker-blue-url); } [data-object|="unit-missile"][data-coalition="red"] .unit-marker { - background-image: var( --unit-missile-marker-red-url ); + background-image: var(--unit-missile-marker-red-url); } [data-object|="unit-bomb"] .unit-marker { - background-image: var( --unit-bomb-marker-neutral-url ); - height: var( --unit-bomb-marker-height ); - width: var( --unit-bomb-marker-width ); + background-image: var(--unit-bomb-marker-neutral-url); + height: var(--unit-bomb-marker-height); + width: var(--unit-bomb-marker-width); } [data-object|="unit-bomb"][data-coalition="blue"] .unit-marker { - background-image: var( --unit-bomb-marker-blue-url ); + background-image: var(--unit-bomb-marker-blue-url); } [data-object|="unit-bomb"][data-coalition="red"] .unit-marker { - background-image: var( --unit-bomb-marker-red-url ); + background-image: var(--unit-bomb-marker-red-url); } @@ -336,12 +329,12 @@ ********************************************/ [data-object|="unit"] .unit-short-label { - color: var( --secondary-gunmetal-grey ); + color: var(--secondary-gunmetal-grey); font-size: var(--unit-font-size); font-weight: var(--unit-font-weight); line-height: normal; position: absolute; - z-index:10; + z-index: 10; } [data-object|="unit-groundunit"] .unit-short-label { @@ -349,81 +342,81 @@ } [data-object|="unit-sam"] .unit-short-label { - translate:0 50%; + translate: 0 25%; } [data-object|="unit-navyunit"] .unit-short-label { - translate:0 -50%; + translate: 0 -50%; } [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 ); + 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 ); + translate: var(--unit-aircraft-fuel-x) var(--unit-aircraft-fuel-y); + width: var(--unit-aircraft-fuel-width); z-index: 5; } [data-object|="unit"] .unit-fuel-level { - background-color: var( --secondary-light-grey ); - height:100%; - width:100%; + background-color: var(--secondary-light-grey); + height: 100%; + width: 100%; } [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; + 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 > * { +[data-object|="unit"] .unit-ammo>* { background-color: white; - border: var( --unit-aircraft-ammo-border-width ) solid var( --secondary-dark-steel ); + border: var(--unit-aircraft-ammo-border-width) solid var(--secondary-dark-steel); border-radius: 50%; - padding: var( --unit-aircraft-ammo-radius ); + padding: var(--unit-aircraft-ammo-radius); } - [data-object|="unit"] .unit-summary { + pointer-events: none; column-gap: 6px; - color:white; - display:flex; + color: white; + display: flex; flex-wrap: wrap; - font-size:11px; + font-size: 11px; font-weight: bold; justify-content: right; line-height: 12px; - position:absolute; + position: absolute; row-gap: 1px; text-shadow: - -1px -1px 0 #000, + -1px -1px 0 #000, 1px -1px 0 #000, - -1px 1px 0 #000, - 1px 1px 0 #000; + -1px 1px 0 #000, + 1px 1px 0 #000; translate: -60px 0; - width:fit-content; - z-index:20; + width: fit-content; + z-index: 20; } [data-hide-labels] [data-object|="unit"] .unit-summary { - display:none; + display: none; } -[data-object|="unit"] .unit-summary > * { - padding:1px; +[data-object|="unit"] .unit-summary>* { + padding: 1px; } [data-object|="unit"] .unit-summary .unit-callsign { - color:white; + color: white; overflow: hidden; text-align: right; transform-origin: right; @@ -433,35 +426,30 @@ [data-object|="unit"] .unit-summary .unit-callsign:hover { direction: rtl; - overflow:visible; + overflow: visible; } - - -[data-object|="unit"][data-pilot|="ai"]:hover .unit-ammo, -[data-object|="unit"][data-pilot|="ai"]:hover .unit-fuel { - display:flex; +[data-object|="unit"]:hover .unit-ammo, +[data-object|="unit"]:hover .unit-fuel { + display: flex; } [data-object|="unit"][data-is-in-hotgroup] .unit-hotgroup, -[data-object|="unit"][data-pilot|="ai"][data-is-selected] .unit-ammo, -[data-object|="unit"][data-pilot|="ai"][data-is-selected] .unit-fuel, +[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; + display: flex; } [data-object|="unit"][data-has-fox-1] .unit-ammo-fox-1, [data-object|="unit"][data-has-fox-2] .unit-ammo-fox-2, [data-object|="unit"][data-has-fox-3] .unit-ammo-fox-3, [data-object|="unit"][data-has-other-ammo] .unit-ammo-other { - background-color: var( --secondary-gunmetal-grey ); + background-color: var(--secondary-gunmetal-grey); } - - - [data-object|="unit"][data-coalition="blue"][data-is-selected] .unit-short-label { - color: var( --secondary-blue-text ); + color: var(--secondary-blue-text); } [data-object|="unit"][data-coalition="blue"] .unit-fuel-level, @@ -469,16 +457,16 @@ [data-object|="unit"][data-coalition="blue"][data-has-fox-2] .unit-ammo-fox-2, [data-object|="unit"][data-coalition="blue"][data-has-fox-3] .unit-ammo-fox-3, [data-object|="unit"][data-coalition="blue"][data-has-other-ammo] .unit-ammo-other { - background-color: var( --primary-blue ); + background-color: var(--primary-blue); } [data-object|="unit"][data-coalition="blue"] .unit-vvi { - background-color: var( --secondary-blue-outline ); + background-color: var(--secondary-blue-outline); } [data-object|="unit"][data-coalition="red"][data-is-selected] .unit-short-label { - color: var( --secondary-red-text ); + color: var(--secondary-red-text); } [data-object|="unit"][data-coalition="red"] .unit-fuel-level, @@ -486,105 +474,100 @@ [data-object|="unit"][data-coalition="red"][data-has-fox-2] .unit-ammo-fox-2, [data-object|="unit"][data-coalition="red"][data-has-fox-3] .unit-ammo-fox-3, [data-object|="unit"][data-coalition="red"][data-has-other-ammo] .unit-ammo-other { - background-color: var( --primary-red ); + background-color: var(--primary-red); } [data-object|="unit"][data-coalition="blue"] .unit-vvi { - background-color: var( --secondary-red-outline ); + background-color: var(--secondary-red-outline); } - - @keyframes pulse { 50% { - opacity: 0; + opacity: 0; } } -[data-object|="unit"][data-pilot|="ai"][data-has-low-fuel] .unit-fuel { +[data-object|="unit"][data-has-low-fuel] .unit-fuel { animation: pulse 1.5s linear infinite; } - [data-object|="unit"] .unit-state { background-repeat: no-repeat; - position:absolute; - height:var( --unit-aircraft-state-height ); - width:var( --unit-aircraft-state-width ); + position: absolute; + height: var(--unit-aircraft-state-height); + width: var(--unit-aircraft-state-width); z-index: 10; } [data-object|="unit"][data-state="rtb"] .unit-state { - background-image: var( --unit-aircraft-state-rtb ); + background-image: var(--unit-aircraft-state-rtb); } [data-object|="unit"][data-state="land"] .unit-state { - background-image: var( --unit-aircraft-state-rtb ); + background-image: var(--unit-aircraft-state-rtb); } [data-object|="unit"][data-state="idle"] .unit-state { - background-image: var( --unit-aircraft-state-idle ); + background-image: var(--unit-aircraft-state-idle); } [data-object|="unit"][data-state="attack"] .unit-state { - background-image: var( --unit-aircraft-state-attack ); + background-image: var(--unit-aircraft-state-attack); } [data-object|="unit"][data-state="follow"] .unit-state { - background-image: var( --unit-aircraft-state-follow ); + background-image: var(--unit-aircraft-state-follow); } [data-object|="unit"][data-state="refuel"] .unit-state { - background-image: var( --unit-aircraft-state-refuel ); + background-image: var(--unit-aircraft-state-refuel); } [data-object|="unit"][data-state="human"] .unit-state { - background-image: var( --unit-aircraft-state-human ); + background-image: var(--unit-aircraft-state-human); } [data-object|="unit"][data-state="dcs"] .unit-state { - background-image: var( --unit-aircraft-state-dcs ); + background-image: var(--unit-aircraft-state-dcs); } /*** DEAD ***/ -[data-object|="unit-aircraft"][ data-is-dead ] { +[data-object|="unit-aircraft"][ data-is-dead] { cursor: default; } -[data-object|="unit-aircraft"][ data-is-dead ] .unit-marker { - background-image: var( --unit-aircraft-marker-neutral-dead-url ); +[data-object|="unit-aircraft"][ data-is-dead] .unit-marker { + background-image: var(--unit-aircraft-marker-neutral-dead-url); background-position: 50% 50%; background-size: auto 32px; } -[data-object|="unit-aircraft"][ data-is-dead ][data-coalition="blue"] .unit-marker { - background-image: var( --unit-aircraft-marker-blue-dead-url ); +[data-object|="unit-aircraft"][ data-is-dead][data-coalition="blue"] .unit-marker { + background-image: var(--unit-aircraft-marker-blue-dead-url); } -[data-object|="unit-aircraft"][ data-is-dead ][data-coalition="red"] .unit-marker { - background-image: var( --unit-aircraft-marker-red-dead-url ); +[data-object|="unit-aircraft"][ data-is-dead][data-coalition="red"] .unit-marker { + background-image: var(--unit-aircraft-marker-red-dead-url); } -[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 !important; +[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 !important; } - -[data-object|="unit-aircraft"][ data-is-dead ] .unit-summary > * { - display:none; -} - -[data-object|="unit-aircraft"][ data-is-dead ] .unit-summary .unit-callsign { - display:block; +[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/themes/olympus/images/icon_airbase_blue.svg b/client/public/themes/olympus/images/icon_airbase_blue.svg index 0800974c..d1fcf84c 100644 --- a/client/public/themes/olympus/images/icon_airbase_blue.svg +++ b/client/public/themes/olympus/images/icon_airbase_blue.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icon_airbase_neutral.svg b/client/public/themes/olympus/images/icon_airbase_neutral.svg index 69713bd5..43222171 100644 --- a/client/public/themes/olympus/images/icon_airbase_neutral.svg +++ b/client/public/themes/olympus/images/icon_airbase_neutral.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icon_airbase_red.svg b/client/public/themes/olympus/images/icon_airbase_red.svg index 45d55abd..d95872f1 100644 --- a/client/public/themes/olympus/images/icon_airbase_red.svg +++ b/client/public/themes/olympus/images/icon_airbase_red.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/task_tanker.svg b/client/public/themes/olympus/images/task_tanker.svg new file mode 100644 index 00000000..32ee5980 --- /dev/null +++ b/client/public/themes/olympus/images/task_tanker.svg @@ -0,0 +1,1256 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/olympus.css b/client/public/themes/olympus/olympus.css index 5f128e79..ed623daa 100644 --- a/client/public/themes/olympus/olympus.css +++ b/client/public/themes/olympus/olympus.css @@ -3,35 +3,35 @@ /** Colours **/ /*** Coalition: neutral **/ - --primary-grey : #CFD9E8; - --secondary-neutral : #111111; + --primary-neutral : #949ba7; + --secondary-neutral-outline : #111111; + --secondary-neutral-text : #111111; /*** Coalition: blue **/ - --primary-blue : #247be2; - --secondary-blue-outline : #082e44; - --secondary-blue-text : #017DC1; + --primary-blue : #247be2; + --secondary-blue-outline : #082e44; + --secondary-blue-text : #017DC1; /*** Coalition: red **/ - --primary-red : #ff5858; - --secondary-red-outline : #262222; - --secondary-red-text : #D42121; + --primary-red : #ff5858; + --secondary-red-outline : #262222; + --secondary-red-text : #D42121; + --accent-green : #8bff63; + --accent-light-blue : #5ca7ff; + --accent-light-red : #F5B6B6; - --accent-green : #8bff63; - --accent-light-blue : #5ca7ff; - --accent-light-red : #F5B6B6; + --background-grey : #3d4651; + --background-slate-blue : #363c43; + --background-offwhite : #f2f2f3; + --background-steel : #202831; - --background-grey : #3d4651; - --background-slate-blue : #363c43; - --background-offwhite : #f2f2f3; - --background-steel : #202831; + --secondary-dark-steel : #181e25; + --secondary-gunmetal-grey : #2f2f2f; + --secondary-light-grey : #797e83; + --secondary-yellow : #ffd46893; - --secondary-dark-steel : #181e25; - --secondary-gunmetal-grey : #2f2f2f; - --secondary-light-grey : #797e83; - --secondary-yellow : #ffd46893; - - --background-hover : #f2f2f333; + --background-hover : #f2f2f333; --nav-text : #ECECEC; diff --git a/client/src/@types/dom.d.ts b/client/src/@types/dom.d.ts index 094d3444..3feba2b0 100644 --- a/client/src/@types/dom.d.ts +++ b/client/src/@types/dom.d.ts @@ -5,7 +5,8 @@ interface CustomEventMap { "unitsDeselection": CustomEvent, "clearSelection": CustomEvent<>, "unitCreation": CustomEvent, - "unitDeletion": CustomEvent, + "unitDeletion": CustomEvent, + "unitDeath": CustomEvent, "unitUpdated": CustomEvent, "unitMoveCommand": CustomEvent, "unitAttackCommand": CustomEvent, diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index 9e621fde..d2f0d109 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -29,11 +29,7 @@ interface MissionData { } interface FormationData { - formation: string; - isLeader: boolean; - isWingman: boolean; leaderID: number; - wingmenIDs: number[]; } interface TaskData { @@ -44,14 +40,12 @@ interface TaskData { targetAltitude: number; isTanker: boolean; isAWACS: boolean; - TACANOn: boolean; TACANChannel: number; TACANXY: string; TACANCallsign: string; radioFrequency: number; radioCallsign: number; radioCallsignNumber: number; - radioAMFM: string; } interface OptionsData { diff --git a/client/src/controls/contextmenu.ts b/client/src/controls/contextmenu.ts index 4f8db9ae..9a19562f 100644 --- a/client/src/controls/contextmenu.ts +++ b/client/src/controls/contextmenu.ts @@ -49,12 +49,12 @@ export class ContextMenu { 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 + "px"; + 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 + "px"; + this.#container.style.top = window.innerHeight - this.#container.offsetHeight - 10 + "px"; } } } \ No newline at end of file diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index fb8af28d..cb96e4c0 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -19,17 +19,19 @@ export class Dropdown { this.setOptions(options); } - this.#value.addEventListener( "click", ev => { - this.#element.classList.toggle( "is-open" ); + this.#value.addEventListener("click", (ev) => { + this.#element.classList.toggle("is-open"); + this.#options.classList.toggle("scrollbar-visible", this.#options.scrollHeight > this.#options.clientHeight); this.#clip(); }); - this.#options.classList.add( "ol-scrollable" ); + 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.#element.classList.remove("is-open"); + } + }); - // Commented out since it is a bit frustrating, particularly when the dropdown opens towards the top and not to the bottom - //this.#element.addEventListener("mouseleave", ev => { - // this.#close(); - //}); + this.#options.classList.add( "ol-scrollable" ); } setOptions(optionsList: string[]) @@ -46,7 +48,7 @@ export class Dropdown { button.addEventListener("click", (e: MouseEvent) => { e.stopPropagation(); - this.#value.innerText = option; + this.#value.innerHTML = `
${option}
`; this.#close(); this.#callback(option, e); this.#index = idx; @@ -69,7 +71,7 @@ export class Dropdown { if (idx < this.#optionsList.length) { var option = this.#optionsList[idx]; - this.#value.innerText = option; + this.#value.innerHTML = `
${option}
`; this.#index = idx; this.#close(); this.#callback(option); diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index 20c4c4c5..929dad05 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -1,5 +1,5 @@ import { LatLng } from "leaflet"; -import { getActiveCoalition, setActiveCoalition } from ".."; +import { getActiveCoalition, getMap, setActiveCoalition } from ".."; import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../server/server"; import { aircraftDatabase } from "../units/aircraftdatabase"; import { groundUnitsDatabase } from "../units/groundunitsdatabase"; @@ -25,7 +25,8 @@ export class MapContextMenu extends ContextMenu { constructor(id: string) { super(id); - this.getContainer()?.querySelector("#context-menu-switch")?.addEventListener('change', (e) => this.#onSwitch(e)); + this.getContainer()?.querySelector("#context-menu-switch")?.addEventListener('click', (e) => this.#onToggleLeftClick(e)); + this.getContainer()?.querySelector("#context-menu-switch")?.addEventListener('contextmenu', (e) => this.#onToggleRightClick(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)); @@ -41,14 +42,20 @@ export class MapContextMenu extends ContextMenu { 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) => { @@ -97,15 +104,28 @@ export class MapContextMenu extends ContextMenu { this.#spawnOptions.latlng = latlng; } - #onSwitch(e: any) { + #onToggleLeftClick(e: any) { if (this.getContainer() != null) { - if (e.srcElement.checked) + if (e.srcElement.dataset.activeCoalition == "blue") + setActiveCoalition("neutral"); + else if (e.srcElement.dataset.activeCoalition == "neutral") setActiveCoalition("red"); else setActiveCoalition("blue"); } } + #onToggleRightClick(e: any) { + if (this.getContainer() != null) { + if (e.srcElement.dataset.activeCoalition == "red") + setActiveCoalition("neutral"); + else if (e.srcElement.dataset.activeCoalition == "neutral") + setActiveCoalition("blue"); + else + setActiveCoalition("red"); + } + } + /********* Aircraft spawn menu *********/ #setAircraftRole(role: string) { this.#spawnOptions.role = role; @@ -135,7 +155,6 @@ export class MapContextMenu extends ContextMenu { image.src = `images/units/${aircraftDatabase.getByLabel(label)?.filename}`; image.classList.toggle("hide", false); } - this.clip(); } diff --git a/client/src/featureswitches.ts b/client/src/featureswitches.ts index ed52d8eb..c5776683 100644 --- a/client/src/featureswitches.ts +++ b/client/src/featureswitches.ts @@ -90,7 +90,7 @@ export class FeatureSwitches { }), new FeatureSwitch({ - "defaultEnabled": false, + "defaultEnabled": true, "label": "Show splash screen", "masterSwitch": true, "name": "splashScreen" @@ -116,36 +116,24 @@ export class FeatureSwitches { #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 = {}; diff --git a/client/src/index.ts b/client/src/index.ts index 2c817671..86f0057a 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -9,11 +9,12 @@ import { AIC } from "./aic/aic"; import { ATC } from "./atc/atc"; import { FeatureSwitches } from "./featureswitches"; import { LogPanel } from "./panels/logpanel"; -import { getAirbases, getBullseye as getBullseyes, getConfig, getMission, getUnits, setAddress, toggleDemoEnabled } from "./server/server"; +import { getConfig, getPaused, setAddress, setCredentials, setPaused, startUpdate, toggleDemoEnabled } from "./server/server"; import { UnitDataTable } from "./units/unitdatatable"; import { keyEventWasInInput } from "./other/utils"; import { Popup } from "./popups/popup"; import { Dropdown } from "./controls/dropdown"; +import { HotgroupPanel } from "./panels/hotgrouppanel"; var map: Map; @@ -28,23 +29,20 @@ var connectionStatusPanel: ConnectionStatusPanel; var unitControlPanel: UnitControlPanel; var mouseInfoPanel: MouseInfoPanel; var logPanel: LogPanel; +var hotgroupPanel: HotgroupPanel; var infoPopup: Popup; -var connected: boolean = false; -var paused: boolean = false; var activeCoalition: string = "blue"; -var sessionHash: string | null = null; var unitDataTable: UnitDataTable; var featureSwitches; function setup() { - featureSwitches = new FeatureSwitches(); - /* Initialize base functionalitites*/ + /* Initialize base functionalitites */ map = new Map('map-container'); unitsManager = new UnitsManager(); missionHandler = new MissionHandler(); @@ -54,18 +52,22 @@ function setup() { unitControlPanel = new UnitControlPanel("unit-control-panel"); connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel"); mouseInfoPanel = new MouseInfoPanel("mouse-info-panel"); + hotgroupPanel = new HotgroupPanel("hotgroup-panel"); //logPanel = new LogPanel("log-panel"); /* Popups */ infoPopup = new Popup("info-popup"); + /* Controls */ + new Dropdown("app-icon", () => { }); + + /* Unit data table */ unitDataTable = new UnitDataTable("unit-data-table"); /* AIC */ let aicFeatureSwitch = featureSwitches.getSwitch("aic"); if (aicFeatureSwitch?.isEnabled()) { aic = new AIC(); - // TODO: add back buttons } /* ATC */ @@ -75,90 +77,29 @@ function setup() { atc.startUpdates(); } - - new Dropdown( "app-icon", () => {} ); - /* Setup event handlers */ setupEvents(); - getConfig(readConfig) + /* Load the config file */ + getConfig(readConfig); } -function readConfig(config: any) -{ - if (config && config["server"] != undefined && config["server"]["address"] != undefined && config["server"]["port"] != undefined) - { - const address = config["server"]["address"]; - const port = config["server"]["port"]; +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); - - /* On the first connection, force request of full data */ - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((data: BullseyesData) => getMissionData()?.update(data)); - getMission((data: any) => {getMissionData()?.update(data)}); - getUnits((data: UnitsData) => getUnitsManager()?.update(data), true /* Does a full refresh */); - - /* Start periodically requesting updates */ - startPeriodicUpdate(); + setAddress(address == "*" ? window.location.hostname : address, port); } else { throw new Error('Could not read configuration file!'); - } -} - -function startPeriodicUpdate() { - requestUpdate(); - requestRefresh(); -} - -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()); -} - -function requestRefresh() { - /* Main refresh rate = 5000ms. */ - getUnits((data: UnitsData) => { - if (!getPaused()){ - getUnitsManager()?.update(data); - getAirbases((data: AirbasesData) => getMissionData()?.update(data)); - getBullseyes((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); -} - -function checkSessionHash(newSessionHash: string) { - if (sessionHash != null) { - if (newSessionHash != sessionHash) - location.reload(); } - else - sessionHash = newSessionHash; } 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")) { @@ -166,7 +107,7 @@ function setupEvents() { } const triggerElement = target.closest("[data-on-click]"); - + if (triggerElement instanceof HTMLElement) { const eventName: string = triggerElement.dataset.onClick || ""; let params = JSON.parse(triggerElement.dataset.onClickParams || "{}"); @@ -183,16 +124,14 @@ function setupEvents() { /* Keyup events */ document.addEventListener("keyup", ev => { - - if ( keyEventWasInInput( ev ) ) { + if (keyEventWasInInput(ev)) { return; } - switch (ev.code) { case "KeyL": document.body.toggleAttribute("data-hide-labels"); break; - case "KeyD": + case "KeyT": toggleDemoEnabled(); break; case "Quote": @@ -201,37 +140,80 @@ function setupEvents() { 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; } }); - /* - const unitName = document.getElementById( "unit-name" ); - if ( unitName instanceof HTMLInputElement ) { - unitName.addEventListener( "change", ev => { - unitName.setAttribute( "disabled", "true" ); - unitName.setAttribute( "readonly", "true" ); - - // Do something with this: - }); - - document.addEventListener( "editUnitName", ev => { - unitName.removeAttribute( "disabled" ); - unitName.removeAttribute( "readonly" ); - unitName.focus(); - }); - } - //*/ - - document.addEventListener( "closeDialog", (ev: CustomEventInit) => { - ev.detail._element.closest( ".ol-dialog" ).classList.add( "hide" ); + /* 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( "toggleElements", (ev: CustomEventInit) => { - document.querySelectorAll( ev.detail.selector ).forEach( el => { - el.classList.toggle( "hide" ); + 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(); + }) } export function getMap() { @@ -270,6 +252,10 @@ 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) }); @@ -279,23 +265,10 @@ export function getActiveCoalition() { return activeCoalition; } -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; -} - -export function getConnected() { - return connected; -} - -export function setPaused(newPaused: boolean) { - paused = newPaused; - paused? getInfoPopup().setText("Paused"): getInfoPopup().setText("Unpaused"); -} - -export function getPaused() { - return paused; +export function setConnectionStatus(status: string) { + const el = document.querySelector("#connection-status") as HTMLElement; + if (el) + el.dataset["status"] = status; } export function getInfoPopup() { diff --git a/client/src/map/boxselect.ts b/client/src/map/boxselect.ts index 421509ef..b285c10a 100644 --- a/client/src/map/boxselect.ts +++ b/client/src/map/boxselect.ts @@ -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) { diff --git a/client/src/map/map.ts b/client/src/map/map.ts index be0c7031..a531e33a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -3,7 +3,7 @@ import { MiniMap, MiniMapOptions } from "leaflet-control-mini-map"; import { getUnitsManager } from ".."; import { BoxSelect } from "./boxselect"; -import { MapContextMenu } from "../controls/mapcontextmenu"; +import { MapContextMenu, SpawnOptions } from "../controls/mapcontextmenu"; import { UnitContextMenu } from "../controls/unitcontextmenu"; import { AirbaseContextMenu } from "../controls/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; @@ -18,9 +18,14 @@ export const MOVE_UNIT = "MOVE_UNIT"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); +var temporaryIcon = new L.Icon({ + iconUrl: 'images/icon-temporary.png', + iconSize: [52, 52], + iconAnchor: [26, 26] +}); + export class ClickableMiniMap extends MiniMap { - constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) - { + constructor(layer: L.TileLayer | L.LayerGroup, options?: MiniMapOptions) { super(layer, options); } @@ -34,11 +39,18 @@ export class Map extends L.Map { #state: string; #layer: L.TileLayer | null = null; #preventLeftClick: boolean = false; - #leftClickTimer: any = 0; + #leftClickTimer: number = 0; + #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: L.Marker[] = []; #mapContextMenu: MapContextMenu = new MapContextMenu("map-contextmenu"); #unitContextMenu: UnitContextMenu = new UnitContextMenu("unit-contextmenu"); @@ -49,7 +61,7 @@ export class Map extends L.Map { constructor(ID: string) { /* Init the leaflet map */ //@ts-ignore - super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7 }); + 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"); @@ -57,59 +69,59 @@ export class Map extends L.Map { /* Minimap */ /* Draw the limits of the maps in the minimap*/ var latlngs = [[ // NTTR - new L.LatLng(39.7982463, -119.985425 ), - new L.LatLng(34.4037128, -119.7806729), - new L.LatLng(34.3483316, -112.4529351), - new L.LatLng(39.7372411, -112.1130805), - new L.LatLng(39.7982463, -119.985425 ) - ], - [ // Syria - new L.LatLng(37.3630556, 29.2686111), - new L.LatLng(31.8472222, 29.8975), - new L.LatLng(32.1358333, 42.1502778), - new L.LatLng(37.7177778, 42.3716667), - new L.LatLng(37.3630556, 29.2686111) - ], - [ // Caucasus - new L.LatLng(39.6170191, 27.634935), - new L.LatLng(38.8735863, 47.1423108), - new L.LatLng(47.3907982, 49.3101946), - new L.LatLng(48.3955879, 26.7753625), - new L.LatLng(39.6170191, 27.634935) - ], - [ // Persian Gulf - new L.LatLng(32.9355285, 46.5623682), - new L.LatLng(21.729393, 47.572675), - new L.LatLng(21.8501348, 63.9734737), - new L.LatLng(33.131584, 64.7313594), - new L.LatLng(32.9355285, 46.5623682) - ], - [ // Marianas - new L.LatLng(22.09, 135.0572222), - new L.LatLng(10.5777778, 135.7477778), - new L.LatLng(10.7725, 149.3918333), - new L.LatLng(22.5127778, 149.5427778), - new L.LatLng(22.09, 135.0572222) - ] - ]; + new L.LatLng(39.7982463, -119.985425), + new L.LatLng(34.4037128, -119.7806729), + new L.LatLng(34.3483316, -112.4529351), + new L.LatLng(39.7372411, -112.1130805), + new L.LatLng(39.7982463, -119.985425) + ], + [ // Syria + new L.LatLng(37.3630556, 29.2686111), + new L.LatLng(31.8472222, 29.8975), + new L.LatLng(32.1358333, 42.1502778), + new L.LatLng(37.7177778, 42.3716667), + new L.LatLng(37.3630556, 29.2686111) + ], + [ // Caucasus + new L.LatLng(39.6170191, 27.634935), + new L.LatLng(38.8735863, 47.1423108), + new L.LatLng(47.3907982, 49.3101946), + new L.LatLng(48.3955879, 26.7753625), + new L.LatLng(39.6170191, 27.634935) + ], + [ // Persian Gulf + new L.LatLng(32.9355285, 46.5623682), + new L.LatLng(21.729393, 47.572675), + new L.LatLng(21.8501348, 63.9734737), + new L.LatLng(33.131584, 64.7313594), + new L.LatLng(32.9355285, 46.5623682) + ], + [ // Marianas + new L.LatLng(22.09, 135.0572222), + new L.LatLng(10.5777778, 135.7477778), + new L.LatLng(10.7725, 149.3918333), + new L.LatLng(22.5127778, 149.5427778), + new L.LatLng(22.09, 135.0572222) + ] + ]; var minimapLayer = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); - var miniMapPolyline = new L.Polyline(latlngs, {color: '#202831'}); + var miniMapPolyline = new L.Polyline(latlngs, { 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); + L.control.scalenautic({ position: "topright", maxWidth: 300, nautic: true, metric: true, imperial: false }).addTo(this); /* Init the state machine */ this.#state = IDLE; /* Register event handles */ this.on("click", (e: any) => this.#onClick(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("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)); @@ -121,7 +133,7 @@ export class Map extends L.Map { document.body.toggleAttribute("data-hide-" + ev.detail.coalition); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); - + document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { document.body.toggleAttribute("data-hide-" + ev.detail.category); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); @@ -131,8 +143,13 @@ export class Map extends L.Map { if (this.#centerUnit != null && ev.detail == this.#centerUnit) this.#panToUnit(this.#centerUnit); }); - + this.#mapSourceDropdown = new Dropdown("map-type", (layerName: string) => this.setLayer(layerName), this.getLayers()) + + this.#panInterval = window.setInterval(() => { + 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); } setLayer(layerName: string) { @@ -187,10 +204,10 @@ export class Map extends L.Map { setState(state: string) { this.#state = state; if (this.#state === IDLE) { - L.DomUtil.removeClass(this.getContainer(),'crosshair-cursor-enabled'); + L.DomUtil.removeClass(this.getContainer(), 'crosshair-cursor-enabled'); } else if (this.#state === MOVE_UNIT) { - L.DomUtil.addClass(this.getContainer(),'crosshair-cursor-enabled'); + L.DomUtil.addClass(this.getContainer(), 'crosshair-cursor-enabled'); } document.dispatchEvent(new CustomEvent("mapStateChanged")); } @@ -200,8 +217,7 @@ export class Map extends L.Map { } /* Context Menus */ - hideAllContextMenus() - { + hideAllContextMenus() { this.hideMapContextMenu(); this.hideUnitContextMenu(); this.hideAirbaseContextMenu(); @@ -220,7 +236,7 @@ export class Map extends L.Map { document.dispatchEvent(new CustomEvent("mapContextMenu")); } - getMapContextMenu(){ + getMapContextMenu() { return this.#mapContextMenu; } @@ -231,7 +247,7 @@ export class Map extends L.Map { this.#unitContextMenu.show(x, y, e.latlng); } - getUnitContextMenu(){ + getUnitContextMenu() { return this.#unitContextMenu; } @@ -247,7 +263,7 @@ export class Map extends L.Map { this.#airbaseContextMenu.setAirbase(airbase); } - getAirbaseContextMenu(){ + getAirbaseContextMenu() { return this.#airbaseContextMenu; } @@ -270,8 +286,7 @@ export class Map extends L.Map { } centerOnUnit(ID: number | null) { - if (ID != null) - { + if (ID != null) { this.options.scrollWheelZoom = 'center'; this.#centerUnit = getUnitsManager().getUnitByID(ID); } @@ -289,45 +304,112 @@ export class Map extends L.Map { else if (theatre == "MarianaIslands") bounds = new L.LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]); else if (theatre == "Nevada") - bounds = new L.LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]) + bounds = new L.LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]) else if (theatre == "PersianGulf") - bounds = new L.LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]) - else if (theatre == "Falklands") - { + bounds = new L.LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]) + else if (theatre == "Falklands") { // TODO } - else if (theatre == "Caucasus") - { - bounds = new L.LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]) + else if (theatre == "Caucasus") { + bounds = new L.LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]) miniMapZoom = 4; } this.setView(bounds.getCenter(), 8); - this.setMaxBounds(bounds); + //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 = 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 L.Marker(latlng, {icon: temporaryIcon}); + 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.hideAllContextMenus(); if (this.#state === IDLE) { - + } else if (this.#state === MOVE_UNIT) { this.setState(IDLE); @@ -337,7 +419,7 @@ export class Map extends L.Map { } #onDoubleClick(e: any) { - + } #onContextMenu(e: any) { @@ -355,44 +437,34 @@ export class Map extends L.Map { } } - #onSelectionEnd(e: any) - { + #onSelectionEnd(e: any) { clearTimeout(this.#leftClickTimer); this.#preventLeftClick = true; this.#leftClickTimer = window.setTimeout(() => { - this.#preventLeftClick = false; + this.#preventLeftClick = false; }, 200); getUnitsManager().selectFromBounds(e.selectionBounds); } - #onMouseDown(e: any) - { + #onMouseDown(e: any) { this.hideAllContextMenus(); - if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0)) - this.dragging.disable(); } - #onMouseUp(e: any) - { - if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0)) - this.dragging.enable(); + #onMouseUp(e: any) { } - #onMouseMove(e: any) - { + #onMouseMove(e: any) { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; } - #onZoom(e: any) - { + #onZoom(e: any) { if (this.#centerUnit != null) this.#panToUnit(this.#centerUnit); } - #panToUnit(unit: Unit) - { + #panToUnit(unit: Unit) { var unitPosition = new L.LatLng(unit.getFlightData().latitude, unit.getFlightData().longitude); - this.setView(unitPosition, this.getZoom(), {animate: false}); + this.setView(unitPosition, this.getZoom(), { animate: false }); } } diff --git a/client/src/panels/hotgrouppanel.ts b/client/src/panels/hotgrouppanel.ts new file mode 100644 index 00000000..8431c2ab --- /dev/null +++ b/client/src/panels/hotgrouppanel.ts @@ -0,0 +1,48 @@ +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) { + const hotgroupHtml = `
+
${hotgroup}
+
+ x${getUnitsManager().getUnitsByHotgroup(hotgroup).length}` + var el = document.createElement("div"); + el.classList.add("hotgroup-selector"); + el.innerHTML = hotgroupHtml; + 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/panel.ts b/client/src/panels/panel.ts index fd439abe..df0a2f48 100644 --- a/client/src/panels/panel.ts +++ b/client/src/panels/panel.ts @@ -11,13 +11,9 @@ export class Panel { this.#visible = true; } - protected onHide() {} - hide() { this.#element.classList.toggle("hide", true); this.#visible = false; - - this.onHide(); } toggle() { diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index b8dadaeb..75056e4a 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -72,8 +72,9 @@ export class UnitControlPanel extends Panel { this.#advancedSettingsDialog = document.querySelector("#advanced-settings-dialog"); - document.addEventListener("unitUpdated", (e: CustomEvent) => { if (e.detail.getSelected()) this.update() }); - document.addEventListener("unitsSelection", (e: CustomEvent) => { this.show(); this.update() }); + 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", () => { @@ -84,13 +85,65 @@ export class UnitControlPanel extends Panel { this.hide(); } - // Do this after panel is hidden (make sure there's a reset) - protected onHide() { + hide() { + super.hide(); + this.#expectedAltitude = -1; this.#expectedSpeed = -1; } + addButtons() { + var units = getUnitsManager().getSelectedUnits(); + if (units.length < 20) { + this.getElement().querySelector("#selected-units-container")?.replaceChildren(...units.map((unit: Unit, index: number) => { + let database: UnitDatabase | null; + if (unit instanceof Aircraft) + database = aircraftDatabase; + else if (unit instanceof GroundUnit) + database = groundUnitsDatabase; + else + database = null; // TODO add databases for other unit types + + var button = document.createElement("button"); + var callsign = unit.getBaseData().unitName || ""; + + button.setAttribute("data-short-label", database?.getByName(unit.getBaseData().name)?.shortLabel || unit.getBaseData().name); + 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() { + if (this.getVisible()){ + var units = getUnitsManager().getSelectedUnits(); + this.getElement().querySelector("#advanced-settings-div")?.classList.toggle("hide", units.length != 1); + if (this.getElement() != null && units.length > 0) { + this.#showFlightControlSliders(units); + + 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)) + }); + } + } + } // Update function will only be allowed to update the sliders once it's matched the expected value for the first time (due to lag of Ajax request) #updateCanSetAltitudeSlider(altitude: number) { @@ -109,46 +162,6 @@ export class UnitControlPanel extends Panel { return false; } - update() { - var units = getUnitsManager().getSelectedUnits(); - - this.getElement().querySelector("#advanced-settings-div")?.classList.toggle("hide", units.length != 1); - - if (this.getElement() != null && units.length > 0) { - this.#showFlightControlSliders(units); - - this.getElement().querySelector("#selected-units-container")?.replaceChildren(...units.map((unit: Unit, index: number) => { - let database: UnitDatabase | null; - if (unit instanceof Aircraft) - database = aircraftDatabase; - else if (unit instanceof GroundUnit) - database = groundUnitsDatabase; - else - database = null; // TODO add databases for other unit types - - var button = document.createElement("button"); - var callsign = unit.getBaseData().unitName || ""; - - button.setAttribute("data-short-label", database?.getByName(unit.getBaseData().name)?.shortLabel || ""); - button.setAttribute("data-callsign", callsign); - - button.setAttribute("data-coalition", unit.getMissionData().coalition); - button.classList.add("pill", "highlight-coalition") - - button.addEventListener("click", () => getUnitsManager().selectUnit(unit.ID, true)); - return (button); - })); - - 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)) - }); - } - } - #showFlightControlSliders(units: Unit[]) { if (getUnitsManager().getSelectedUnitsType() !== undefined) this.#airspeedSlider.show() @@ -215,7 +228,7 @@ export class UnitControlPanel extends Panel { // Default values for "normal" units this.#radioCallsignDropdown.setOptions(["Enfield", "Springfield", "Uzi", "Colt", "Dodge", "Ford", "Chevy", "Pontiac"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); // Input values var tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") @@ -237,7 +250,7 @@ export class UnitControlPanel extends Panel { this.#radioDecimalsDropdown.setValue("." + radioDecimals); // Make sure its in the valid range - if (!this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign)) + if (!this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1)) this.#radioCallsignDropdown.selectValue(0); // Set options for tankers @@ -245,7 +258,7 @@ export class UnitControlPanel extends Panel { if (roles != undefined && Array.prototype.concat.apply([], roles)?.includes("Tanker")){ this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.classList.remove("hide"); this.#radioCallsignDropdown.setOptions(["Texaco", "Arco", "Shell"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); } else { this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.classList.add("hide"); @@ -255,7 +268,7 @@ export class UnitControlPanel extends Panel { if (roles != undefined && Array.prototype.concat.apply([], roles)?.includes("AWACS")){ this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.classList.remove("hide"); this.#radioCallsignDropdown.setOptions(["Overlord", "Magic", "Wizard", "Focus", "Darkstar"]); - this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); + this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign - 1); } else { this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.classList.add("hide"); } @@ -267,12 +280,14 @@ export class UnitControlPanel extends Panel { { const isTanker = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input")?.checked? true: false; const isAWACS = this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input")?.checked? true: false; + const TACANChannel = Number(this.#advancedSettingsDialog.querySelector("#TACAN-channel")?.querySelector("input")?.value); const TACANXY = this.#TACANXYDropdown.getValue(); const TACANCallsign = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input")?.value + const radioMHz = Number(this.#advancedSettingsDialog.querySelector("#radio-mhz")?.querySelector("input")?.value); const radioDecimals = this.#radioDecimalsDropdown.getValue(); - const radioCallsign = this.#radioCallsignDropdown.getIndex(); + const radioCallsign = this.#radioCallsignDropdown.getIndex() + 1; const radioCallsignNumber = Number(this.#advancedSettingsDialog.querySelector("#radio-callsign-number")?.querySelector("input")?.value); var radioFrequency = (radioMHz * 1000 + Number(radioDecimals.substring(1))) * 1000; diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts index 19c3646e..4d4eb77e 100644 --- a/client/src/panels/unitinfopanel.ts +++ b/client/src/panels/unitinfopanel.ts @@ -54,7 +54,7 @@ export class UnitInfoPanel extends Panel { const baseData = unit.getBaseData(); /* Set the unit info */ - this.#unitLabel.innerText = aircraftDatabase.getByName(baseData.name)?.label || ""; + this.#unitLabel.innerText = aircraftDatabase.getByName(baseData.name)?.label || baseData.name; this.#unitName.innerText = baseData.unitName; this.#unitControl.innerText = ( ( baseData.AI ) ? "AI" : "Human" ) + " controlled"; // this.#groupName.innerText = baseData.groupName; diff --git a/client/src/server/server.ts b/client/src/server/server.ts index c1cd1420..24ccf144 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,7 +1,10 @@ import * as L from 'leaflet' -import { setConnected } from '..'; +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"; @@ -10,29 +13,46 @@ 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() -{ +export function toggleDemoEnabled() { demoEnabled = !demoEnabled; } -export function GET(callback: CallableFunction, uri: string, options?: string){ +export function setCredentials(newUsername: string, newCredentials: string) { + username = newUsername; + credentials = newCredentials; +} + +export function GET(callback: CallableFunction, uri: string, options?: string) { var xmlHttp = new XMLHttpRequest(); xmlHttp.open("GET", `${demoEnabled? DEMO_ADDRESS: REST_ADDRESS}/${uri}${options? options: ''}`, true); + if (credentials) + xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); xmlHttp.onload = function (e) { - var data = JSON.parse(xmlHttp.responseText); - if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) - { - callback(data); - lastUpdateTime = parseInt(data.time); - if (isNaN(lastUpdateTime)) - lastUpdateTime = 0; - setConnected(true); + if (xmlHttp.status == 200) { + var data = JSON.parse(xmlHttp.responseText); + if (uri !== UNITS_URI || 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 () { + xmlHttp.onerror = function (res) { console.error("An error occurred during the XMLHttpRequest"); setConnected(false); }; @@ -40,13 +60,15 @@ export function GET(callback: CallableFunction, uri: string, options?: string){ } export function POST(request: object, callback: CallableFunction){ - var xhr = new XMLHttpRequest(); - xhr.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); - xhr.setRequestHeader("Content-Type", "application/json"); - xhr.onreadystatechange = () => { + 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(); }; - xhr.send(JSON.stringify(request)); + xmlHttp.send(JSON.stringify(request)); } export function getConfig(callback: CallableFunction) { @@ -208,4 +230,81 @@ export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolea 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 index fda4334a..b738b4b6 100644 --- a/client/src/units/aircraftdatabase.ts +++ b/client/src/units/aircraftdatabase.ts @@ -322,7 +322,7 @@ export class AircraftDatabase extends UnitDatabase { }, "H-6J": { "name": "H-6J", - "label": "H-6J (the other) Copyright Infringement", + "label": "H-6J Badger, "era": ["Mid Cold War, Late Cold War", "Modern"], "shortLabel": "H6", "loadouts": [ @@ -359,7 +359,7 @@ export class AircraftDatabase extends UnitDatabase { }, "J-11A": { "name": "J-11A", - "label": "J-11A Copyright Infringement", + "label": "J-11A Flaming Dragon", "era": ["Modern"], "shortLabel": "11", "loadouts": [ @@ -1183,7 +1183,7 @@ export class AircraftDatabase extends UnitDatabase { }, "M-2000C": { "name": "M-2000C", - "label": "M-2000C Baguette", + "label": "M-2000C Mirage", "era": ["Late Cold War", "Modern"], "shortLabel": "M2KC", "loadouts": [ diff --git a/client/src/units/helicopterdatabase.ts b/client/src/units/helicopterdatabase.ts index 32312440..d7fc9996 100644 --- a/client/src/units/helicopterdatabase.ts +++ b/client/src/units/helicopterdatabase.ts @@ -381,57 +381,6 @@ export class HelicopterDatabase extends UnitDatabase { ], "filename": "ah-1.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", diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index b8ad84b2..e8b64881 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,4 +1,4 @@ -import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker } from 'leaflet'; +import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; import { getMap, getUnitsManager } from '..'; import { rad2deg } from '../other/utils'; import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit } from '../server/server'; @@ -40,11 +40,7 @@ export class Unit extends Marker { coalition: "", }, formationData: { - formation: "", - isLeader: false, - isWingman: false, - leaderID: 0, - wingmenIDs: [], + leaderID: 0 }, taskData: { currentState: "IDLE", @@ -54,14 +50,12 @@ export class Unit extends Marker { targetAltitude: 0, isTanker: false, isAWACS: false, - TACANOn: false, TACANChannel: 0, TACANXY: "X", TACANCallsign: "", radioFrequency: 0, radioCallsign: 0, - radioCallsignNumber: 0, - radioAMFM: "AM" + radioCallsignNumber: 0 }, optionsData: { ROE: "", @@ -72,6 +66,7 @@ export class Unit extends Marker { #selectable: boolean; #selected: boolean = false; #hidden: boolean = false; + #highlighted: boolean = false; #preventClick: boolean = false; @@ -81,7 +76,8 @@ export class Unit extends Marker { #miniMapMarker: CircleMarker | null = null; #timer: number = 0; - #forceUpdate: boolean = false; + + #hotgroup: number | null = null; static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -93,27 +89,29 @@ export class Unit extends Marker { } constructor(ID: number, data: UpdateData) { - super(new LatLng(0, 0), { riseOnHover: true }); + super(new LatLng(0, 0), { riseOnHover: true, keyboard: false }); this.ID = ID; this.#selectable = true; - + 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 = []; /* Deselect units if they are hidden */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { - window.setTimeout(() => {this.setSelected(this.getSelected() && !this.getHidden())}, 300); + window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); }); - + document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { - window.setTimeout(() => {this.setSelected(this.getSelected() && !this.getHidden())}, 300); + window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); }); /* Set the unit data */ @@ -123,82 +121,80 @@ export class Unit extends Marker { var icon = new DivIcon({ html: this.getMarkerHTML(), className: 'leaflet-unit-marker', - iconAnchor: [0, 0] + iconAnchor: [25, 25], + iconSize: [50, 50], }); this.setIcon(icon); } getMarkerHTML() { - return `
+ return `
` } - getMarkerCategory() - { + getMarkerCategory() { // Overloaded by child classes return ""; } + /********************** Unit data *************************/ + setData(data: UpdateData) { - var updateMarker = false; - - if ((data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude)) - || (data.flightData.heading != undefined && this.getFlightData().heading != data.flightData.heading) - || (data.baseData.alive != undefined && this.getBaseData().alive != data.baseData.alive) - || this.#forceUpdate || !getMap().hasLayer(this)) - updateMarker = true; - - if (data.baseData != undefined) - { + /* 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); + var updateMarker = (positionChanged || headingChanged || aliveChanged || !getMap().hasLayer(this)); + + if (data.baseData != undefined) { for (let key in this.#data.baseData) if (key in data.baseData) //@ts-ignore this.#data.baseData[key] = data.baseData[key]; } - if (data.flightData != undefined) - { + if (data.flightData != undefined) { for (let key in this.#data.flightData) if (key in data.flightData) //@ts-ignore this.#data.flightData[key] = data.flightData[key]; } - if (data.missionData != undefined) - { + if (data.missionData != undefined) { for (let key in this.#data.missionData) if (key in data.missionData) //@ts-ignore this.#data.missionData[key] = data.missionData[key]; } - if (data.formationData != undefined) - { + if (data.formationData != undefined) { for (let key in this.#data.formationData) if (key in data.formationData) //@ts-ignore this.#data.formationData[key] = data.formationData[key]; } - if (data.taskData != undefined) - { + if (data.taskData != undefined) { for (let key in this.#data.taskData) if (key in data.taskData) //@ts-ignore this.#data.taskData[key] = data.taskData[key]; } - if (data.optionsData != undefined) - { + if (data.optionsData != undefined) { for (let key in this.#data.optionsData) if (key in data.optionsData) //@ts-ignore this.#data.optionsData[key] = data.optionsData[key]; } + /* 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()) @@ -268,32 +264,32 @@ export class Unit extends Marker { return this.#selectable; } - addDestination(latlng: L.LatLng) { - 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); + setHotgroup(hotgroup: number | null) { + this.#hotgroup = hotgroup; } - clearDestinations() { - this.getTaskData().activePath = undefined; + getHotgroup() { + return this.#hotgroup; } - updateVisibility() - { - this.setHidden( document.body.getAttribute(`data-hide-${this.getMissionData().coalition}`) != null || - document.body.getAttribute(`data-hide-${this.getMarkerCategory()}`) != null || - !this.getBaseData().alive) + setHighlighted(highlighted: boolean) { + this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-highlighted", highlighted); + this.#highlighted = highlighted; } - setHidden(hidden: boolean) - { - this.#hidden = hidden; + getHighlighted() { + return this.#highlighted; + } + + /********************** Visibility *************************/ + updateVisibility() { + this.setHidden(document.body.getAttribute(`data-hide-${this.getMissionData().coalition}`) != null || + document.body.getAttribute(`data-hide-${this.getMarkerCategory()}`) != null || + !this.getBaseData().alive) + } + + setHidden(hidden: boolean) { + this.#hidden = hidden; /* Add the marker if not present */ if (!getMap().hasLayer(this) && !this.getHidden()) { @@ -303,7 +299,7 @@ export class Unit extends Marker { /* Hide the marker if necessary*/ if (getMap().hasLayer(this) && this.getHidden()) { getMap().removeLayer(this); - } + } } getHidden() { @@ -314,94 +310,114 @@ export class Unit extends Marker { return getUnitsManager().getUnitByID(this.getFormationData().leaderID); } - getFormation() { - return [this].concat(this.getWingmen()) + /********************** 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); + } } - getWingmen() { - var wingmen: Unit[] = []; - if (this.getFormationData().wingmenIDs != undefined) { - for (let ID of this.getFormationData().wingmenIDs) { - var unit = getUnitsManager().getUnitByID(ID) - if (unit) - wingmen.push(unit); - } - } - return wingmen; + clearDestinations() { + if (!this.getMissionData().flags.Human) + this.getTaskData().activePath = undefined; } attackUnit(targetID: number) { - if (this.ID != targetID) { - attackUnit(this.ID, targetID); - } - else { - // TODO: show a message - } + /* 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}) { - if (this.ID != targetID) { - followUnit(this.ID, targetID, offset); - } - else { - // TODO: show a message - } + 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) { - landAt(this.ID, latlng); + if (!this.getMissionData().flags.Human) + landAt(this.ID, latlng); } changeSpeed(speedChange: string) { - changeSpeed(this.ID, speedChange); + if (!this.getMissionData().flags.Human) + changeSpeed(this.ID, speedChange); } changeAltitude(altitudeChange: string) { - changeAltitude(this.ID, altitudeChange); + if (!this.getMissionData().flags.Human) + changeAltitude(this.ID, altitudeChange); } setSpeed(speed: number) { - setSpeed(this.ID, speed); + if (!this.getMissionData().flags.Human) + setSpeed(this.ID, speed); } setAltitude(altitude: number) { - setAltitude(this.ID, altitude); + if (!this.getMissionData().flags.Human) + setAltitude(this.ID, altitude); } setROE(ROE: string) { - setROE(this.ID, ROE); + if (!this.getMissionData().flags.Human) + setROE(this.ID, ROE); } setReactionToThreat(reactionToThreat: string) { - setReactionToThreat(this.ID, reactionToThreat); + if (!this.getMissionData().flags.Human) + setReactionToThreat(this.ID, reactionToThreat); } setLeader(isLeader: boolean, wingmenIDs: number[] = []) { - setLeader(this.ID, isLeader, wingmenIDs); + if (!this.getMissionData().flags.Human) + setLeader(this.ID, isLeader, wingmenIDs); } delete() { + // TODO: add confirmation popup deleteUnit(this.ID); } refuel() { - refuel(this.ID); + if (!this.getMissionData().flags.Human) + refuel(this.ID); } setAdvancedOptions(isTanker: boolean, isAWACS: boolean, TACANChannel: number, TACANXY: string, TACANcallsign: string, radioFrequency: number, radioCallsign: number, radioCallsignNumber: number) { - setAdvacedOptions(this.ID, isTanker, isAWACS, TACANChannel, TACANXY, TACANcallsign, radioFrequency, radioCallsign, radioCallsignNumber); + if (!this.getMissionData().flags.Human) + setAdvacedOptions(this.ID, isTanker, isAWACS, TACANChannel, TACANXY, TACANcallsign, radioFrequency, radioCallsign, radioCallsignNumber); } + /***********************************************/ + onAdd(map: Map): this { + super.onAdd(map); + getMap().removeTemporaryMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + return this; + } + + /***********************************************/ #onClick(e: any) { - this.#timer = window.setTimeout(() => { - if (!this.#preventClick) { - if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { - if (!e.originalEvent.ctrlKey) { - getUnitsManager().deselectAllUnits(); - } - this.setSelected(true); + if (!this.#preventClick) { + if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { + if (!e.originalEvent.ctrlKey) { + getUnitsManager().deselectAllUnits(); } + this.setSelected(!this.getSelected()); } + } + + this.#timer = window.setTimeout(() => { this.#preventClick = false; }, 200); } @@ -412,27 +428,22 @@ export class Unit extends Marker { } #onContextMenu(e: any) { - var options: {[key: string]: string} = {}; + var options: { [key: string]: string } = {}; - options["Center"] = `
Center map
`; + options["Center"] = `
Center map
`; - if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().includes(this))) - { - options = { - 'Attack': `
Attack
`, - 'Follow': `
Follow
`, - } + if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().length == 1 && (getUnitsManager().getSelectedUnits().includes(this)))) { + options['Attack'] = `
Attack
`; + if (getUnitsManager().getSelectedUnitsType() === "Aircraft") + options['Follow'] = `
Follow
`; } - else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) - { - if (this.getBaseData().category == "Aircraft") - { + else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { + if (this.getBaseData().category == "Aircraft") { options["Refuel"] = `
Refuel
`; // TODO Add some way of knowing which aircraft can AAR } } - if (Object.keys(options).length > 0) - { + if (Object.keys(options).length > 0) { getMap().showUnitContextMenu(e); getMap().getUnitContextMenu().setOptions(options, (option: string) => { getMap().hideUnitContextMenu(); @@ -453,7 +464,7 @@ export class Unit extends Marker { } #showFollowOptions(e: any) { - var options: {[key: string]: string} = {}; + var options: { [key: string]: string } = {}; options = { 'Trail': `
Trail
`, @@ -462,6 +473,7 @@ export class Unit extends Marker { 'Line abreast (LH)': `
Line abreast (left)
`, 'Line abreast (RH)': `
Line abreast (right)
`, 'Front': `
In front
`, + 'Diamond': `
Diamond
`, 'Custom': `
Custom
` } @@ -472,63 +484,31 @@ export class Unit extends Marker { getMap().showUnitContextMenu(e); } - #applyFollowOptions(action: string) - { - if (action === "Custom") - { + #applyFollowOptions(action: string) { + if (action === "Custom") { document.getElementById("custom-formation-dialog")?.classList.remove("hide"); - getMap().getUnitContextMenu().setCustomFormationCallback((offset: {x: number, y: number, z: number}) => { + getMap().getUnitContextMenu().setCustomFormationCallback((offset: { x: number, y: number, z: number }) => { getUnitsManager().selectedUnitsFollowUnit(this.ID, offset); }) } else { - // X: front-rear, positive front - // Y: top-bottom, positive top - // Z: left-right, positive right - - var offset = {"x": 0, "y": 0, "z": 0}; - if (action == "Trail") - { - offset.x = -50; offset.y = -30; offset.z = 0; - } - else if (action == "Echelon (LH)") - { - offset.x = -50; offset.y = -10; offset.z = -50; - } - else if (action == "Echelon (RH)") - { - offset.x = -50; offset.y = -10; offset.z = 50; - } - else if (action == "Line abreast (RH)") - { - offset.x = 0; offset.y = 0; offset.z = 50; - } - else if (action == "Line abreast (LH)") - { - offset.x = 0; offset.y = 0; offset.z = -50; - } - else if (action == "Front") - { - offset.x = 100; offset.y = 0; offset.z = 0; - } - getUnitsManager().selectedUnitsFollowUnit(this.ID, offset); + getUnitsManager().selectedUnitsFollowUnit(this.ID, undefined, action); } } #updateMarker() { this.updateVisibility(); - if (this.getBaseData().alive ) - { - if (this.#miniMapMarker == null) - { - this.#miniMapMarker = new CircleMarker(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude), {radius: 0.5}); + /* 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"}); + this.#miniMapMarker.setStyle({ color: "#CFD9E8" }); else if (this.getMissionData().coalition == "red") - this.#miniMapMarker.setStyle({color: "#ff5858"}); - else - this.#miniMapMarker.setStyle({color: "#247be2"}); + this.#miniMapMarker.setStyle({ color: "#ff5858" }); + else + this.#miniMapMarker.setStyle({ color: "#247be2" }); this.#miniMapMarker.addTo(getMap().getMiniMapLayerGroup()); this.#miniMapMarker.bringToBack(); } @@ -544,47 +524,85 @@ export class Unit extends Marker { } } + /* 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;`); - element.querySelector(".unit")?.setAttribute("data-pilot", this.getMissionData().flags.human? "human": "ai"); + /* 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); - if (this.getMissionData().flags.Human) // Unit is human + /* Set current unit state */ + if (this.getMissionData().flags.Human) // Unit is human element.querySelector(".unit")?.setAttribute("data-state", "human"); - else if (!this.getBaseData().AI) // Unit is under DCS control (no Olympus) + else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus) element.querySelector(".unit")?.setAttribute("data-state", "dcs"); - else // Unit is under Olympus control + else // Unit is under Olympus control element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase()); - var unitAltitudeDiv = element.querySelector(".unit-altitude"); - if (unitAltitudeDiv != null) - unitAltitudeDiv.innerHTML = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 1000)); - - var unitSpeedDiv = element.querySelector(".unit-speed"); - if (unitSpeedDiv != null) - unitSpeedDiv.innerHTML = String(Math.floor(this.getFlightData().speed * 1.94384 ) ); - - 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);` ); + /* Set altitude and speed */ + if (element.querySelector(".unit-altitude")) + (element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 100)); + if (element.querySelector(".unit-speed")) + (element.querySelector(".unit-speed")).innerHTML = String(Math.floor(this.getFlightData().speed * 1.94384)); + + /* 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 ordnance 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); + } } - var pos = getMap().latLngToLayerPoint(this.getLatLng()).round(); - this.setZIndexOffset(1000 + Math.floor(this.getFlightData().altitude) - pos.y); - } - this.#forceUpdate = false; + /* 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() { @@ -643,7 +661,7 @@ export class Unit extends Marker { 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 }); targetPolyline.addTo(getMap()); this.#targetsPolylines.push(targetPolyline) } @@ -667,8 +685,7 @@ export class Aircraft extends AirUnit { super(ID, data); } - getMarkerHTML() - { + getMarkerHTML() { return `
@@ -696,8 +713,7 @@ export class Aircraft extends AirUnit {
` } - getMarkerCategory() - { + getMarkerCategory() { return "aircraft"; } } @@ -707,8 +723,7 @@ export class Helicopter extends AirUnit { super(ID, data); } - getVisibilityCategory() - { + getVisibilityCategory() { return "helicopter"; } } @@ -720,15 +735,17 @@ export class GroundUnit extends Unit { getMarkerHTML() { var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0]; - return `
+ return `
${role?.substring(0, 1)?.toUpperCase() || ""}
+
+
+
` } - getMarkerCategory() - { + getMarkerCategory() { // TODO this is very messy var role = groundUnitsDatabase.getByName(this.getBaseData().name)?.loadouts[0].roles[0]; var markerCategory = (role === "SAM") ? "sam" : "groundunit"; @@ -740,7 +757,7 @@ export class NavyUnit extends Unit { constructor(ID: number, data: UnitData) { super(ID, data); } - + getMarkerCategory() { return "navyunit"; } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 7c6086d5..40adf4dc 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -1,5 +1,5 @@ import { LatLng, LatLngBounds } from "leaflet"; -import { getInfoPopup, getMap, getUnitDataTable } from ".."; +import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from ".."; import { Unit } from "./unit"; import { cloneUnit } from "../server/server"; import { IDLE, MOVE_UNIT } from "../map/map"; @@ -20,25 +20,18 @@ export class UnitsManager { document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); document.addEventListener('keydown', (event) => this.#onKeyDown(event)); - document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete() ) + document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()) } 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 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() { @@ -52,6 +45,10 @@ 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) { /* The name of the unit category is exactly the same as the constructor name */ var constructor = Unit.getConstructor(data.baseData.category); @@ -59,58 +56,57 @@ export class UnitsManager { this.#units[ID] = new constructor(ID, data); } } - + removeUnit(ID: number) { } update(data: UnitsData) { 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); - + .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) => this.#units[parseInt(ID)]?.setData(data.units[ID])); + .filter((ID: string) => ID in this.#units) + .forEach((ID: string) => this.#units[parseInt(ID)]?.setData(data.units[ID])); } - selectUnit(ID: number, deselectAllUnits: boolean = true) - { - if (deselectAllUnits) - this.getSelectedUnits().filter((unit: Unit) => unit.ID !== ID ).forEach((unit: Unit) => unit.setSelected(false)); + 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) - { + selectFromBounds(bounds: LatLngBounds) { this.deselectAllUnits(); - for (let ID in this.#units) - { - if (this.#units[ID].getHidden() == false) - { + 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)) - { + if (bounds.contains(latlng)) { this.#units[ID].setSelected(true); } } } } - getSelectedUnits() { + getSelectedUnits(options?: {excludeHumans?: 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}); + } return selectedUnits; } @@ -120,224 +116,245 @@ export class UnitsManager { } } - getSelectedLeaders() { - var leaders: Unit[] = []; - for (let idx in this.getSelectedUnits()) - { - var unit = this.getSelectedUnits()[idx]; - if (unit.getFormationData().isLeader) - leaders.push(unit); - else if (unit.getFormationData().isWingman) - { - var leader = unit.getLeader(); - if (leader && !leaders.includes(leader)) - leaders.push(leader); - } - } - return leaders; + selectUnitsByHotgroup(hotgroup: number) { + this.deselectAllUnits(); + this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) } - getSelectedSingletons() { - var singletons: Unit[] = []; - for (let idx in this.getSelectedUnits()) - { - var unit = this.getSelectedUnits()[idx]; - if (!unit.getFormationData().isLeader && !unit.getFormationData().isWingman) - singletons.push(unit); - } - return singletons; - } - - getSelectedUnitsType () { + getSelectedUnitsType() { if (this.getSelectedUnits().length == 0) return undefined; return this.getSelectedUnits().map((unit: Unit) => { return unit.constructor.name })?.reduce((a: any, b: any) => { - return a == b? a: undefined + return a == b ? a : undefined }); }; - getSelectedUnitsTargetSpeed () { + getSelectedUnitsTargetSpeed() { if (this.getSelectedUnits().length == 0) return undefined; return this.getSelectedUnits().map((unit: Unit) => { return unit.getTaskData().targetSpeed })?.reduce((a: any, b: any) => { - return a == b? a: undefined + return a == b ? a : undefined }); }; - getSelectedUnitsTargetAltitude () { + getSelectedUnitsTargetAltitude() { if (this.getSelectedUnits().length == 0) return undefined; return this.getSelectedUnits().map((unit: Unit) => { return unit.getTaskData().targetAltitude })?.reduce((a: any, b: any) => { - return a == b? a: undefined + return a == b ? a : undefined }); }; - getSelectedUnitsCoalition () { + 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 + return a == b ? a : undefined }); }; + /*********************** Actions on selected units ************************/ selectedUnitsAddDestination(latlng: L.LatLng) { - var selectedUnits = this.getSelectedUnits(); + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - 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 + unit.addDestination(latlng); } this.#showActionMessage(selectedUnits, " new destination added"); } selectedUnitsClearDestinations() { - var selectedUnits = this.getSelectedUnits(); + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - commandedUnit.clearDestinations(); + const unit = selectedUnits[idx]; + if (unit.getTaskData().currentState === "Follow") { + const leader = this.getUnitByID(unit.getFormationData().leaderID) + if (leader && leader.getSelected()) + leader.clearDestinations(); + else + unit.clearDestinations(); + } + else + unit.clearDestinations(); } } - selectedUnitsLandAt(latlng: LatLng) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsLandAt(latlng: LatLng) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].landAt(latlng); } this.#showActionMessage(selectedUnits, " landing"); } - selectedUnitsChangeSpeed(speedChange: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsChangeSpeed(speedChange: string) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].changeSpeed(speedChange); } } - selectedUnitsChangeAltitude(altitudeChange: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsChangeAltitude(altitudeChange: string) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].changeAltitude(altitudeChange); } } - selectedUnitsSetSpeed(speed: number) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsSetSpeed(speed: number) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].setSpeed(speed); } - this.#showActionMessage(selectedUnits, `setting speed to ${speed * 1.94384} kts`); } - selectedUnitsSetAltitude(altitude: number) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsSetAltitude(altitude: number) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].setAltitude(altitude); } this.#showActionMessage(selectedUnits, `setting altitude to ${altitude / 0.3048} ft`); } - selectedUnitsSetROE(ROE: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsSetROE(ROE: string) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].setROE(ROE); } this.#showActionMessage(selectedUnits, `ROE set to ${ROE}`); } - selectedUnitsSetReactionToThreat(reactionToThreat: string) - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsSetReactionToThreat(reactionToThreat: string) { + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); + for (let idx in selectedUnits) { selectedUnits[idx].setReactionToThreat(reactionToThreat); } this.#showActionMessage(selectedUnits, `reaction to threat set to ${reactionToThreat}`); } selectedUnitsAttackUnit(ID: number) { - var selectedUnits = this.getSelectedUnits(); + var selectedUnits = this.getSelectedUnits({excludeHumans: true}); for (let idx in selectedUnits) { selectedUnits[idx].attackUnit(ID); } this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getBaseData().unitName}`); } - selectedUnitsDelete() - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsDelete() { + var selectedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ + for (let idx in selectedUnits) { selectedUnits[idx].delete(); } this.#showActionMessage(selectedUnits, `deleted`); } - selectedUnitsRefuel() - { - var selectedUnits = this.getSelectedUnits(); - for (let idx in selectedUnits) - { + selectedUnitsRefuel() { + var selectedUnits = this.getSelectedUnits({excludeHumans: 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}) { - var selectedUnits = this.getSelectedUnits(); + 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}); var count = 1; + var xr = 0; var yr = 1; var zr = -1; + var layer = 1; for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - commandedUnit.followUnit(ID, {"x": offset.x * count, "y": offset.y * count, "z": offset.z * count} ); + 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}`); } - copyUnits() + selectedUnitsSetHotgroup(hotgroup: number) { - this.#copiedUnits = this.getSelectedUnits(); + 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(); + } + + /***********************************************/ + 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) - { + 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.#showActionMessage(this.#copiedUnits, `pasted`); } this.#pasteDisabled = true; window.setTimeout(() => this.#pasteDisabled = false, 250); } } - #onKeyDown(event: KeyboardEvent) - { - if ( !keyEventWasInInput( event ) && event.key === "Delete") - { + /***********************************************/ + #onKeyDown(event: KeyboardEvent) { + if (!keyEventWasInInput(event) && event.key === "Delete") { this.selectedUnitsDelete(); } } @@ -346,10 +363,9 @@ export class UnitsManager { 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) - { + if (!this.#selectionEventDisabled) { window.setTimeout(() => { - document.dispatchEvent(new CustomEvent("unitsSelection", {detail: this.getSelectedUnits()})); + document.dispatchEvent(new CustomEvent("unitsSelection", { detail: this.getSelectedUnits() })); this.#selectionEventDisabled = false; }, 100); this.#selectionEventDisabled = true; @@ -366,14 +382,14 @@ export class UnitsManager { getMap().setState(IDLE); document.dispatchEvent(new CustomEvent("clearSelection")); } - else - document.dispatchEvent(new CustomEvent("unitsDeselection", {detail: this.getSelectedUnits()})); + 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 + 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/views/connectionstatuspanel.ejs b/client/views/connectionstatuspanel.ejs index b0c7df1b..ac92da78 100644 --- a/client/views/connectionstatuspanel.ejs +++ b/client/views/connectionstatuspanel.ejs @@ -1,4 +1,4 @@ -
+
diff --git a/client/views/contextmenus.ejs b/client/views/contextmenus.ejs index b386ec9a..30628b6c 100644 --- a/client/views/contextmenus.ejs +++ b/client/views/contextmenus.ejs @@ -1,8 +1,7 @@ -
+
-
+
-
+

diff --git a/client/views/dialogs.ejs b/client/views/dialogs.ejs index 1822808d..442b89d9 100644 --- a/client/views/dialogs.ejs +++ b/client/views/dialogs.ejs @@ -1,23 +1,28 @@ -
- +
-

DCS Olympus

Dynamic Unit Command

-
Version v0.2.0
+
Version v0.2.1
+
+
Username
+
Password
+ +
+ +

+ -
-
-
+
@@ -141,7 +146,7 @@
-
+
diff --git a/client/views/hotgrouppanel.ejs b/client/views/hotgrouppanel.ejs new file mode 100644 index 00000000..0838c635 --- /dev/null +++ b/client/views/hotgrouppanel.ejs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/client/views/index.ejs b/client/views/index.ejs index c477210c..afb19752 100644 --- a/client/views/index.ejs +++ b/client/views/index.ejs @@ -24,7 +24,6 @@
<%- include('aic.ejs') %> <%- include('atc.ejs') %> - <%- include('contextmenus.ejs') %> <%- include('unitcontrolpanel.ejs') %> <%- include('unitinfopanel.ejs') %> @@ -34,6 +33,9 @@ <%- include('dialogs.ejs') %> <%- include('unitdatatable.ejs') %> <%- include('popups.ejs') %> + <%- include('hotgrouppanel.ejs') %> + +
<% /* %> <%- include('log.ejs') %> diff --git a/client/views/mouseinfopanel.ejs b/client/views/mouseinfopanel.ejs index 932e4e01..5127e4a4 100644 --- a/client/views/mouseinfopanel.ejs +++ b/client/views/mouseinfopanel.ejs @@ -1,4 +1,4 @@ -
+
diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index aad54404..045aeafb 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -1,4 +1,4 @@ -