diff --git a/client/copy.bat b/client/copy.bat index 1acc220e..f163f70e 100644 --- a/client/copy.bat +++ b/client/copy.bat @@ -1,3 +1,2 @@ copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet\\leaflet.css -copy .\\node_modules\\@iconfu\\svg-inject\\dist\\svg-inject.js .\\public\\javascripts\\svg-inject.js copy .\\node_modules\\leaflet.nauticscale\\dist\\leaflet.nauticscale.js .\\public\\javascripts\\leaflet.nauticscale.js diff --git a/client/demo.js b/client/demo.js index 292e2f52..63e7940c 100644 --- a/client/demo.js +++ b/client/demo.js @@ -50,16 +50,11 @@ const DEMO_UNIT_DATA = { currentState: "Idle", activePath: undefined, targetSpeed: 400, + targetSpeedType: "CAS", targetAltitude: 3000, + targetAltitudeType: "ASL", isTanker: false, - TACANOn: false, - TACANChannel: 32, - TACANXY: "Y", - TACANCallsign: "ASD", - radioFrequency: 123.750, - radioCallsign: 2, - radioCallsignNumber: 3, - radioAMFM: "FM" + }, optionsData: { ROE: "Designated", @@ -112,7 +107,7 @@ const DEMO_UNIT_DATA = { ["3"]:{ baseData: { AI: true, - name: "2S6 Tunguska", + name: "M-60", unitName: "Olympus 1-3", groupName: "Group 4", alive: true, @@ -145,7 +140,8 @@ const DEMO_UNIT_DATA = { currentTask: "Example task", activePath: undefined, targetSpeed: 400, - targetAltitude: 3000 + targetAltitude: 3000, + onOff: false }, optionsData: { ROE: "None", @@ -157,7 +153,6 @@ const DEMO_UNIT_DATA = { AI: true, name: "2S6 Tunguska", unitName: "Olympus 1-4", - groupName: "Group 1", alive: true, category: "GroundUnit", }, diff --git a/client/package-lock.json b/client/package-lock.json index e6e672a7..e627bd43 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "DCSOlympus", - "version": "v0.2.1-alpha", + "version": "v0.3.0-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "DCSOlympus", - "version": "v0.2.1-alpha", + "version": "v0.3.0-alpha", "dependencies": { "@types/geojson": "^7946.0.10", "@types/leaflet": "^1.9.0", diff --git a/client/package.json b/client/package.json index f0db50cb..9f2fec90 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.1-alpha", + "version": "v0.3.0-alpha", "private": true, "scripts": { "copy": "copy.bat", diff --git a/client/public/stylesheets/aic/aic.css b/client/public/stylesheets/aic/aic.css index 16022448..ea972d71 100644 --- a/client/public/stylesheets/aic/aic.css +++ b/client/public/stylesheets/aic/aic.css @@ -130,7 +130,7 @@ padding: 10px; position: absolute; width: fit-content; - z-index: 1000; + z-index: 9999; } .aic-enabled #aic-teleprompt { diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index 91926b4b..7ae3c7b5 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -11,7 +11,11 @@ left: 10px; position: absolute; top: 10px; - z-index: 1000; + z-index: 9999; +} + +#app-icon>.ol-select-options { + width: fit-content; } #toolbar-summary { @@ -25,13 +29,17 @@ text-indent: 60px; } +#toolbar-summary { + white-space: nowrap; +} + #connection-status-panel { bottom: 20px; font-size: 12px; position: absolute; right: 10px; - width: 160px; - z-index: 1000; + width: 180px; + z-index: 9999; } #mouse-info-panel { @@ -42,8 +50,8 @@ position: absolute; right: 10px; row-gap: 10px; - width: 160px; - z-index: 1000; + width: 180px; + z-index: 9999; } #unit-control-panel { @@ -51,8 +59,8 @@ left: 10px; position: absolute; top: 80px; - width: 240px; - z-index: 1000; + width: 320px; + z-index: 9999; } #unit-info-panel { @@ -61,7 +69,8 @@ left: 10px; position: absolute; width: fit-content; - z-index: 1000; + z-index: 9999; + padding: 24px 30px; } #info-popup { diff --git a/client/public/stylesheets/markers/units.css b/client/public/stylesheets/markers/units.css index 43bc53a0..65ec1d4b 100644 --- a/client/public/stylesheets/markers/units.css +++ b/client/public/stylesheets/markers/units.css @@ -1,9 +1,3 @@ -:root { - /* Generic marker settings */ - --unit-centre-x: calc(var(--unit-width) / 2); - --unit-centre-y: calc(var(--unit-height) / 2); -} - /*** Unit marker elements ***/ [data-object|="unit"] { align-items: center; @@ -266,7 +260,14 @@ background-image: url("/resources/theme/images/states/idle.svg"); } -[data-object|="unit"][data-state="attack"] .unit-state { +[data-object*="groundunit"][data-state="idle"] .unit-state { + background-image: url(""); /* To avoid clutter, dont show the idle state for non flying units */ +} + +[data-object|="unit"][data-state="attack"] .unit-state, +[data-object|="unit"][data-state="bombing point"] .unit-state, +[data-object|="unit"][data-state="carpet bombing"] .unit-state, +[data-object|="unit"][data-state="firing at area"] .unit-state { background-image: url("/resources/theme/images/states/attack.svg"); } @@ -286,6 +287,10 @@ background-image: url("/resources/theme/images/states/dcs.svg"); } +[data-object|="unit"][data-state="no-task"] .unit-state { + background-image: url("/resources/theme/images/states/no-task.svg"); +} + /*** Dead unit ***/ [data-object|="unit-aircraft"][data-is-dead] .unit-selected-spotlight, [data-object|="unit-aircraft"][data-is-dead] .unit-short-label, diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 92e7573b..d4afca30 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -34,6 +34,10 @@ body { width: 100%; } +.hidden-cursor { + cursor: none !important; +} + a { text-decoration: none; } @@ -50,6 +54,7 @@ button { cursor: pointer; font-weight: var(--font-weight-bolder); padding: 6px; + column-gap: 5px; } button:hover { @@ -61,6 +66,14 @@ button[disabled="disabled"] { cursor: not-allowed; } +button>svg:first-child, +button>img:first-child { + position: relative; + aspect-ratio: initial; + height: 100%; + pointer-events: none; +} + form { margin: 0; padding: 0; @@ -121,10 +134,10 @@ form>div { } .ol-panel hr { - background-color: var(--secondary-light-grey); + background-color: var(--secondary-transparent-white); border: none; height: 1px; - margin: 20px 0; + margin: 10px 0; width: 100%; } @@ -194,7 +207,7 @@ form>div { max-height: 0; overflow: hidden; position: absolute; - z-index: 1000; + z-index: 9999; } .ol-select-options.scrollbar-visible { @@ -308,16 +321,15 @@ form>div { .ol-panel-board>.panel-section { border-right: 1px solid #555; - margin: 10px 0; padding: 0 30px; } .ol-panel-board>.panel-section:first-child { - padding-left: 20px; + padding-left: 0px; } .ol-panel-board>.panel-section:last-child { - padding-right: 20px; + padding-right: 0px; } .ol-panel-board>.panel-section:last-of-type { @@ -356,6 +368,12 @@ h4 { button.ol-button-warning { border: 1px solid var(--primary-red); color: var(--primary-red); + font-weight: bold; +} + +button.ol-button-warning>svg:first-child { + stroke: var(--primary-red); + fill: var(--primary-red); } nav.ol-panel { @@ -482,17 +500,22 @@ nav.ol-panel> :last-child { flex-direction: column; } -.slider-container { +.ol-slider-container { width: 100%; } -.slider { +.ol-slider-container:not(:first-of-type) { + margin-top: 10px; + width: 100%; +} + +.ol-slider { -webkit-appearance: none; appearance: none; background: #d3d3d3; height: 2px; margin-bottom: 10px; - margin-top: 10px; + margin-top: 15px; opacity: 0.7; outline: none; -webkit-transition: .2s; @@ -500,34 +523,51 @@ nav.ol-panel> :last-child { width: 100%; } -.slider:hover { +.ol-slider:hover { opacity: 1; } -.slider::-webkit-slider-thumb { +.ol-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; - background: gray; + background: var(--background-grey); border-radius: 999px; cursor: pointer; - height: 20px; - width: 20px; + height: 25px; + width: 25px; } -.active .slider::-webkit-slider-thumb { - background: #5ca7ff; +.active .ol-slider::-webkit-slider-thumb { + background: radial-gradient(circle at center, var(--accent-light-blue), var(--accent-light-blue) 40%, color-mix(in srgb, var(--accent-light-blue), transparent 66%) 50%); } -.slider::-moz-range-thumb { - background: gray; +.ol-slider::-moz-range-thumb { + -moz-appearance: none; + border: 0px solid transparent; + background: var(--background-grey); border-radius: 999px; cursor: pointer; - height: 20px; - width: 20px; + height: 25px; + width: 25px; } -.active .slider::-moz-range-thumb { - background: #5ca7ff; +.active .ol-slider::-moz-range-thumb { + -moz-appearance: none; + background: radial-gradient(circle at center, var(--accent-light-blue), var(--accent-light-blue) 40%, color-mix(in srgb, var(--accent-light-blue), transparent 66%) 50%); +} + +.ol-slider-min-max { + display: flex; + justify-content: space-between; + color: var(--secondary-light-grey); +} + +.ol-slider-min-max::before { + content: attr(data-min-value); +} + +.ol-slider-min-max::after { + content: attr(data-max-value); } .main-logo { @@ -538,9 +578,9 @@ nav.ol-panel> :last-child { .ol-measure-box { background-color: var(--background-steel); border-radius: 999px; - color: var(--primary-neutral); + color: var(--background-offwhite); font-size: 12px; - font-weight: var(--font-weight-bolder); + font-weight: bolder; height: fit-content; padding-bottom: 0.2em; padding-left: 0.5em; @@ -550,6 +590,7 @@ nav.ol-panel> :last-child { text-align: center; width: fit-content; z-index: 2000; + pointer-events: none; } .ol-sortable .handle { @@ -672,7 +713,6 @@ nav.ol-panel> :last-child { position: relative; row-gap: 10px; width: 50%; - z-index: 10; } #splash-content::after { @@ -823,15 +863,18 @@ nav.ol-panel> :last-child { } .ol-destination-preview-icon { - background-color: var(--secondary-yellow); - border-radius: 999px; + background-image: url("/resources/theme/images/markers/move.svg"); height: 52px; pointer-events: none; width: 52px; } -.ol-destination-preview { +.ol-target-icon { + background-image: url("/resources/theme/images/markers/target.svg"); + height: 52px; pointer-events: none; + width: 52px; + z-index: 9999; } dl.ol-data-grid { @@ -843,12 +886,8 @@ dl.ol-data-grid { row-gap: 4px; } -dl.ol-data-grid dt { - width: 60%; -} - dl.ol-data-grid dd { - width: 40%; + width: fit-content; } dl.ol-data-grid dt.icon { @@ -877,18 +916,6 @@ dl.ol-data-grid dd { margin-left: auto; } -.br-info::after { - content: attr(data-bearing) '\00B0 / ' attr(data-distance) attr(data-distance-units); -} - -.br-info[data-message]::after { - content: attr(data-message); -} - -.coordinates::after { - content: attr(data-dd) "\00b0 " attr(data-mm) "'" attr(data-ss) "." attr(data-sss) '"' attr(data-label); -} - .ol-button-box { column-gap: 6px; display: flex; @@ -911,7 +938,7 @@ dl.ol-data-grid dd { color: white; justify-self: center; position: absolute; - z-index: 1000; + z-index: 9999; } .ol-panel.ol-dialog { @@ -1049,4 +1076,61 @@ input[type=number]::-webkit-outer-spin-button { filter: invert(100%); height: 24px; width: 24px; +} + +.ol-switch { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.ol-switch-input { + display: none; +} + +.ol-switch-fill { + border-radius: 999px; + position: relative; + transition: background-color 0.2s; + height: var(--height); + width: var(--width); +} + +.ol-switch-fill::after { + aspect-ratio : 1 / 1; + background-clip: content-box; + background-color: #ffffff; + border-radius: 999px; + box-sizing: border-box; + content: ""; + height: 100%; + padding: 3px; + position: absolute; + transition: transform 0.2s; + top: 0px; +} + +.ol-switch-fill::before { + align-items: center; + box-sizing: border-box; + color: white; + display: flex; + font-size: 11px; + height: 100%; + padding: 0px 7px; + position: absolute; + transition: transform 0.2s; +} + +.ol-switch[data-value="false"]>.ol-switch-fill::before { + transform: translateX(calc(var(--width) - 100%)); +} + +.ol-switch[data-value="true"]>.ol-switch-fill::after { + transform: translateX(calc(var(--width) - var(--height))); +} + +.ol-switch[data-value="undefined"]>.ol-switch-fill::after { + transform: translateX(calc((var(--width) - var(--height)) * 0.5)); } \ No newline at end of file diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index 9e325678..9f50a4d4 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -4,7 +4,7 @@ height: fit-content; position: absolute; row-gap: 5px; - width: 230px; + width: 280px; z-index: 9999; } @@ -29,8 +29,22 @@ width: fit-content; } -#context-menu-switch { +#coalition-switch { margin-right: 10px; + height: 25px; + width: 50px; +} + +#coalition-switch[data-value="false"]>.ol-switch-fill { + background-color: var(--primary-blue); +} + +#coalition-switch[data-value="true"]>.ol-switch-fill { + background-color: var(--primary-red); +} + +#coalition-switch[data-value="undefined"]>.ol-switch-fill { + background-color: var(--primary-neutral); } #map-contextmenu>div:nth-child(2) { @@ -95,6 +109,11 @@ background-size: 48px; } +#explosion-spawn-button { + background-image: url("/resources/theme/images/buttons/spawn/explosion.svg"); + background-size: 48px; +} + .unit-spawn-button { border: none; border-radius: 0px; @@ -109,7 +128,6 @@ border-top-right-radius: var(--border-radius-sm); } -[data-active-coalition="blue"].toggle-fill, [data-active-coalition="blue"].unit-spawn-button:hover, [data-active-coalition="blue"].unit-spawn-button.is-open, [data-active-coalition="blue"]#active-coalition-label, @@ -118,7 +136,6 @@ background-color: var(--primary-blue) } -[data-active-coalition="red"].toggle-fill, [data-active-coalition="red"].unit-spawn-button:hover, [data-active-coalition="red"].unit-spawn-button.is-open, [data-active-coalition="red"]#active-coalition-label, @@ -127,7 +144,6 @@ 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, @@ -154,18 +170,6 @@ 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"; } @@ -209,6 +213,7 @@ text-align: center; } +#explosion-menu>button, #smoke-spawn-menu>button { align-items: center; column-gap: 10px; @@ -246,6 +251,17 @@ background-color: orange; } +#aircraft-spawn-menu .ol-slider-value { + color: var(--accent-light-blue); + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +#aircraft-spawn-altitude-slider { + padding: 0px 10px; +} + /* Unit context menu */ #unit-contextmenu { display: flex; @@ -255,7 +271,7 @@ position: absolute; row-gap: 5px; width: fit-content; - z-index: 1000; + z-index: 9999; } #unit-contextmenu button { @@ -291,6 +307,18 @@ content: url("/resources/theme/images/icons/sword.svg"); } +#bomb::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + +#carpet-bomb::before { + content: url("/resources/theme/images/icons/explosion-solid.svg"); +} + +#fire-at-area::before { + content: url("/resources/theme/images/icons/crosshairs-solid.svg"); +} + #follow::before { content: url("/resources/theme/images/icons/follow.svg"); } @@ -377,39 +405,5 @@ position: absolute; row-gap: 5px; width: 180px; - z-index: 1000; + z-index: 9999; } - -.toggle { - --width: 40px; - --height: calc(var(--width) / 2); - --border-radius: calc(var(--height) / 2); - cursor: pointer; - - display: inline-block; -} - -.toggle-input { - display: none; -} - -.toggle-fill { - border-radius: var(--border-radius); - height: var(--height); - position: relative; - transition: background-color 0.2s; - width: var(--width); -} - -.toggle-fill::after { - background-color: #ffffff; - border-radius: var(--border-radius); - box-shadow: 0 0 10px rgba(0, 0, 0, 0.25); - content: ""; - height: calc(var(--height) - 4px); - left: 2; - position: absolute; - top: 2; - transition: transform 0.2s; - width: calc(var(--height) - 4px); -} \ No newline at end of file diff --git a/client/public/stylesheets/panels/mouseinfo.css b/client/public/stylesheets/panels/mouseinfo.css index 1c3d5e2e..e32f4540 100644 --- a/client/public/stylesheets/panels/mouseinfo.css +++ b/client/public/stylesheets/panels/mouseinfo.css @@ -6,11 +6,11 @@ #mouse-info-panel dl { margin-bottom: 4px; - row-gap: 8px; + row-gap: 5px; } #mouse-info-panel dt { - height: 20px; + height: fit-content; width: 30%; } @@ -30,20 +30,22 @@ width: 16px; } -#mouse-info-panel dt#ref-unit-position::after { - background-image: url("/resources/theme/images/icons/ruler.svg"); - background-position: 50% 50%; - background-repeat: no-repeat; - background-size: 16px 16px; - content: ""; +#mouse-info-panel #measuring-tool dt { + height: 24px; + width: 24px; + background-color: var(--background-offwhite); + border-radius: var(--border-radius-sm); } -#mouse-info-panel dt#ref-measure-position::after { - background-image: url("/resources/theme/images/icons/pin.png"); - background-position: 50% 50%; - background-repeat: no-repeat; - background-size: 16px 16px; - content: ""; +#mouse-info-panel #measuring-tool svg { + padding: 3px; + height: 100%; + width: 100%; +} + +#mouse-info-panel #measuring-tool dt svg>* { + fill: black; + stroke: black; } #mouse-info-panel dt[data-label]::after { @@ -72,4 +74,36 @@ #mouse-info-panel dd { width: 70%; +} + +.br-info::after { + content: attr(data-bearing) '\00B0 / ' attr(data-distance) " " attr(data-distance-units); + font-weight: bold; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--background-offwhite); +} + +.br-info[data-coalition="blue"]::after { + color: var(--primary-blue) +} + +.br-info[data-coalition="red"]::after { + color: var(--primary-red) +} + +.br-info[data-message]::after { + content: attr(data-message); +} + +.coordinates::after { + content: attr(data-dd) "\00b0 " attr(data-mm) "'" attr(data-ss) "." attr(data-sss) '"' attr(data-label); + font-weight: bold; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--background-offwhite); } \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 521ec15f..0ad67eb6 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -2,6 +2,12 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { display: block !important; } +#unit-control-panel { + display: flex; + flex-direction: column; + row-gap: 10px; +} + #unit-control-panel h3 { margin-bottom: 8px; } @@ -23,43 +29,28 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { display: flex; font-size: 11px; height: 32px; + justify-content: space-between; margin-right: 5px; - padding: 8px 0; position: relative; width: calc(100% - 5px); } -#unit-control-panel #selected-units-container button::before { - background-color: var(--primary-neutral); +#unit-control-panel #selected-units-container button::after { border-radius: 999px; - content: attr(data-short-label); + color: var(--secondary-semitransparent-white); + content: attr(data-label); font-size: 10px; - margin: 2px 4px; - max-width: 30px; - min-width: 20px; - overflow: hidden; padding: 4px 6px; - text-overflow: ellipsis; white-space: nowrap; width: fit-content; } -#unit-control-panel #selected-units-container button:hover::before { - background-color: black; +#unit-control-panel #selected-units-container button:hover::after { max-width: 100%; text-overflow: unset; } -#unit-control-panel #selected-units-container button[data-coalition="blue"]::before { - 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) -} - -#unit-control-panel #selected-units-container button::after { +#unit-control-panel #selected-units-container button::before { border-radius: var(--border-radius-sm); content: attr(data-callsign); display: block; @@ -76,40 +67,10 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { margin-bottom: 8px; } -#unit-control-panel #threat, -#unit-control-panel #roe, -#unit-control-panel #emissions-countermeasures { - margin-top: 12px; -} - #advanced-settings-dialog { width: 400px; } -#advanced-settings-dialog:not([data-show-settings]) #general-settings { - display: none; -} - -#advanced-settings-dialog:not([data-show-tasking]) #tasking { - display: none; -} - -#advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox { - display: none; -} - -#advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox { - display: none; -} - -#advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options { - display: none; -} - -#advanced-settings-dialog:not([data-show-radio]) #radio-options { - display: none; -} - #advanced-settings-dialog>.ol-dialog-content { display: flex; flex-direction: column; @@ -147,4 +108,119 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { #general-settings-grid>div { width: 49%; +} + +#flight-data .ol-slider { + margin: 20px 0px; +} + +.ol-slider-container dd { + column-gap: 5px; +} + +#flight-data .ol-switch { + height: 20px; + width: 50px; +} + +#flight-data .ol-switch-fill { + background-color: var(--accent-light-blue); +} + +#flight-data .ol-switch-fill::after { + background-color: white; +} + +#altitude-type-switch[data-value="true"]>.ol-switch-fill::before { + content: "AGL"; +} + +#altitude-type-switch[data-value="false"]>.ol-switch-fill::before { + content: "ASL"; +} + +#speed-type-switch[data-value="true"]>.ol-switch-fill::before { + content: "GS"; +} + +#speed-type-switch[data-value="false"]>.ol-switch-fill::before { + content: "CAS"; +} + +#unit-control-panel .ol-slider-value { + color: var(--accent-light-blue); + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +#unit-control-panel .switch-control { + align-items: center; + display: grid; + grid-template-columns: 1.35fr 0.65fr; +} + +#unit-control-panel .switch-control>*:nth-child(2) { + justify-self: end; +} + +#unit-control-panel .switch-control>*:nth-child(3) { + color: var(--secondary-semitransparent-white); +} + +#unit-control-panel .switch-control h4 { + margin: 0px; +} + +#unit-control-panel .switch-control .ol-switch { + height: 25px; + width: 60px; +} + +#unit-control-panel .switch-control .ol-switch-fill { + background-color: var(--accent-light-blue); +} + +#unit-control-panel .switch-control .ol-switch-fill::after { + background-color: white; +} + +#unit-control-panel .switch-control .ol-switch[data-value="true"]>.ol-switch-fill::before { + content: "YES"; +} + +#unit-control-panel .switch-control .ol-switch[data-value="false"]>.ol-switch-fill::before { + content: "NO"; +} + +#advanced-settings-div { + column-gap: 5px; + display: flex; +} + +#advanced-settings-div>*:nth-child(2) { + margin-left: auto; +} + +#advanced-settings-div button { + height: 40px; +} + +/* Element visibility control */ +#unit-control-panel:not([data-show-categories-tooltip]) #categories-tooltip, +#unit-control-panel:not([data-show-speed-slider]) #speed-slider, +#unit-control-panel:not([data-show-altitude-slider]) #altitude-slider, +#unit-control-panel:not([data-show-roe]) #roe, +#unit-control-panel:not([data-show-threat]) #threat, +#unit-control-panel:not([data-show-emissions-countermeasures]) #emissions-countermeasures, +#unit-control-panel:not([data-show-on-off]) #ai-on-off, +#unit-control-panel:not([data-show-follow-roads]) #follow-roads, +#unit-control-panel:not([data-show-advanced-settings-button]) #advanced-settings-button, +#advanced-settings-dialog:not([data-show-settings]) #general-settings, +#advanced-settings-dialog:not([data-show-tasking]) #tasking, +#advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox, +#advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox, +#advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options, +#advanced-settings-dialog:not([data-show-radio]) #radio-options { + display: none; } \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitinfo.css b/client/public/stylesheets/panels/unitinfo.css index 8e20e07d..95703f8a 100644 --- a/client/public/stylesheets/panels/unitinfo.css +++ b/client/public/stylesheets/panels/unitinfo.css @@ -1,15 +1,38 @@ -#unit-info-panel #unit-name { - padding: 0px 0; - margin-bottom: 4px; +#unit-info-panel>* { + position: relative; + min-height: 100px; + bottom: 0px; } -#unit-info-panel #current-task { +#general { + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 4px; + position: relative; +} + +#unit-label { + font-weight: bold; +} + +#unit-control { + color: var(--secondary-lighter-grey); + font-weight: bold; +} + +#unit-name { + margin-bottom: 4px; + padding: 0px 0; +} + +#current-task { border-radius: var(--border-radius-lg); - margin-top: 8px; + margin-top: auto; padding: 6px 16px; } -#unit-info-panel #current-task::after { +#current-task::after { content: attr(data-current-task); display: block; } @@ -17,25 +40,21 @@ #loadout { display: flex; overflow: visible; + width: 100%; + min-width: 125px; +} + +#loadout-container { + display: flex; + flex-direction: column; + justify-content: space-between; } #loadout-silhouette { - align-items: center; - display: flex; - justify-content: center; - width: 100px; -} - -#loadout-silhouette::before { - background-image: var(--loadout-background-image); - background-repeat: no-repeat; - background-size: 75px 75px; - content: ""; - display: block; filter: invert(100%); - height: 75px; - translate: -10px 0; - width: 75px; + height: 100px; + margin-right: 25px; + width: 100px; } #loadout-items { @@ -45,37 +64,37 @@ row-gap: 8px; } - #loadout-items>* { align-items: center; column-gap: 8px; display: flex; - justify-content: flex-end; white-space: nowrap; } #loadout-items>*::before { align-items: center; background-color: var(--secondary-light-grey); - border-radius: var(--border-radius-sm); + border-radius: 999px; content: attr(data-qty); display: flex; - font-weight: var(--font-weight-bolder); - padding: 1px 4px; + font-size: 11px; + font-weight: bold; + padding: 4px 6px; } #loadout-items>*::after { content: attr(data-item); + max-width: 125px; overflow: hidden; position: relative; text-overflow: ellipsis; - width: 80px; + width: 100%; } - #fuel-percentage { align-items: center; display: flex; + margin-top: auto; } #fuel-percentage::before { @@ -91,7 +110,6 @@ content: attr(data-percentage) "%"; } - #fuel-display { background-color: var(--background-grey); border-radius: var(--border-radius-md); diff --git a/client/public/themes/olympus/images/buttons/spawn/explosion.svg b/client/public/themes/olympus/images/buttons/spawn/explosion.svg new file mode 100644 index 00000000..192784b0 --- /dev/null +++ b/client/public/themes/olympus/images/buttons/spawn/explosion.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/icons/bomb-solid.svg b/client/public/themes/olympus/images/icons/bomb-solid.svg new file mode 100644 index 00000000..79718751 --- /dev/null +++ b/client/public/themes/olympus/images/icons/bomb-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/explosion-solid.svg b/client/public/themes/olympus/images/icons/explosion-solid.svg new file mode 100644 index 00000000..b6383531 --- /dev/null +++ b/client/public/themes/olympus/images/icons/explosion-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/pin.png b/client/public/themes/olympus/images/icons/pin.png deleted file mode 100644 index c6222cd2..00000000 Binary files a/client/public/themes/olympus/images/icons/pin.png and /dev/null differ diff --git a/client/public/themes/olympus/images/icons/pin.svg b/client/public/themes/olympus/images/icons/pin.svg new file mode 100644 index 00000000..e42653e9 --- /dev/null +++ b/client/public/themes/olympus/images/icons/pin.svg @@ -0,0 +1,46 @@ + + + + + + + diff --git a/client/public/themes/olympus/images/icons/plane.svg b/client/public/themes/olympus/images/icons/plane.svg new file mode 100644 index 00000000..b672646e --- /dev/null +++ b/client/public/themes/olympus/images/icons/plane.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/public/themes/olympus/images/icons/trash-can-regular.svg b/client/public/themes/olympus/images/icons/trash-can-regular.svg new file mode 100644 index 00000000..011e1a5e --- /dev/null +++ b/client/public/themes/olympus/images/icons/trash-can-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg new file mode 100644 index 00000000..bb69b55f --- /dev/null +++ b/client/public/themes/olympus/images/icons/triangle-exclamation-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/markers/move.svg b/client/public/themes/olympus/images/markers/move.svg new file mode 100644 index 00000000..4af0aa8e --- /dev/null +++ b/client/public/themes/olympus/images/markers/move.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/markers/target.svg b/client/public/themes/olympus/images/markers/target.svg new file mode 100644 index 00000000..7afbf612 --- /dev/null +++ b/client/public/themes/olympus/images/markers/target.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/public/themes/olympus/images/states/no-task.svg b/client/public/themes/olympus/images/states/no-task.svg new file mode 100644 index 00000000..2e1e906d --- /dev/null +++ b/client/public/themes/olympus/images/states/no-task.svg @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/client/public/themes/olympus/images/units/groundunit-sam.svg b/client/public/themes/olympus/images/units/groundunit-sam.svg index 996af35d..ffc18a40 100644 --- a/client/public/themes/olympus/images/units/groundunit-sam.svg +++ b/client/public/themes/olympus/images/units/groundunit-sam.svg @@ -1,4 +1,4 @@ - + diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css index a7b402af..57f6bb3f 100644 --- a/client/public/themes/olympus/theme.css +++ b/client/public/themes/olympus/theme.css @@ -31,7 +31,10 @@ --secondary-dark-steel: #181e25; --secondary-gunmetal-grey: #2f2f2f; + --secondary-lighter-grey: #949ba7; --secondary-light-grey: #797e83; + --secondary-semitransparent-white: #FFFFFFAA; + --secondary-transparent-white: #FFFFFF30; --secondary-yellow: #ffd46893; --background-hover: #f2f2f333; diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index e6b1b345..9f70f291 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -37,9 +37,15 @@ interface TaskData { currentTask: string; activePath: any; targetSpeed: number; + targetSpeedType: string; targetAltitude: number; + targetAltitudeType: string; + targetLocation: any; isTanker: boolean; isAWACS: boolean; + onOff: boolean; + followRoads: boolean; + targetID: number; } interface OptionsData { @@ -51,15 +57,6 @@ interface OptionsData { generalSettings: GeneralSettings; } -interface UnitData { - baseData: BaseData; - flightData: FlightData; - missionData: MissionData; - formationData: FormationData; - taskData: TaskData; - optionsData: OptionsData; -} - interface TACAN { isOn: boolean; channel: number; @@ -91,4 +88,13 @@ interface UnitIconOptions { showAmmo: boolean, showSummary: boolean, rotateToHeading: boolean +} + +interface UnitData { + baseData: BaseData; + flightData: FlightData; + missionData: MissionData; + formationData: FormationData; + taskData: TaskData; + optionsData: OptionsData; } \ No newline at end of file diff --git a/client/src/atc/board/tower.ts b/client/src/atc/board/tower.ts index 5c7a0fb4..c1698966 100644 --- a/client/src/atc/board/tower.ts +++ b/client/src/atc/board/tower.ts @@ -1,5 +1,6 @@ import { getUnitsManager } from "../.."; import { Dropdown } from "../../controls/dropdown"; +import { mToFt, msToKnots } from "../../other/utils"; import { ATC } from "../atc"; import { ATCBoard } from "../atcboard"; @@ -139,7 +140,7 @@ export class ATCBoardTower extends ATCBoard { assignedAltitude.value = flight.assignedAltitude; } - flightData.altitude = Math.floor( flightData.altitude / 0.3048 ); + flightData.altitude = Math.floor( mToFt(flightData.altitude) ); strip.element.querySelectorAll( `[data-point="altitude"]` ).forEach( el => { if ( el instanceof HTMLElement ) { @@ -163,7 +164,7 @@ export class ATCBoardTower extends ATCBoard { assignedSpeed.value = flight.assignedSpeed; } - flightData.speed = Math.floor( flightData.speed * 1.94384 ); + flightData.speed = Math.floor( msToKnots(flightData.speed) ); strip.element.querySelectorAll( `[data-point="speed"]` ).forEach( el => { if ( el instanceof HTMLElement ) { diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts new file mode 100644 index 00000000..0f91ed25 --- /dev/null +++ b/client/src/constants/constants.ts @@ -0,0 +1,102 @@ +import { LatLng, LatLngBounds, TileLayer, tileLayer } from "leaflet"; + +export const ROEs: string[] = ["Hold", "Return", "Designated", "Free"]; +export const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"]; +export const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"]; + +export const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"]; +export const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"]; +export const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"]; + +export const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; +export const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 }; +export const speedIncrements: { [key: string]: number } = { Aircraft: 25, Helicopter: 10, NavyUnit: 5, GroundUnit: 5 }; +export const minAltitudeValues: { [key: string]: number } = { Aircraft: 0, Helicopter: 0 }; +export const maxAltitudeValues: { [key: string]: number } = { Aircraft: 50000, Helicopter: 10000 }; +export const altitudeIncrements: { [key: string]: number } = { Aircraft: 500, Helicopter: 100 }; + +export const minimapBoundaries = [ + [ // NTTR + new LatLng(39.7982463, -119.985425), + new LatLng(34.4037128, -119.7806729), + new LatLng(34.3483316, -112.4529351), + new LatLng(39.7372411, -112.1130805), + new LatLng(39.7982463, -119.985425) + ], + [ // Syria + new LatLng(37.3630556, 29.2686111), + new LatLng(31.8472222, 29.8975), + new LatLng(32.1358333, 42.1502778), + new LatLng(37.7177778, 42.3716667), + new LatLng(37.3630556, 29.2686111) + ], + [ // Caucasus + new LatLng(39.6170191, 27.634935), + new LatLng(38.8735863, 47.1423108), + new LatLng(47.3907982, 49.3101946), + new LatLng(48.3955879, 26.7753625), + new LatLng(39.6170191, 27.634935) + ], + [ // Persian Gulf + new LatLng(32.9355285, 46.5623682), + new LatLng(21.729393, 47.572675), + new LatLng(21.8501348, 63.9734737), + new LatLng(33.131584, 64.7313594), + new LatLng(32.9355285, 46.5623682) + ], + [ // Marianas + new LatLng(22.09, 135.0572222), + new LatLng(10.5777778, 135.7477778), + new LatLng(10.7725, 149.3918333), + new LatLng(22.5127778, 149.5427778), + new LatLng(22.09, 135.0572222) + ] +]; + +export const mapBounds = { + "Syria": { bounds: new LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]), zoom: 5 }, + "MarianaIslands": { bounds: new LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]), zoom: 5 }, + "Nevada": { bounds: new LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]), zoom: 5 }, + "PersianGulf": { bounds: new LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]), zoom: 5 }, + "Caucasus": { bounds: new LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]), zoom: 4 }, + // TODO "Falklands" +} + +export const layers = { + "ArcGIS Satellite": { + urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + maxZoom: 20, + minZoom: 1, + attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + }, + "USGS Topo": { + urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 20, + attribution: 'Tiles courtesy of the U.S. Geological Survey' + }, + "OpenStreetMap Mapnik": { + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 19, + attribution: '© OpenStreetMap contributors' + }, + "OPENVKarte": { + urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' + }, + "Esri.DeLorme": { + urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 11, + attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', + }, + "CyclOSM": { + urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 20, + attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' + } +} \ No newline at end of file diff --git a/client/src/controls/airbasecontextmenu.ts b/client/src/controls/airbasecontextmenu.ts index bb5e7f29..5ee53166 100644 --- a/client/src/controls/airbasecontextmenu.ts +++ b/client/src/controls/airbasecontextmenu.ts @@ -24,7 +24,7 @@ export class AirbaseContextMenu extends ContextMenu { this.setProperties(airbase.getProperties()); this.setParkings(airbase.getParkings()); this.setCoalition(airbase.getCoalition()); - this.enableLandButton(getUnitsManager().getSelectedUnitsType() === "Aircraft" && (getUnitsManager().getSelectedUnitsCoalition() === airbase.getCoalition() || airbase.getCoalition() === "neutral")) + this.enableLandButton(getUnitsManager().getSelectedUnitsTypes().length == 1 && getUnitsManager().getSelectedUnitsTypes()[0] === "Aircraft" && (getUnitsManager().getSelectedUnitsCoalition() === airbase.getCoalition() || airbase.getCoalition() === "neutral")) } setName(airbaseName: string) { diff --git a/client/src/controls/control.ts b/client/src/controls/control.ts new file mode 100644 index 00000000..4786a862 --- /dev/null +++ b/client/src/controls/control.ts @@ -0,0 +1,34 @@ +export class Control { + #container: HTMLElement | null; + expectedValue: any = null; + + constructor(ID: string) { + this.#container = document.getElementById(ID); + } + + show() { + if (this.#container != null) + this.#container.classList.remove("hide"); + } + + hide() { + if (this.#container != null) + this.#container.classList.add("hide"); + } + + getContainer() { + return this.#container; + } + + setExpectedValue(expectedValue: any) { + this.expectedValue = expectedValue; + } + + resetExpectedValue() { + this.expectedValue = null; + } + + checkExpectedValue(value: any) { + return this.expectedValue === null || value === this.expectedValue; + } +} \ No newline at end of file diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 502f1481..2877d80d 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -19,14 +19,12 @@ export class Dropdown { } 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.#toggle(); }); document.addEventListener("click", (ev) => { if (!(this.#value.contains(ev.target as Node) || this.#options.contains(ev.target as Node) || this.#element.contains(ev.target as Node))) { - this.#element.classList.remove("is-open"); + this.#close(); } }); @@ -46,12 +44,7 @@ export class Dropdown { button.addEventListener("click", (e: MouseEvent) => { e.stopPropagation(); - this.#value = document.createElement("div"); - this.#value.classList.add("ol-ellipsed"); - this.#value.innerText = option; - this.#close(); - this.#callback(option, e); - this.#index = idx; + this.selectValue(idx); }); return div; })); @@ -113,6 +106,8 @@ export class Dropdown { #open() { this.#element.classList.add("is-open"); + this.#options.classList.toggle("scrollbar-visible", this.#options.scrollHeight > this.#options.clientHeight); + this.#clip(); } #toggle() { diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index f381ca79..51027f01 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -1,10 +1,13 @@ import { LatLng } from "leaflet"; import { getActiveCoalition, getMap, setActiveCoalition } from ".."; -import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../server/server"; +import { spawnAircraft, spawnExplosion, spawnGroundUnit, spawnSmoke } from "../server/server"; import { aircraftDatabase } from "../units/aircraftdatabase"; import { groundUnitsDatabase } from "../units/groundunitsdatabase"; import { ContextMenu } from "./contextmenu"; import { Dropdown } from "./dropdown"; +import { Switch } from "./switch"; +import { Slider } from "./slider"; +import { ftToM } from "../other/utils"; export interface SpawnOptions { role: string; @@ -13,24 +16,32 @@ export interface SpawnOptions { coalition: string; loadout: string | null; airbaseName: string | null; + altitude: number | null; } export class MapContextMenu extends ContextMenu { + #coalitionSwitch: Switch; #aircraftRoleDropdown: Dropdown; #aircraftTypeDropdown: Dropdown; #aircraftLoadoutDropdown: Dropdown; + #aircrafSpawnAltitudeSlider: Slider; #groundUnitRoleDropdown: Dropdown; #groundUnitTypeDropdown: Dropdown; - #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null }; - + #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: ftToM(20000) }; + constructor(id: string) { super(id); - 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.#coalitionSwitch = new Switch("coalition-switch", this.#onSwitchClick); + this.#coalitionSwitch.setValue(false); + this.#coalitionSwitch.getContainer()?.addEventListener("contextmenu", (e) => this.#onSwitchRightClick(e)); this.#aircraftRoleDropdown = new Dropdown("aircraft-role-options", (role: string) => this.#setAircraftRole(role)); this.#aircraftTypeDropdown = new Dropdown("aircraft-type-options", (type: string) => this.#setAircraftType(type)); this.#aircraftLoadoutDropdown = new Dropdown("loadout-options", (loadout: string) => this.#setAircraftLoadout(loadout)); + this.#aircrafSpawnAltitudeSlider = new Slider("aircraft-spawn-altitude-slider", 0, 50000, "ft", (value: number) => {this.#spawnOptions.altitude = ftToM(value);}); + this.#aircrafSpawnAltitudeSlider.setIncrement(500); + this.#aircrafSpawnAltitudeSlider.setValue(20000); + this.#aircrafSpawnAltitudeSlider.setActive(true); this.#groundUnitRoleDropdown = new Dropdown("ground-unit-role-options", (role: string) => this.#setGroundUnitRole(role)); this.#groundUnitTypeDropdown = new Dropdown("ground-unit-type-options", (type: string) => this.#setGroundUnitType(type)); @@ -61,6 +72,12 @@ export class MapContextMenu extends ContextMenu { spawnSmoke(e.detail.color, this.getLatLng()); }); + document.addEventListener("contextMenuExplosion", (e: any) => { + this.hide(); + spawnExplosion(e.detail.strength, this.getLatLng()); + }); + + this.hide(); } @@ -78,6 +95,8 @@ export class MapContextMenu extends ContextMenu { this.getContainer()?.querySelector("#ground-unit-spawn-button")?.classList.toggle("is-open", type === "ground-unit"); this.getContainer()?.querySelector("#smoke-spawn-menu")?.classList.toggle("hide", type !== "smoke"); this.getContainer()?.querySelector("#smoke-spawn-button")?.classList.toggle("is-open", type === "smoke"); + this.getContainer()?.querySelector("#explosion-menu")?.classList.toggle("hide", type !== "explosion"); + this.getContainer()?.querySelector("#explosion-spawn-button")?.classList.toggle("is-open", type === "explosion"); this.#resetAircraftRole(); this.#resetAircraftType(); @@ -102,26 +121,13 @@ export class MapContextMenu extends ContextMenu { this.#spawnOptions.latlng = latlng; } - #onToggleLeftClick(e: any) { - if (this.getContainer() != null) { - if (e.srcElement.dataset.activeCoalition == "blue") - setActiveCoalition("neutral"); - else if (e.srcElement.dataset.activeCoalition == "neutral") - setActiveCoalition("red"); - else - setActiveCoalition("blue"); - } + #onSwitchClick(value: boolean) { + value? setActiveCoalition("red"): 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"); - } + #onSwitchRightClick(e: any) { + this.#coalitionSwitch.setValue(undefined); + setActiveCoalition("neutral"); } /********* Aircraft spawn menu *********/ diff --git a/client/src/controls/slider.ts b/client/src/controls/slider.ts index e8070d2e..edfb18a1 100644 --- a/client/src/controls/slider.ts +++ b/client/src/controls/slider.ts @@ -1,73 +1,71 @@ -export class Slider { - #container: HTMLElement | null; - #callback: CallableFunction; +import { zeroPad } from "../other/utils"; +import { Control } from "./control"; + +export class Slider extends Control { + #callback: CallableFunction | null = null; #slider: HTMLInputElement | null = null; #valueText: HTMLElement | null = null; - #minValue: number; - #maxValue: number; - #increment: number; - #minValueDiv: HTMLElement | null = null; - #maxValueDiv: HTMLElement | null = null; - #unit: string; - #display: string = ""; + #minValue: number = 0; + #maxValue: number = 0; + #increment: number = 0; + #minMaxValueDiv: HTMLElement | null = null; + #unitOfMeasure: string; #dragged: boolean = false; #value: number = 0; - constructor(ID: string, minValue: number, maxValue: number, unit: string, callback: CallableFunction) { - this.#container = document.getElementById(ID); - this.#callback = callback; - this.#minValue = minValue; - this.#maxValue = maxValue; - this.#increment = 1; - this.#unit = unit; - if (this.#container != null) { - this.#display = this.#container.style.display; - this.#slider = this.#container.querySelector("input"); - if (this.#slider != null) { - this.#slider.addEventListener("input", (e: any) => this.#onInput()); - this.#slider.addEventListener("mousedown", (e: any) => this.#onStart()); - this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize()); - } - this.#valueText = this.#container.querySelector("#value"); + constructor(ID: string, minValue: number, maxValue: number, unitOfMeasure: string, callback: CallableFunction) { + super(ID); + this.#callback = callback; + this.#unitOfMeasure = unitOfMeasure; + this.#slider = this.getContainer()?.querySelector("input") as HTMLInputElement; + + if (this.#slider != null) { + this.#slider.addEventListener("input", (e: any) => this.#update()); + this.#slider.addEventListener("mousedown", (e: any) => this.#onStart()); + this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize()); } - } - show() { - if (this.#container != null) - this.#container.style.display = this.#display; - } + this.#valueText = this.getContainer()?.querySelector(".ol-slider-value") as HTMLElement; + this.#minMaxValueDiv = this.getContainer()?.querySelector(".ol-slider-min-max") as HTMLElement; - hide() { - if (this.#container != null) - this.#container.style.display = 'none'; + this.setIncrement(1); + this.setMinMax(minValue, maxValue); } setActive(newActive: boolean) { - if (this.#container && !this.#dragged) { - this.#container.classList.toggle("active", newActive); + if (!this.getDragged()) { + this.getContainer()?.classList.toggle("active", newActive); if (!newActive && this.#valueText != null) this.#valueText.innerText = "Mixed values"; } } setMinMax(newMinValue: number, newMaxValue: number) { - this.#minValue = newMinValue; - this.#maxValue = newMaxValue; - this.#updateMax(); + if (this.#minValue != newMinValue || this.#maxValue != newMaxValue) { + this.#minValue = newMinValue; + this.#maxValue = newMaxValue; + this.#updateMaxValue(); + + if (this.#minMaxValueDiv != null) { + this.#minMaxValueDiv.setAttribute('data-min-value', `${this.#minValue}${this.#unitOfMeasure}`); + this.#minMaxValueDiv.setAttribute('data-max-value', `${this.#maxValue}${this.#unitOfMeasure}`); + } + } } setIncrement(newIncrement: number) { - this.#increment = newIncrement; - this.#updateMax(); + if (this.#increment != newIncrement) { + this.#increment = newIncrement; + this.#updateMaxValue(); + } } - setValue(newValue: number) { - // Disable value setting if the user is dragging the element - if (!this.#dragged) { + setValue(newValue: number, ignoreExpectedValue: boolean = true) { + if (!this.getDragged() && (ignoreExpectedValue || this.checkExpectedValue(newValue))) { this.#value = newValue; if (this.#slider != null) this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * parseFloat(this.#slider.max)); - this.#onValue() + this.#update(); } } @@ -75,36 +73,51 @@ export class Slider { return this.#value; } + setDragged(newDragged: boolean) { + this.#dragged = newDragged; + } + getDragged() { return this.#dragged; } - #updateMax() { + #updateMaxValue() { var oldValue = this.getValue(); if (this.#slider != null) this.#slider.max = String((this.#maxValue - this.#minValue) / this.#increment); this.setValue(oldValue); } - #onValue() { + #update() { if (this.#valueText != null && this.#slider != null) - this.#valueText.innerText = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)) + this.#unit + { + /* Update the text value */ + var value = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); + var strValue = String(value); + if (value > 1000) + strValue = String(Math.floor(value / 1000)) + "," + zeroPad(value - Math.floor(value / 1000) * 1000, 3); + this.#valueText.innerText = `${strValue} ${this.#unitOfMeasure.toUpperCase()}`; + + /* Update the position of the slider */ + var percentValue = parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * 90 + 5; + this.#slider.style.background = `linear-gradient(to right, var(--accent-light-blue) 5%, var(--accent-light-blue) ${percentValue}%, var(--background-grey) ${percentValue}%, var(--background-grey) 100%)` + } this.setActive(true); } - #onInput() { - this.#onValue(); - } - #onStart() { - this.#dragged = true; + this.setDragged(true); } #onFinalize() { - this.#dragged = false; + this.setDragged(false); if (this.#slider != null) { - this.#value = this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue); - this.#callback(this.getValue()); + this.resetExpectedValue(); + this.setValue(this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } } } } \ No newline at end of file diff --git a/client/src/controls/switch.ts b/client/src/controls/switch.ts new file mode 100644 index 00000000..60f9e3e5 --- /dev/null +++ b/client/src/controls/switch.ts @@ -0,0 +1,46 @@ +import { Control } from "./control"; + +export class Switch extends Control { + #value: boolean | undefined = false; + #callback: CallableFunction | null = null; + + constructor(ID: string, callback: CallableFunction, initialValue?: boolean) { + super(ID); + this.getContainer()?.addEventListener('click', (e) => this.#onToggle()); + this.setValue(initialValue !== undefined? initialValue: true); + + this.#callback = callback; + + /* Add the toggle itself to the document */ + const container = this.getContainer(); + if (container != undefined){ + const width = getComputedStyle(container).width; + const height = getComputedStyle(container).height; + var el = document.createElement("div"); + el.classList.add("ol-switch-fill"); + el.style.setProperty("--width", width? width: "0"); + el.style.setProperty("--height", height? height: "0"); + this.getContainer()?.appendChild(el); + } + } + + setValue(newValue: boolean | undefined, ignoreExpectedValue: boolean = true) { + if (ignoreExpectedValue || this.checkExpectedValue(newValue)) { + this.#value = newValue; + this.getContainer()?.setAttribute("data-value", String(newValue)); + } + } + + getValue() { + return this.#value; + } + + #onToggle() { + this.resetExpectedValue(); + this.setValue(!this.getValue()); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } + } +} \ No newline at end of file diff --git a/client/src/controls/unitcontextmenu.ts b/client/src/controls/unitcontextmenu.ts index 4acbaf3e..15ce7227 100644 --- a/client/src/controls/unitcontextmenu.ts +++ b/client/src/controls/unitcontextmenu.ts @@ -1,4 +1,4 @@ -import { deg2rad } from "../other/utils"; +import { deg2rad, ftToM } from "../other/utils"; import { ContextMenu } from "./contextmenu"; export class UnitContextMenu extends ContextMenu { @@ -19,8 +19,8 @@ export class UnitContextMenu extends ContextMenu { } var angleDeg = 360 - (clock - 1) * 45; var angleRad = deg2rad(angleDeg); - var distance = parseInt((dialog.querySelector(`#distance`)?.querySelector("input")).value) * 0.3048; - var upDown = parseInt((dialog.querySelector(`#up-down`)?.querySelector("input")).value) * 0.3048; + var distance = ftToM(parseInt((dialog.querySelector(`#distance`)?.querySelector("input")).value)); + var upDown = ftToM(parseInt((dialog.querySelector(`#up-down`)?.querySelector("input")).value)); // X: front-rear, positive front // Y: top-bottom, positive top diff --git a/client/src/index.ts b/client/src/index.ts index 8d57b546..e4760d01 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -15,6 +15,7 @@ import { keyEventWasInInput } from "./other/utils"; import { Popup } from "./popups/popup"; import { Dropdown } from "./controls/dropdown"; import { HotgroupPanel } from "./panels/hotgrouppanel"; +import { SVGInjector } from "@tanem/svg-injector"; var map: Map; @@ -195,6 +196,15 @@ function setupEvents() { location.reload(); }) + document.querySelectorAll("[inject-svg]").forEach((el: Element) => { + var img = el as HTMLImageElement; + var isLoaded = img.complete && img.naturalHeight !== 0; + if (isLoaded) + SVGInjector(img); + else + img.onload = () => SVGInjector(img); + }) + } export function getMap() { diff --git a/client/src/map/destinationpreviewmarker.ts b/client/src/map/destinationpreviewmarker.ts index 1146992d..aa76f395 100644 --- a/client/src/map/destinationpreviewmarker.ts +++ b/client/src/map/destinationpreviewmarker.ts @@ -6,7 +6,7 @@ export class DestinationPreviewMarker extends CustomMarker { this.setIcon(new DivIcon({ iconSize: [52, 52], iconAnchor: [26, 26], - className: "leaflet-destination-preview" + className: "leaflet-destination-preview", })); var el = document.createElement("div"); el.classList.add("ol-destination-preview-icon"); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 477b8640..83339a7a 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -12,6 +12,8 @@ import { DestinationPreviewMarker } from "./destinationpreviewmarker"; import { TemporaryUnitMarker } from "./temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' +import { layers as mapLayers, mapBounds, minimapBoundaries } from "../constants/constants"; +import { TargetMarker } from "./targetmarker"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); @@ -19,12 +21,16 @@ L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); require("../../public/javascripts/leaflet.nauticscale.js") /* Map constants */ -export const IDLE = "IDLE"; -export const MOVE_UNIT = "MOVE_UNIT"; +export const IDLE = "Idle"; +export const MOVE_UNIT = "Move unit"; +export const BOMBING = "Bombing"; +export const CARPET_BOMBING = "Carpet bombing"; +export const FIRE_AT_AREA = "Fire at area"; export const visibilityControls: string[] = ["human", "dcs", "aircraft", "groundunit-sam", "groundunit-other", "navyunit", "airbase"]; export const visibilityControlsTootlips: string[] = ["Toggle human players visibility", "Toggle DCS controlled units visibility", "Toggle aircrafts visibility", "Toggle SAM units visibility", "Toggle ground units (not SAM) visibility", "Toggle navy units visibility", "Toggle airbases visibility"]; export class Map extends L.Map { + #ID: string; #state: string; #layer: L.TileLayer | null = null; #preventLeftClick: boolean = false; @@ -39,8 +45,9 @@ export class Map extends L.Map { #centerUnit: Unit | null = null; #miniMap: ClickableMiniMap | null = null; #miniMapLayerGroup: L.LayerGroup; - #temporaryMarkers: L.Marker[] = []; - #destinationPreviewMarkers: L.Marker[] = []; + #temporaryMarkers: TemporaryUnitMarker[] = []; + #destinationPreviewMarkers: DestinationPreviewMarker[] = []; + #targetMarker: TargetMarker; #destinationGroupRotation: number = 0; #computeDestinationRotation: boolean = false; #destinationRotationCenter: L.LatLng | null = null; @@ -54,14 +61,17 @@ 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, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); - this.setLayer("ArcGIS Satellite"); + this.#ID = ID; + + this.setLayer(Object.keys(mapLayers)[0]); /* Minimap */ - var minimapLayer = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { minZoom: 0, maxZoom: 13 }); + var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); var miniMapPolyline = new L.Polyline(this.#getMinimapBoundaries(), { color: '#202831' }); miniMapPolyline.addTo(this.#miniMapLayerGroup); @@ -111,8 +121,9 @@ export class Map extends L.Map { /* Pan interval */ 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)); + if (this.#panLeft || this.#panDown || this.#panRight || this.#panLeft) + this.panBy(new L.Point(((this.#panLeft? -1 : 0) + (this.#panRight ? 1 : 0)) * this.#deafultPanDelta, + ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta)); }, 20); /* Option buttons */ @@ -120,89 +131,50 @@ export class Map extends L.Map { return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleUnitVisibility", `{"type": "${option}"}`); }); document.querySelector("#unit-visibility-control")?.append(...this.#optionButtons["visibility"]); + + /* Markers */ + this.#targetMarker = new TargetMarker(new L.LatLng(0, 0), {interactive: false}); } setLayer(layerName: string) { - if (this.#layer != null) { + if (this.#layer != null) this.removeLayer(this.#layer) + + if (layerName in mapLayers){ + const layerData = mapLayers[layerName as keyof typeof mapLayers]; + var options: L.TileLayerOptions = { + attribution: layerData.attribution, + minZoom: layerData.minZoom, + maxZoom: layerData.maxZoom + }; + this.#layer = new L.TileLayer(layerData.urlTemplate, options); } - if (layerName == "ArcGIS Satellite") { - this.#layer = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { - attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" - }); - } - else if (layerName == "USGS Topo") { - this.#layer = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', { - maxZoom: 20, - attribution: 'Tiles courtesy of the U.S. Geological Survey' - }); - } - else if (layerName == "OpenStreetMap Mapnik") { - this.#layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap contributors' - }); - } - else if (layerName == "OPENVKarte") { - this.#layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' - }); - } - else if (layerName == "Esri.DeLorme") { - this.#layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', - minZoom: 1, - maxZoom: 11 - }); - } - else if (layerName == "CyclOSM") { - this.#layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { - maxZoom: 20, - attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' - }); - } this.#layer?.addTo(this); } getLayers() { - return ["ArcGIS Satellite", "USGS Topo", "OpenStreetMap Mapnik", "OPENVKarte", "Esri.DeLorme", "CyclOSM"] + return Object.keys(mapLayers); } /* State machine */ setState(state: string) { this.#state = state; if (this.#state === IDLE) { - L.DomUtil.removeClass(this.getContainer(), 'crosshair-cursor-enabled'); - - /* Remove all the destination preview markers */ - this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { - this.removeLayer(marker); - }) - this.#destinationPreviewMarkers = []; - - this.#destinationGroupRotation = 0; - this.#computeDestinationRotation = false; - this.#destinationRotationCenter = null; + this.#resetDestinationMarkers(); + this.#resetTargetMarker(); + this.#showCursor(); } else if (this.#state === MOVE_UNIT) { - L.DomUtil.addClass(this.getContainer(), 'crosshair-cursor-enabled'); - - /* Remove all the exising destination preview markers */ - this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { - this.removeLayer(marker); - }) - this.#destinationPreviewMarkers = []; - - if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 1 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) { - /* Create the unit destination preview markers */ - this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => { - var marker = new DestinationPreviewMarker(this.getMouseCoordinates()); - marker.addTo(this); - return marker; - }) - } + this.#resetTargetMarker(); + this.#createDestinationMarkers(); + if (this.#destinationPreviewMarkers.length > 0) + this.#hideCursor(); + } + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#resetDestinationMarkers(); + this.#createTargetMarker(); + this.#hideCursor(); } document.dispatchEvent(new CustomEvent("mapStateChanged")); } @@ -294,20 +266,9 @@ export class Map extends L.Map { setTheatre(theatre: string) { var bounds = new L.LatLngBounds([-90, -180], [90, 180]); var miniMapZoom = 5; - if (theatre == "Syria") - bounds = new L.LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]); - 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]) - else if (theatre == "PersianGulf") - 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]) - miniMapZoom = 4; + if (theatre in mapBounds) { + bounds = mapBounds[theatre as keyof typeof mapBounds].bounds; + miniMapZoom = mapBounds[theatre as keyof typeof mapBounds].zoom; } this.setView(bounds.getCenter(), 8); @@ -403,7 +364,7 @@ export class Map extends L.Map { if (this.#state === IDLE) { } - else if (this.#state === MOVE_UNIT) { + else { this.setState(IDLE); getUnitsManager().deselectAllUnits(); } @@ -425,11 +386,34 @@ export class Map extends L.Map { if (!e.originalEvent.ctrlKey) { getUnitsManager().selectedUnitsClearDestinations(); } - getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, !e.originalEvent.shiftKey, this.#destinationGroupRotation) + getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; } + else if (this.#state === BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + } + else if (this.#state === CARPET_BOMBING) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + } + else if (this.#state === FIRE_AT_AREA) { + getUnitsManager().getSelectedUnits().length > 0? this.setState(MOVE_UNIT): this.setState(IDLE); + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); + } + } + + #executeAction(e: any, action: string) { + if (action === "bomb") + getUnitsManager().selectedUnitsBombPoint(this.getMouseCoordinates()); + else if (action === "carpet-bomb") + getUnitsManager().selectedUnitsCarpetBomb(this.getMouseCoordinates()); + else if (action === "building-bomb") + getUnitsManager().selectedUnitsBombBuilding(this.getMouseCoordinates()); + else if (action === "fire-at-area") + getUnitsManager().selectedUnitsFireAtArea(this.getMouseCoordinates()); } #onSelectionEnd(e: any) { @@ -462,10 +446,14 @@ export class Map extends L.Map { this.#lastMousePosition.x = e.originalEvent.x; this.#lastMousePosition.y = e.originalEvent.y; - if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) - this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); - - this.#updateDestinationPreview(e); + if (this.#state === MOVE_UNIT){ + if (this.#computeDestinationRotation && this.#destinationRotationCenter != null) + this.#destinationGroupRotation = -bearing(this.#destinationRotationCenter.lat, this.#destinationRotationCenter.lng, this.getMouseCoordinates().lat, this.getMouseCoordinates().lng); + this.#updateDestinationPreview(e); + } + else if ([BOMBING, CARPET_BOMBING, FIRE_AT_AREA].includes(this.#state)) { + this.#targetMarker.setLatLng(this.getMouseCoordinates()); + } } #onZoom(e: any) { @@ -480,48 +468,13 @@ export class Map extends L.Map { #getMinimapBoundaries() { /* Draw the limits of the maps in the minimap*/ - return [[ // 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) - ] - ]; + return minimapBoundaries; } #updateDestinationPreview(e: any) { Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(), this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { if (idx < this.#destinationPreviewMarkers.length) - this.#destinationPreviewMarkers[idx].setLatLng(!e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); + this.#destinationPreviewMarkers[idx].setLatLng(e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); }) } @@ -537,5 +490,48 @@ export class Map extends L.Map { button.setAttribute("data-on-click-params", argument); return button; } + + #createDestinationMarkers() { + this.#resetDestinationMarkers(); + + if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 0 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) { + /* Create the unit destination preview markers */ + this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => { + var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); + marker.addTo(this); + return marker; + }) + } + } + + #resetDestinationMarkers() { + /* Remove all the destination preview markers */ + this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { + this.removeLayer(marker); + }) + this.#destinationPreviewMarkers = []; + + this.#destinationGroupRotation = 0; + this.#computeDestinationRotation = false; + this.#destinationRotationCenter = null; + } + + #createTargetMarker(){ + this.#resetTargetMarker(); + this.#targetMarker.addTo(this); + } + + #resetTargetMarker() { + this.#targetMarker.setLatLng(new L.LatLng(0, 0)); + this.removeLayer(this.#targetMarker); + } + + #showCursor() { + document.getElementById(this.#ID)?.classList.remove("hidden-cursor"); + } + + #hideCursor() { + document.getElementById(this.#ID)?.classList.add("hidden-cursor"); + } } diff --git a/client/src/map/targetmarker.ts b/client/src/map/targetmarker.ts new file mode 100644 index 00000000..30232dc1 --- /dev/null +++ b/client/src/map/targetmarker.ts @@ -0,0 +1,18 @@ +import { DivIcon } from "leaflet"; +import { CustomMarker } from "./custommarker"; + +export class TargetMarker extends CustomMarker { + #interactive: boolean = false; + + createIcon() { + this.setIcon(new DivIcon({ + iconSize: [52, 52], + iconAnchor: [26, 26], + className: "leaflet-target-marker", + })); + var el = document.createElement("div"); + el.classList.add("ol-target-icon"); + el.classList.toggle("ol-target-icon-interactive", this.#interactive) + this.getElement()?.appendChild(el); + } +} diff --git a/client/src/missionhandler/missionhandler.ts b/client/src/missionhandler/missionhandler.ts index a6271c16..867e08bf 100644 --- a/client/src/missionhandler/missionhandler.ts +++ b/client/src/missionhandler/missionhandler.ts @@ -68,7 +68,7 @@ export class MissionHandler for (let idx in data.airbases) { var airbase = data.airbases[idx] - if (this.#airbases[idx] === undefined) + if (this.#airbases[idx] === undefined && airbase.callsign != '') { this.#airbases[idx] = new Airbase({ position: new LatLng(airbase.latitude, airbase.longitude), @@ -76,7 +76,8 @@ export class MissionHandler }).addTo(getMap()); this.#airbases[idx].on('contextmenu', (e) => this.#onAirbaseClick(e)); } - if (airbase.latitude && airbase.longitude && airbase.coalition) + + if (this.#airbases[idx] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) { this.#airbases[idx].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); this.#airbases[idx].setCoalition(airbase.coalition); @@ -86,7 +87,7 @@ export class MissionHandler } } - if ("mission" in data) + if ("mission" in data && data.mission != null) { if (data.mission != null && data.mission.theatre != this.#theatre) { diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index ce1ecc1f..d6ef4fd9 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -79,8 +79,8 @@ export function reciprocalHeading(heading: number): number { return heading > 180? heading - 180: heading + 180; } -export const zeroAppend = function (num: number, places: number) { - var string = String(num); +export const zeroAppend = function (num: number, places: number, decimal: boolean = false) { + var string = decimal? num.toFixed(2): String(num); while (string.length < places) { string = "0" + string; } @@ -160,4 +160,28 @@ export function createDivWithClass(className: string) { var el = document.createElement("div"); el.classList.add(className); return el; +} + +export function knotsToMs(knots: number) { + return knots / 1.94384; +} + +export function msToKnots(ms: number) { + return ms * 1.94384; +} + +export function ftToM(ft: number) { + return ft * 0.3048; +} + +export function mToFt(m: number) { + return m / 0.3048; +} + +export function mToNm(m: number) { + return m * 0.000539957; +} + +export function nmToFt(nm: number) { + return nm * 6076.12; } \ No newline at end of file diff --git a/client/src/panels/mouseinfopanel.ts b/client/src/panels/mouseinfopanel.ts index bdd38fb3..81ef04e6 100644 --- a/client/src/panels/mouseinfopanel.ts +++ b/client/src/panels/mouseinfopanel.ts @@ -1,6 +1,6 @@ import { Icon, LatLng, Marker, Polyline } from "leaflet"; import { getMap, getMissionData, getUnitsManager } from ".."; -import { distance, bearing, zeroPad, zeroAppend, reciprocalHeading } from "../other/utils"; +import { distance, bearing, zeroAppend, mToNm, nmToFt } from "../other/utils"; import { Unit } from "../units/unit"; import { Panel } from "./panel"; @@ -14,8 +14,8 @@ export class MouseInfoPanel extends Panel { constructor(ID: string) { super(ID); - this.#measureIcon = new Icon({ iconUrl: 'images/pin.png', iconAnchor: [16, 32]}); - this.#measureMarker = new Marker([0, 0], {icon: this.#measureIcon, interactive: false}); + this.#measureIcon = new Icon({ iconUrl: 'resources/theme/images/icons/pin.svg', iconAnchor: [16, 32] }); + this.#measureMarker = new Marker([0, 0], { icon: this.#measureIcon, interactive: false }); this.#measureBox = document.createElement("div"); this.#measureBox.classList.add("ol-measure-box", "hide"); @@ -25,109 +25,36 @@ export class MouseInfoPanel extends Panel { getMap()?.on('zoom', (e: any) => this.#onZoom(e)); getMap()?.on('mousemove', (e: any) => this.#onMouseMove(e)); - document.addEventListener('unitsSelection', (e: CustomEvent) => this.#onUnitsSelection(e.detail)); - document.addEventListener('clearSelection', () => this.#onClearSelection()); + document.addEventListener('unitsSelection', (e: CustomEvent) => this.#update()); + document.addEventListener('clearSelection', () => this.#update()); } - #update(mousePosition: LatLng, measurePosition: LatLng | null, unitPosition: LatLng | null) { + #update() { + const mousePosition = getMap().getMouseCoordinates(); + + var selectedUnitPosition = null; + var selectedUnits = getUnitsManager().getSelectedUnits(); + if (selectedUnits && selectedUnits.length == 1) + selectedUnitPosition = new LatLng(selectedUnits[0].getFlightData().latitude, selectedUnits[0].getFlightData().longitude); + + /* Draw measures from selected unit, from pin location, and from bullseyes */ + this.#drawMeasure("ref-measure-position", "measure-position", this.#measurePoint, mousePosition); + this.#drawMeasure("ref-unit-position", "unit-position", selectedUnitPosition, mousePosition); + + this.getElement().querySelector(`#measuring-tool`)?.classList.toggle("hide", this.#measurePoint === null && selectedUnitPosition === null); + var bullseyes = getMissionData().getBullseyes(); for (let idx in bullseyes) - { - var el = this.getElement().querySelector(`#bullseye-${idx}`); + this.#drawMeasure(null, `bullseye-${idx}`, bullseyes[idx].getLatLng(), mousePosition); - if ( el != null ) { - - var dist = distance(bullseyes[idx].getLatLng().lat, bullseyes[idx].getLatLng().lng, mousePosition.lat, mousePosition.lng); - var bear = bearing(bullseyes[idx].getLatLng().lat, bullseyes[idx].getLatLng().lng, mousePosition.lat, mousePosition.lng); - - let bng = zeroAppend(Math.floor(bear), 3); - - if ( bng === "000" ) { - bng = "360"; - } - - el.dataset.bearing = bng; - el.dataset.distance = zeroAppend(Math.floor(dist*0.000539957), 3); - el.dataset.distanceUnits = "NM"; - } - - } - - if (measurePosition) { - var el = this.getElement().querySelector(`#measure-position`); - - if (el != null) { - var bear = bearing(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng); - var dist = distance(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng); - - let bng = zeroAppend(Math.floor(bear), 3); - - if ( bng === "000" ) { - bng = "360"; - } - - el.dataset.bearing = bng; - el.dataset.distance = zeroAppend(Math.floor(dist*0.000539957), 3); - el.dataset.distanceUnits = "NM"; - - } - } - - - if (unitPosition) { - var el = this.getElement().querySelector(`#unit-position`); - if (el != null) { - var dist = distance(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng); - var bear = bearing(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng); - - el.dataset.bearing = zeroAppend(Math.floor(bear), 3); - el.dataset.distance = zeroAppend(Math.floor(dist*0.000539957), 3); - el.dataset.distanceUnits = "NM"; - } - } - - const refMouseLat = document.getElementById( "ref-mouse-position-latitude" ); - const mouseLat = document.getElementById( "mouse-position-latitude" ); - - if ( refMouseLat && mouseLat ) { - - let matches = String( mousePosition.lat ).match( /^\-?(\d+)\.(\d{2})(\d{2})(\d{2})/ ); - - if ( matches && matches.length ) { - mouseLat.dataset.dd = matches[1]; - mouseLat.dataset.mm = matches[2]; - mouseLat.dataset.ss = matches[3]; - mouseLat.dataset.sss = matches[4]; - } - - refMouseLat.dataset.label = ( mousePosition.lat < 0 ) ? "S" : "N"; - - } - - const refMouseLng = document.getElementById( "ref-mouse-position-longitude" ); - const mouseLng = document.getElementById( "mouse-position-longitude" ); - - if ( refMouseLng && mouseLng ) { - - let matches = String( mousePosition.lng ).match( /^\-?(\d+)\.(\d{2})(\d{2})(\d{2})/ ); - - if ( matches && matches.length ) { - mouseLng.dataset.dd = matches[1]; - mouseLng.dataset.mm = matches[2]; - mouseLng.dataset.ss = matches[3]; - mouseLng.dataset.sss = matches[4]; - } - - refMouseLng.dataset.label = ( mousePosition.lng < 0 ) ? "W" : "E"; - } + /* Draw coordinates */ + this.#drawCoordinates("ref-mouse-position-latitude", "mouse-position-latitude", mousePosition.lat, ["N", "S"]); + this.#drawCoordinates("ref-mouse-position-longitude", "mouse-position-longitude", mousePosition.lng, ["E", "W"]); } - #onMapClick(e: any) - { - if (e.originalEvent.ctrlKey) - { - if (!this.#measurePoint) - { + #onMapClick(e: any) { + if (e.originalEvent.ctrlKey) { + if (!this.#measurePoint) { this.#measureBox.classList.toggle("hide", false); this.#measurePoint = e.latlng; this.#measureMarker.setLatLng(e.latlng); @@ -135,8 +62,7 @@ export class MouseInfoPanel extends Panel { if (!getMap().hasLayer(this.#measureLine)) this.#measureLine.addTo(getMap()); } - else - { + else { this.#measureBox.classList.toggle("hide", true); this.#measurePoint = null; if (getMap().hasLayer(this.#measureMarker)) @@ -147,13 +73,13 @@ export class MouseInfoPanel extends Panel { getMap().removeLayer(this.#measureLine); } } + + this.#update(); } - #drawMeasureLine() - { + #drawMeasureLine() { var mouseLatLng = getMap().containerPointToLatLng(getMap().getMousePosition()); - if (this.#measurePoint != null) - { + if (this.#measurePoint != null) { var points = [this.#measurePoint, mouseLatLng]; this.#measureLine.setLatLngs(points); var dist = distance(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng); @@ -163,74 +89,102 @@ export class MouseInfoPanel extends Panel { var dy = (getMap().getMousePosition().y - startXY.y); var angle = Math.atan2(dy, dx); - if (angle > Math.PI / 2) + if (angle > Math.PI / 2) angle = angle - Math.PI; - if (angle < -Math.PI / 2) + if (angle < -Math.PI / 2) angle = angle + Math.PI; let bng = zeroAppend(Math.floor(bear), 3); - const reciprocal = zeroAppend( reciprocalHeading( parseInt( bng ) ), 3 ); - if ( bng === "000" ) { + if (bng === "000") bng = "360"; - } - let data = [ `${bng}°`, `${Math.floor(dist*0.000539957)}NM`, `${reciprocal}°` ]; + var [str, unit] = this.#computeDistanceString(dist) - if ( bear < 180 ) { - data = data.reverse(); - } + let data = [`${bng}°`, `${str} ${unit}`]; - this.#measureBox.innerText = data.join( " | " ); + this.#measureBox.innerText = data.join(" / "); this.#measureBox.style.left = (getMap().getMousePosition().x + startXY.x) / 2 - this.#measureBox.offsetWidth / 2 + "px"; this.#measureBox.style.top = (getMap().getMousePosition().y + startXY.y) / 2 - this.#measureBox.offsetHeight / 2 + "px"; this.#measureBox.style.rotate = angle + "rad"; } } - #onMouseMove(e: any) - { - var selectedUnitPosition = null; - var selectedUnits = getUnitsManager().getSelectedUnits(); - if (selectedUnits && selectedUnits.length == 1) - selectedUnitPosition = new LatLng(selectedUnits[0].getFlightData().latitude, selectedUnits[0].getFlightData().longitude); + #onMouseMove(e: any) { - this.#update(e.latlng, this.#measurePoint, selectedUnitPosition); + this.#update(); this.#drawMeasureLine(); } - #onZoom(e: any) - { + #onZoom(e: any) { this.#drawMeasureLine(); } - #onUnitsSelection(units: Unit[]) - { - const pos = this.getElement().querySelector(`#unit-position`); - - if ( units.length > 1 ) { - pos?.setAttribute( "data-message", "(multiple units)" ); - } else { - pos?.removeAttribute( "data-message" ); - } + #drawMeasure(imgId: string | null, textId: string, value: LatLng | null, mousePosition: LatLng) { + var el = this.getElement().querySelector(`#${textId}`) as HTMLElement; + var img = imgId != null ? this.getElement().querySelector(`#${imgId}`) as HTMLElement : null; + if (value) { + if (el != null) { + el.classList.remove("hide"); + var bear = bearing(value.lat, value.lng, mousePosition.lat, mousePosition.lng); + var dist = distance(value.lat, value.lng, mousePosition.lat, mousePosition.lng); + + let bng = zeroAppend(Math.floor(bear), 3); + + if (bng === "000") + bng = "360"; + + var [str, unit] = this.#computeDistanceString(dist) + + el.dataset.bearing = bng; + el.dataset.distance = str; + el.dataset.distanceUnits = unit; + } + if (img != null) + img.classList.remove("hide"); + } + else { + if (el != null) + el.classList.add("hide"); + if (img != null) + img.classList.add("hide"); + } } - - #onClearSelection() - { - this.#measureBox.classList.toggle("hide", true); - - const pos = this.getElement().querySelector(`#unit-position`); - - if ( pos instanceof HTMLElement ) { - pos?.removeAttribute( "data-message" ); - - pos.dataset.bearing = "---"; - pos.dataset.distance = "---"; - pos.dataset.distanceUnits = "NM"; + #drawCoordinates(imgId: string, textId: string, value: number, prefixes: string[]) { + const el = this.getElement().querySelector(`#${textId}`) as HTMLElement; + const img = this.getElement().querySelector(`#${imgId}`) as HTMLElement; + if (img && el) { + let matches = String(value).match(/^\-?(\d+)\.(\d{2})(\d{2})(\d{2})/); + if (matches && matches.length) { + el.dataset.dd = matches[1]; + el.dataset.mm = matches[2]; + el.dataset.ss = matches[3]; + el.dataset.sss = matches[4]; + } + img.dataset.label = (value < 0) ? prefixes[1] : prefixes[0]; } } + + #computeDistanceString(dist: number) { + var val = mToNm(dist); + var strVal = 0; + var decimal = false; + var unit = "NM"; + if (val > 10) + strVal = Math.floor(val); + else if (val > 1 && val <= 10) { + strVal = Math.floor(val * 100) / 100; + decimal = true; + } + else { + strVal = Math.floor(nmToFt(val)); + unit = "ft"; + } + + return [zeroAppend(strVal, 3, decimal), unit]; + } } diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index ea90161d..cba59326 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -3,34 +3,22 @@ import { getUnitsManager } from ".."; import { Dropdown } from "../controls/dropdown"; import { Slider } from "../controls/slider"; import { aircraftDatabase } from "../units/aircraftdatabase"; -import { groundUnitsDatabase } from "../units/groundunitsdatabase"; -import { Aircraft, GroundUnit, Unit } from "../units/unit"; -import { UnitDatabase } from "../units/unitdatabase"; +import { Unit } from "../units/unit"; import { Panel } from "./panel"; - -const ROEs: string[] = ["Hold", "Return", "Designated", "Free"]; -const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"]; -const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"]; - -const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"]; -const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"]; -const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"]; - -const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; -const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 }; -const speedIncrements: { [key: string]: number } = { Aircraft: 25, Helicopter: 10, NavyUnit: 5, GroundUnit: 5 }; -const minAltitudeValues: { [key: string]: number } = { Aircraft: 0, Helicopter: 0 }; -const maxAltitudeValues: { [key: string]: number } = { Aircraft: 50000, Helicopter: 10000 }; -const altitudeIncrements: { [key: string]: number } = { Aircraft: 500, Helicopter: 100 }; +import { Switch } from "../controls/switch"; +import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, emissionsCountermeasuresDescriptions, maxAltitudeValues, maxSpeedValues, minAltitudeValues, minSpeedValues, reactionsToThreat, reactionsToThreatDescriptions, speedIncrements } from "../constants/constants"; +import { ftToM, knotsToMs, mToFt, msToKnots } from "../other/utils"; export class UnitControlPanel extends Panel { #altitudeSlider: Slider; - #airspeedSlider: Slider; + #altitudeTypeSwitch: Switch; + #speedSlider: Slider; + #speedTypeSwitch: Switch; + #onOffSwitch: Switch; + #followRoadsSwitch: Switch; #TACANXYDropdown: Dropdown; #radioDecimalsDropdown: Dropdown; #radioCallsignDropdown: Dropdown; - #expectedAltitude: number = -1; - #expectedSpeed: number = -1; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} #advancedSettingsDialog: HTMLElement; @@ -38,22 +26,11 @@ export class UnitControlPanel extends Panel { super(ID); /* Unit control sliders */ - this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { - this.#expectedAltitude = value; - getUnitsManager().selectedUnitsSetAltitude(value * 0.3048) - }); + this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getUnitsManager().selectedUnitsSetAltitude(ftToM(value)); }); + this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetAltitudeType(value? "AGL": "ASL"); }); - this.#airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => { - this.#expectedSpeed = value; - getUnitsManager().selectedUnitsSetSpeed(value / 1.94384) - }); - - /* Advanced settings dropdowns */ - this.#TACANXYDropdown = new Dropdown("TACAN-XY", () => {}); - this.#TACANXYDropdown.setOptions(["X", "Y"]); - this.#radioDecimalsDropdown = new Dropdown("radio-decimals", () => {}); - this.#radioDecimalsDropdown.setOptions([".000", ".250", ".500", ".750"]); - this.#radioCallsignDropdown = new Dropdown("radio-callsign", () => {}); + this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getUnitsManager().selectedUnitsSetSpeed(knotsToMs(value)); }); + this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetSpeedType(value? "GS": "CAS"); }); /* Option buttons */ this.#optionButtons["ROE"] = ROEs.map((option: string, index: number) => { @@ -72,8 +49,27 @@ export class UnitControlPanel extends Panel { this.getElement().querySelector("#reaction-to-threat-buttons-container")?.append(...this.#optionButtons["reactionToThreat"]); this.getElement().querySelector("#emissions-countermeasures-buttons-container")?.append(...this.#optionButtons["emissionsCountermeasures"]); + /* On off switch */ + this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetOnOff(value); + }); + + /* Follow roads switch */ + this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetFollowRoads(value); + }); + + /* Advanced settings dialog */ this.#advancedSettingsDialog = document.querySelector("#advanced-settings-dialog"); + /* Advanced settings dropdowns */ + this.#TACANXYDropdown = new Dropdown("TACAN-XY", () => {}); + this.#TACANXYDropdown.setOptions(["X", "Y"]); + this.#radioDecimalsDropdown = new Dropdown("radio-decimals", () => {}); + this.#radioDecimalsDropdown.setOptions([".000", ".250", ".500", ".750"]); + this.#radioCallsignDropdown = new Dropdown("radio-callsign", () => {}); + + /* Events and timer */ window.setInterval(() => {this.update();}, 25); document.addEventListener("unitsSelection", (e: CustomEvent) => { this.show(); this.addButtons();}); @@ -82,35 +78,30 @@ export class UnitControlPanel extends Panel { document.addEventListener("showAdvancedSettings", () => { this.#updateAdvancedSettingsDialog(getUnitsManager().getSelectedUnits()); this.#advancedSettingsDialog.classList.remove("hide"); - }) + }); this.hide(); } - // Do this after panel is hidden (make sure there's a reset) - hide() { - super.hide(); - - this.#expectedAltitude = -1; - this.#expectedSpeed = -1; + show() { + super.show(); + this.#speedTypeSwitch.resetExpectedValue(); + this.#altitudeTypeSwitch.resetExpectedValue(); + this.#onOffSwitch.resetExpectedValue(); + this.#followRoadsSwitch.resetExpectedValue(); + this.#altitudeSlider.resetExpectedValue(); + this.#speedSlider.resetExpectedValue(); } 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 || ""; + var label = unit.getDatabase()?.getByName(unit.getBaseData().name)?.label || unit.getBaseData().name; - button.setAttribute("data-short-label", database?.getByName(unit.getBaseData().name)?.shortLabel || unit.getBaseData().name); + button.setAttribute("data-label", label); button.setAttribute("data-callsign", callsign); button.setAttribute("data-coalition", unit.getMissionData().coalition); @@ -131,11 +122,53 @@ export class UnitControlPanel extends Panel { 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); + const element = this.getElement(); + const units = getUnitsManager().getSelectedUnits(); + const selectedUnitsTypes = getUnitsManager().getSelectedUnitsTypes(); + + if (element != null && units.length > 0) { + /* Toggle visibility of control elements */ + element.toggleAttribute("data-show-categories-tooltip", selectedUnitsTypes.length > 1); + element.toggleAttribute("data-show-speed-slider", selectedUnitsTypes.length == 1); + element.toggleAttribute("data-show-altitude-slider", selectedUnitsTypes.length == 1 && (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); + element.toggleAttribute("data-show-roe", true); + element.toggleAttribute("data-show-threat", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); + element.toggleAttribute("data-show-emissions-countermeasures", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); + element.toggleAttribute("data-show-on-off", (selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit")) && !(selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); + element.toggleAttribute("data-show-follow-roads", (selectedUnitsTypes.length == 1 && selectedUnitsTypes.includes("GroundUnit"))); + element.toggleAttribute("data-show-advanced-settings-button", units.length == 1); + + /* Flight controls */ + var targetAltitude: number | undefined = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetAltitude}); + var targetAltitudeType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetAltitudeType}); + var targetSpeed = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetSpeed}); + var targetSpeedType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetSpeedType}); + var onOff = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().onOff}); + var followRoads = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().followRoads}); + if (selectedUnitsTypes.length == 1) { + this.#altitudeTypeSwitch.setValue(targetAltitudeType != undefined? targetAltitudeType == "AGL": undefined, false); + this.#speedTypeSwitch.setValue(targetSpeedType != undefined? targetSpeedType == "GS": undefined, false); + + this.#speedSlider.setMinMax(minSpeedValues[selectedUnitsTypes[0]], maxSpeedValues[selectedUnitsTypes[0]]); + this.#altitudeSlider.setMinMax(minAltitudeValues[selectedUnitsTypes[0]], maxAltitudeValues[selectedUnitsTypes[0]]); + this.#speedSlider.setIncrement(speedIncrements[selectedUnitsTypes[0]]); + this.#altitudeSlider.setIncrement(altitudeIncrements[selectedUnitsTypes[0]]); + + this.#speedSlider.setActive(targetSpeed != undefined); + if (targetSpeed != undefined) + this.#speedSlider.setValue(msToKnots(targetSpeed), false); + + this.#altitudeSlider.setActive(targetAltitude != undefined); + if (targetAltitude != undefined) + this.#altitudeSlider.setValue(mToFt(targetAltitude), false); + } + else { + this.#speedSlider.setActive(false); + this.#altitudeSlider.setActive(false); + } + + /* Option buttons */ this.#optionButtons["ROE"].forEach((button: HTMLButtonElement) => { button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().ROE === button.value)) }); @@ -147,75 +180,13 @@ export class UnitControlPanel extends Panel { this.#optionButtons["emissionsCountermeasures"].forEach((button: HTMLButtonElement) => { button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().emissionsCountermeasures === button.value)) }); + + this.#onOffSwitch.setValue(onOff, false); + this.#followRoadsSwitch.setValue(followRoads, false); } } } - /* 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) { - if (this.#expectedAltitude < 0 || altitude === this.#expectedAltitude) { - this.#expectedAltitude = -1; - return true; - } - return false; - } - - #updateCanSetSpeedSlider(altitude: number) { - if (this.#expectedSpeed < 0 || altitude === this.#expectedSpeed) { - this.#expectedSpeed = -1; - return true; - } - return false; - } - - #showFlightControlSliders(units: Unit[]) { - if (getUnitsManager().getSelectedUnitsType() !== undefined) - this.#airspeedSlider.show() - else - this.#airspeedSlider.hide(); - - if (getUnitsManager().getSelectedUnitsType() === "Aircraft" || getUnitsManager().getSelectedUnitsType() === "Helicopter") - this.#altitudeSlider.show() - else - this.#altitudeSlider.hide(); - - this.getElement().querySelector(`#categories-tooltip`)?.classList.toggle("hide", getUnitsManager().getSelectedUnitsType() !== undefined); - - var unitsType = getUnitsManager().getSelectedUnitsType(); - var targetAltitude = getUnitsManager().getSelectedUnitsTargetAltitude(); - var targetSpeed = getUnitsManager().getSelectedUnitsTargetSpeed(); - - if (unitsType != undefined) { - if (["GroundUnit", "NavyUnit"].includes(unitsType)) - this.#altitudeSlider.hide() - - this.#airspeedSlider.setMinMax(minSpeedValues[unitsType], maxSpeedValues[unitsType]); - this.#altitudeSlider.setMinMax(minAltitudeValues[unitsType], maxAltitudeValues[unitsType]); - this.#airspeedSlider.setIncrement(speedIncrements[unitsType]); - this.#altitudeSlider.setIncrement(altitudeIncrements[unitsType]); - - this.#airspeedSlider.setActive(targetSpeed != undefined); - if (targetSpeed != undefined) { - targetSpeed *= 1.94384; - if (this.#updateCanSetSpeedSlider(targetSpeed)) { - this.#airspeedSlider.setValue(targetSpeed); - } - } - - this.#altitudeSlider.setActive(targetAltitude != undefined); - if (targetAltitude != undefined) { - targetAltitude /= 0.3048; - if (this.#updateCanSetAltitudeSlider(targetAltitude)) { - this.#altitudeSlider.setValue(targetAltitude); - } - } - } - else { - this.#airspeedSlider.setActive(false); - this.#altitudeSlider.setActive(false); - } - } - #updateAdvancedSettingsDialog(units: Unit[]) { if (units.length == 1) diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts index 4d4eb77e..523f7f10 100644 --- a/client/src/panels/unitinfopanel.ts +++ b/client/src/panels/unitinfopanel.ts @@ -16,7 +16,7 @@ export class UnitInfoPanel extends Panel { #latitude: HTMLElement; #longitude: HTMLElement; #loadoutContainer: HTMLElement; - #silhouette: HTMLElement; + #silhouette: HTMLImageElement; #unitControl: HTMLElement; #unitLabel: HTMLElement; #unitName: HTMLElement; @@ -24,21 +24,21 @@ export class UnitInfoPanel extends Panel { constructor(ID: string) { super(ID); - this.#altitude = (this.getElement().querySelector("#altitude")); - this.#currentTask = (this.getElement().querySelector("#current-task")); - this.#groundSpeed = (this.getElement().querySelector("#ground-speed")); - this.#fuelBar = (this.getElement().querySelector("#fuel-bar")); - this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")); - this.#groupName = (this.getElement().querySelector("#group-name")); - this.#heading = (this.getElement().querySelector("#heading")); - this.#name = (this.getElement().querySelector("#name")); - this.#latitude = (this.getElement().querySelector("#latitude")); - this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")); - this.#longitude = (this.getElement().querySelector("#longitude")); - this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")); - this.#unitControl = (this.getElement().querySelector("#unit-control")); - this.#unitLabel = (this.getElement().querySelector("#unit-label")); - this.#unitName = (this.getElement().querySelector("#unit-name")); + this.#altitude = (this.getElement().querySelector("#altitude")) as HTMLElement; + this.#currentTask = (this.getElement().querySelector("#current-task")) as HTMLElement; + this.#groundSpeed = (this.getElement().querySelector("#ground-speed")) as HTMLElement; + this.#fuelBar = (this.getElement().querySelector("#fuel-bar")) as HTMLElement; + this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")) as HTMLElement; + this.#groupName = (this.getElement().querySelector("#group-name")) as HTMLElement; + this.#heading = (this.getElement().querySelector("#heading")) as HTMLElement; + this.#name = (this.getElement().querySelector("#name")) as HTMLElement; + this.#latitude = (this.getElement().querySelector("#latitude")) as HTMLElement; + this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")) as HTMLElement; + this.#longitude = (this.getElement().querySelector("#longitude")) as HTMLElement; + this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")) as HTMLImageElement; + this.#unitControl = (this.getElement().querySelector("#unit-control")) as HTMLElement; + this.#unitLabel = (this.getElement().querySelector("#unit-label")) as HTMLElement; + this.#unitName = (this.getElement().querySelector("#unit-name")) as HTMLElement; document.addEventListener("unitsSelection", (e: CustomEvent) => this.#onUnitsSelection(e.detail)); document.addEventListener("unitsDeselection", (e: CustomEvent) => this.#onUnitsDeselection(e.detail)); @@ -57,30 +57,20 @@ export class UnitInfoPanel extends Panel { 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; - //this.#name.innerText = baseData.name; - //this.#heading.innerText = String(Math.floor(rad2deg(unit.getFlightData().heading)) + " °"); - //this.#altitude.innerText = String(Math.floor(unit.getFlightData().altitude / 0.3048) + " ft"); - //this.#groundSpeed.innerText = String(Math.floor(unit.getFlightData().speed * 1.94384) + " kts"); this.#fuelBar.style.width = String(unit.getMissionData().fuel + "%"); this.#fuelPercentage.dataset.percentage = "" + unit.getMissionData().fuel; - //this.#latitude.innerText = ConvertDDToDMS(unit.getFlightData().latitude, false); - //this.#longitude.innerText = ConvertDDToDMS(unit.getFlightData().longitude, true); this.#currentTask.dataset.currentTask = unit.getTaskData().currentTask !== ""? unit.getTaskData().currentTask: "No task"; this.#currentTask.dataset.coalition = unit.getMissionData().coalition; - this.#silhouette.setAttribute( "style", `--loadout-background-image:url('/images/units/${aircraftDatabase.getByName( baseData.name )?.filename}');` );; - + this.#silhouette.src = `/images/units/${unit.getDatabase()?.getByName(baseData.name)?.filename}`; + this.#silhouette.classList.toggle("hide", unit.getDatabase()?.getByName(baseData.name)?.filename == undefined || unit.getDatabase()?.getByName(baseData.name)?.filename == ''); + /* Add the loadout elements */ const items = this.#loadoutContainer.querySelector( "#loadout-items" ); - if ( items ) { - const ammo = Object.values( unit.getMissionData().ammo ); - if ( ammo.length > 0 ) { - items.replaceChildren(...Object.values(unit.getMissionData().ammo).map( (ammo: any) => { var el = document.createElement("div"); @@ -91,25 +81,28 @@ export class UnitInfoPanel extends Panel { )); } else { - - items.innerText = "No loadout"; - + items.innerText = "No loadout"; } - } } } #onUnitsSelection(units: Unit[]){ if (units.length == 1) + { this.show(); + this.#onUnitUpdate(units[0]); + } else this.hide(); } #onUnitsDeselection(units: Unit[]){ if (units.length == 1) + { this.show(); + this.#onUnitUpdate(units[0]); + } else this.hide(); } diff --git a/client/src/server/server.ts b/client/src/server/server.ts index f8648eb5..01317c18 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -1,4 +1,4 @@ -import * as L from 'leaflet' +import { LatLng } from 'leaflet'; import { getConnectionStatusPanel, getInfoPopup, getMissionData, getUnitDataTable, getUnitsManager, setConnectionStatus } from '..'; import { SpawnOptions } from '../controls/mapcontextmenu'; @@ -29,16 +29,22 @@ export function setCredentials(newUsername: string, newCredentials: string) { credentials = newCredentials; } -export function GET(callback: CallableFunction, uri: string, options?: string) { +export function GET(callback: CallableFunction, uri: string, options?: { time?: number }) { var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("GET", `${demoEnabled? DEMO_ADDRESS: REST_ADDRESS}/${uri}${options? options: ''}`, true); + + /* Assemble the request options string */ + var optionsString = ''; + if (options?.time != undefined) + optionsString = `time=${options.time}`; + + + xmlHttp.open("GET", `${demoEnabled ? DEMO_ADDRESS : REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); if (credentials) xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); xmlHttp.onload = function (e) { if (xmlHttp.status == 200) { var data = JSON.parse(xmlHttp.responseText); - if (uri !== UNITS_URI || parseInt(data.time) > lastUpdateTime) - { + if (uri !== UNITS_URI || (options?.time == 0) || parseInt(data.time) > lastUpdateTime) { callback(data); lastUpdateTime = parseInt(data.time); if (isNaN(lastUpdateTime)) @@ -59,14 +65,14 @@ export function GET(callback: CallableFunction, uri: string, options?: string) { xmlHttp.send(null); } -export function POST(request: object, callback: CallableFunction){ +export function POST(request: object, callback: CallableFunction) { var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); + xmlHttp.open("PUT", demoEnabled ? DEMO_ADDRESS : REST_ADDRESS); xmlHttp.setRequestHeader("Content-Type", "application/json"); if (credentials) xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); - xmlHttp.onreadystatechange = () => { - callback(); + xmlHttp.onreadystatechange = () => { + callback(); }; xmlHttp.send(JSON.stringify(request)); } @@ -106,7 +112,7 @@ export function getMission(callback: CallableFunction) { } export function getUnits(callback: CallableFunction, refresh: boolean = false) { - GET(callback, `${UNITS_URI}`, `?time=${refresh? 0: lastUpdateTime}`); + GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }); } export function addDestination(ID: number, path: any) { @@ -115,12 +121,18 @@ export function addDestination(ID: number, path: any) { POST(data, () => { }); } -export function spawnSmoke(color: string, latlng: L.LatLng) { +export function spawnSmoke(color: string, latlng: LatLng) { var command = { "color": color, "location": latlng }; var data = { "smoke": command } POST(data, () => { }); } +export function spawnExplosion(intensity: number, latlng: LatLng) { + var command = { "intensity": intensity, "location": latlng }; + var data = { "explosion": command } + POST(data, () => { }); +} + export function spawnGroundUnit(spawnOptions: SpawnOptions) { var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition }; var data = { "spawnGround": command } @@ -128,7 +140,7 @@ export function spawnGroundUnit(spawnOptions: SpawnOptions) { } export function spawnAircraft(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "payloadName": spawnOptions.loadout != null? spawnOptions.loadout: "", "airbaseName": spawnOptions.airbaseName != null? spawnOptions.airbaseName: ""}; + var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "" }; var data = { "spawnAir": command } POST(data, () => { }); } @@ -139,79 +151,103 @@ export function attackUnit(ID: number, targetID: number) { POST(data, () => { }); } -export function followUnit(ID: number, targetID: number, offset: {"x": number, "y": number, "z": number}) { +export function followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }) { // X: front-rear, positive front // Y: top-bottom, positive bottom // Z: left-right, positive right - - var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"]}; + + var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] }; var data = { "followUnit": command } POST(data, () => { }); } -export function cloneUnit(ID: number, latlng: L.LatLng) { +export function cloneUnit(ID: number, latlng: LatLng) { var command = { "ID": ID, "location": latlng }; var data = { "cloneUnit": command } POST(data, () => { }); } -export function deleteUnit(ID: number) { - var command = { "ID": ID}; +export function deleteUnit(ID: number, explosion: boolean) { + var command = { "ID": ID, "explosion": explosion }; var data = { "deleteUnit": command } POST(data, () => { }); } -export function landAt(ID: number, latlng: L.LatLng) { +export function landAt(ID: number, latlng: LatLng) { var command = { "ID": ID, "location": latlng }; var data = { "landAt": command } POST(data, () => { }); } export function changeSpeed(ID: number, speedChange: string) { - var command = {"ID": ID, "change": speedChange} - var data = {"changeSpeed": command} + var command = { "ID": ID, "change": speedChange } + var data = { "changeSpeed": command } POST(data, () => { }); } export function setSpeed(ID: number, speed: number) { - var command = {"ID": ID, "speed": speed} - var data = {"setSpeed": command} + var command = { "ID": ID, "speed": speed } + var data = { "setSpeed": command } + POST(data, () => { }); +} + +export function setSpeedType(ID: number, speedType: string) { + var command = { "ID": ID, "speedType": speedType } + var data = { "setSpeedType": command } POST(data, () => { }); } export function changeAltitude(ID: number, altitudeChange: string) { - var command = {"ID": ID, "change": altitudeChange} - var data = {"changeAltitude": command} + var command = { "ID": ID, "change": altitudeChange } + var data = { "changeAltitude": command } + POST(data, () => { }); +} + +export function setAltitudeType(ID: number, altitudeType: string) { + var command = { "ID": ID, "altitudeType": altitudeType } + var data = { "setAltitudeType": command } POST(data, () => { }); } export function setAltitude(ID: number, altitude: number) { - var command = {"ID": ID, "altitude": altitude} - var data = {"setAltitude": command} + var command = { "ID": ID, "altitude": altitude } + var data = { "setAltitude": command } POST(data, () => { }); } export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { - var command = {"ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader} - var data = {"setLeader": command} + var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader } + var data = { "setLeader": command } POST(data, () => { }); } export function setROE(ID: number, ROE: string) { - var command = {"ID": ID, "ROE": ROE} - var data = {"setROE": command} + var command = { "ID": ID, "ROE": ROE } + var data = { "setROE": command } POST(data, () => { }); } export function setReactionToThreat(ID: number, reactionToThreat: string) { - var command = {"ID": ID, "reactionToThreat": reactionToThreat} - var data = {"setReactionToThreat": command} + var command = { "ID": ID, "reactionToThreat": reactionToThreat } + var data = { "setReactionToThreat": command } POST(data, () => { }); } export function setEmissionsCountermeasures(ID: number, emissionCountermeasure: string) { - var command = {"ID": ID, "emissionsCountermeasures": emissionCountermeasure} - var data = {"setEmissionsCountermeasures": command} + var command = { "ID": ID, "emissionsCountermeasures": emissionCountermeasure } + var data = { "setEmissionsCountermeasures": command } + POST(data, () => { }); +} + +export function setOnOff(ID: number, onOff: boolean) { + var command = { "ID": ID, "onOff": onOff } + var data = { "setOnOff": command } + POST(data, () => { }); +} + +export function setFollowRoads(ID: number, followRoads: boolean) { + var command = { "ID": ID, "followRoads": followRoads } + var data = { "setFollowRoads": command } POST(data, () => { }); } @@ -221,14 +257,38 @@ export function refuel(ID: number) { POST(data, () => { }); } -export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) -{ - var command = { "ID": ID, - "isTanker": isTanker, - "isAWACS": isAWACS, - "TACAN": TACAN, - "radio": radio, - "generalSettings": generalSettings +export function bombPoint(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombPoint": command } + POST(data, () => { }); +} + +export function carpetBomb(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "carpetBomb": command } + POST(data, () => { }); +} + +export function bombBuilding(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "bombBuilding": command } + POST(data, () => { }); +} + +export function fireAtArea(ID: number, latlng: LatLng) { + var command = { "ID": ID, "location": latlng } + var data = { "fireAtArea": command } + POST(data, () => { }); +} + +export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { + var command = { + "ID": ID, + "isTanker": isTanker, + "isAWACS": isAWACS, + "TACAN": TACAN, + "radio": radio, + "generalSettings": generalSettings }; var data = { "setAdvancedOptions": command }; diff --git a/client/src/units/aircraftdatabase.ts b/client/src/units/aircraftdatabase.ts index f2448468..845f5aba 100644 --- a/client/src/units/aircraftdatabase.ts +++ b/client/src/units/aircraftdatabase.ts @@ -2603,7 +2603,7 @@ export class AircraftDatabase extends UnitDatabase { } ], "roles": [ - "Recon" + "Reconnaissance" ], "code": "R-60M*2", "name": "Heavy / Fox 2 / Long Range" diff --git a/client/src/units/groundunitsdatabase.ts b/client/src/units/groundunitsdatabase.ts index 0cf7150a..b3ed6b62 100644 --- a/client/src/units/groundunitsdatabase.ts +++ b/client/src/units/groundunitsdatabase.ts @@ -123,7 +123,7 @@ export class GroundUnitsDatabase extends UnitDatabase { } ], "filename": "" - } + }, "2B11 mortar": { "name": "2B11 mortar", "label": "2B11 mortar", diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index d2c70b46..0fd1d761 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,12 +1,14 @@ 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, setEmissionsCountermeasures } from '../server/server'; +import { mToFt, msToKnots, rad2deg } from '../other/utils'; +import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads, bombPoint, carpetBomb, bombBuilding, fireAtArea } from '../server/server'; import { aircraftDatabase } from './aircraftdatabase'; import { groundUnitsDatabase } from './groundunitsdatabase'; import { CustomMarker } from '../map/custommarker'; import { SVGInjector } from '@tanem/svg-injector'; import { UnitDatabase } from './unitdatabase'; +import { BOMBING, CARPET_BOMBING, FIRE_AT_AREA, IDLE, MOVE_UNIT } from '../map/map'; +import { TargetMarker } from '../map/targetmarker'; var pathIcon = new Icon({ iconUrl: '/resources/theme/images/markers/marker-icon.png', @@ -49,9 +51,15 @@ export class Unit extends CustomMarker { currentTask: "", activePath: {}, targetSpeed: 0, + targetSpeedType: "GS", targetAltitude: 0, + targetAltitudeType: "AGL", + targetLocation: {}, isTanker: false, isAWACS: false, + onOff: true, + followRoads: false, + targetID: 0 }, optionsData: { ROE: "", @@ -74,6 +82,8 @@ export class Unit extends CustomMarker { #pathPolyline: Polyline; #targetsPolylines: Polyline[]; #miniMapMarker: CircleMarker | null = null; + #targetLocationMarker: TargetMarker; + #targetLocationPolyline: Polyline; #timer: number = 0; @@ -105,6 +115,9 @@ export class Unit extends CustomMarker { this.#pathPolyline.addTo(getMap()); this.#targetsPolylines = []; + this.#targetLocationMarker = new TargetMarker(new LatLng(0, 0)); + this.#targetLocationPolyline = new Polyline([], { color: '#FF0000', weight: 3, opacity: 0.5, smoothFactor: 1 }); + /* Deselect units if they are hidden */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { window.setTimeout(() => { this.setSelected(this.getSelected() && !this.getHidden()) }, 300); @@ -116,8 +129,6 @@ export class Unit extends CustomMarker { /* Set the unit data */ this.setData(data); - - } getMarkerCategory() { @@ -150,11 +161,16 @@ export class Unit extends CustomMarker { if ((this.getBaseData().alive || !selected) && this.getSelectable() && this.getSelected() != selected) { this.#selected = selected; this.getElement()?.querySelector(`[data-object|="unit"]`)?.toggleAttribute("data-is-selected", selected); - if (selected) + if (selected) { document.dispatchEvent(new CustomEvent("unitSelection", { detail: this })); - else + this.#updateMarker(); + } + else { document.dispatchEvent(new CustomEvent("unitDeselection", { detail: this })); - this.getGroupMembers().forEach((unit: Unit) => unit.setSelected(selected)); + this.#clearDetectedUnits(); + this.#clearPath(); + this.#clearTarget(); + } } } @@ -203,48 +219,19 @@ export class Unit extends CustomMarker { 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) { - 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) { - 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) { - 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) { - 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) { - for (let key in this.#data.optionsData) - if (key in data.optionsData) - //@ts-ignore - this.#data.optionsData[key] = data.optionsData[key]; - } - + /* Load the data from the received json */ + Object.keys(this.#data).forEach((key1: string) => { + Object.keys(this.#data[key1 as keyof(UnitData)]).forEach((key2: string) => { + if (key1 in data && key2 in data[key1]) { + var value1 = this.#data[key1 as keyof(UnitData)]; + var value2 = value1[key2 as keyof typeof value1]; + if (typeof data[key1][key2] === typeof value2 || typeof value2 === "undefined") + //@ts-ignore + this.#data[key1 as keyof(UnitData)][key2 as keyof typeof struct] = data[key1][key2]; + } + }); + }); + /* Fire an event when a unit dies */ if (aliveChanged && this.getBaseData().alive == false) document.dispatchEvent(new CustomEvent("unitDeath", { detail: this })); @@ -255,13 +242,17 @@ export class Unit extends CustomMarker { if (updateMarker) this.#updateMarker(); - this.#clearTargets(); + this.#clearDetectedUnits(); if (this.getSelected()) { this.#drawPath(); - this.#drawTargets(); + this.#drawDetectedUnits(); + this.#drawTarget(); } - else + else { this.#clearPath(); + this.#clearTarget(); + } + document.dispatchEvent(new CustomEvent("unitUpdated", { detail: this })); } @@ -431,6 +422,15 @@ export class Unit extends CustomMarker { return getUnitsManager().getUnitByID(this.getFormationData().leaderID); } + canRole(roles: string | string[]) { + if (typeof(roles) === "string") + roles = [roles]; + + return this.getDatabase()?.getByName(this.getBaseData().name)?.loadouts.some((loadout: LoadoutBlueprint) => { + return (roles as string[]).some((role: string) => {return loadout.roles.includes(role)}); + }); + } + /********************** Unit commands *************************/ addDestination(latlng: L.LatLng) { if (!this.getMissionData().flags.Human) { @@ -485,11 +485,21 @@ export class Unit extends CustomMarker { setSpeed(this.ID, speed); } + setSpeedType(speedType: string) { + if (!this.getMissionData().flags.Human) + setSpeedType(this.ID, speedType); + } + setAltitude(altitude: number) { if (!this.getMissionData().flags.Human) setAltitude(this.ID, altitude); } + setAltitudeType(altitudeType: string) { + if (!this.getMissionData().flags.Human) + setAltitudeType(this.ID, altitudeType); + } + setROE(ROE: string) { if (!this.getMissionData().flags.Human) setROE(this.ID, ROE); @@ -510,8 +520,18 @@ export class Unit extends CustomMarker { setLeader(this.ID, isLeader, wingmenIDs); } - delete() { - deleteUnit(this.ID); + setOnOff(onOff: boolean) { + if (!this.getMissionData().flags.Human) + setOnOff(this.ID, onOff); + } + + setFollowRoads(followRoads: boolean) { + if (!this.getMissionData().flags.Human) + setFollowRoads(this.ID, followRoads); + } + + delete(explosion: boolean) { + deleteUnit(this.ID, explosion); } refuel() { @@ -524,6 +544,22 @@ export class Unit extends CustomMarker { setAdvacedOptions(this.ID, isTanker, isAWACS, TACAN, radio, generalSettings); } + bombPoint(latlng: LatLng) { + bombPoint(this.ID, latlng); + } + + carpetBomb(latlng: LatLng) { + carpetBomb(this.ID, latlng); + } + + bombBuilding(latlng: LatLng) { + bombBuilding(this.ID, latlng); + } + + fireAtArea(latlng: LatLng) { + fireAtArea(this.ID, latlng); + } + /***********************************************/ onAdd(map: Map): this { super.onAdd(map); @@ -534,10 +570,9 @@ export class Unit extends CustomMarker { /***********************************************/ #onClick(e: any) { if (!this.#preventClick) { - if (getMap().getState() === 'IDLE' || getMap().getState() === 'MOVE_UNIT' || e.originalEvent.ctrlKey) { - if (!e.originalEvent.ctrlKey) { + if (getMap().getState() === IDLE || getMap().getState() === MOVE_UNIT || e.originalEvent.ctrlKey) { + if (!e.originalEvent.ctrlKey) getUnitsManager().deselectAllUnits(); - } this.setSelected(!this.getSelected()); } } @@ -554,20 +589,35 @@ export class Unit extends CustomMarker { #onContextMenu(e: any) { var options: {[key: string]: {text: string, tooltip: string}} = {}; + const selectedUnits = getUnitsManager().getSelectedUnits(); + const selectedUnitTypes = getUnitsManager().getSelectedUnitsTypes(); options["center-map"] = {text: "Center map", tooltip: "Center the map on the unit and follow it"}; - if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().length == 1 && (getUnitsManager().getSelectedUnits().includes(this)))) { + if (selectedUnits.length > 0 && !(selectedUnits.length == 1 && (selectedUnits.includes(this)))) { options["attack"] = {text: "Attack", tooltip: "Attack the unit using A/A or A/G weapons"}; - if (getUnitsManager().getSelectedUnitsType() === "Aircraft") + if (getUnitsManager().getSelectedUnitsTypes().length == 1 && getUnitsManager().getSelectedUnitsTypes()[0] === "Aircraft") options["follow"] = {text: "Follow", tooltip: "Follow the unit at a user defined distance and position"};; } - else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { + else if ((selectedUnits.length > 0 && (selectedUnits.includes(this))) || selectedUnits.length == 0) { if (this.getBaseData().category == "Aircraft") { - options["refuel"] = {text: "AAR Refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR + options["refuel"] = {text: "Air to air refuel", tooltip: "Refuel unit at the nearest AAR Tanker. If no tanker is available the unit will RTB."}; // TODO Add some way of knowing which aircraft can AAR } } + if ((selectedUnits.length === 0 && this.getBaseData().category == "Aircraft") || (selectedUnitTypes.length === 1 && ["Aircraft"].includes(selectedUnitTypes[0]))) + { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["CAS", "Strike"])})) { + options["bomb"] = {text: "Precision bombing", tooltip: "Precision bombing of a specific point"}; + options["carpet-bomb"] = {text: "Carpet bombing", tooltip: "Carpet bombing close to a point"}; + } + } + + if ((selectedUnits.length === 0 && this.getBaseData().category == "GroundUnit") || selectedUnitTypes.length === 1 && ["GroundUnit"].includes(selectedUnitTypes[0])) { + if (selectedUnits.concat([this]).every((unit: Unit) => {return unit.canRole(["Gun Artillery", "Rocket Artillery", "Infantry", "IFV", "Tank"])})) + options["fire-at-area"] = {text: "Fire at area", tooltip: "Fire at a large area"}; + } + if (Object.keys(options).length > 0) { getMap().showUnitContextMenu(e); getMap().getUnitContextMenu().setOptions(options, (option: string) => { @@ -586,6 +636,12 @@ export class Unit extends CustomMarker { getUnitsManager().selectedUnitsRefuel(); else if (action === "follow") this.#showFollowOptions(e); + else if (action === "bomb") + getMap().setState(BOMBING); + else if (action === "carpet-bomb") + getMap().setState(CARPET_BOMBING); + else if (action === "fire-at-area") + getMap().setState(FIRE_AT_AREA); } #showFollowOptions(e: any) { @@ -670,14 +726,16 @@ export class Unit extends CustomMarker { element.querySelector(".unit")?.setAttribute("data-state", "human"); else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus) element.querySelector(".unit")?.setAttribute("data-state", "dcs"); + else if ((this.getBaseData().category == "Aircraft" || this.getBaseData().category == "Helicopter") && !this.getMissionData().hasTask) + element.querySelector(".unit")?.setAttribute("data-state", "no-task"); else // Unit is under Olympus control element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase()); /* Set altitude and speed */ if (element.querySelector(".unit-altitude")) - (element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 100)); + (element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(mToFt(this.getFlightData().altitude) / 100)); if (element.querySelector(".unit-speed")) - (element.querySelector(".unit-speed")).innerText = String(Math.floor(this.getFlightData().speed * 1.94384)); + (element.querySelector(".unit-speed")).innerText = String(Math.floor(msToKnots(this.getFlightData().speed))) + "GS"; /* Rotate elements according to heading */ element.querySelectorAll("[data-rotate-to-heading]").forEach(el => { @@ -768,35 +826,74 @@ export class Unit extends CustomMarker { this.#pathPolyline.setLatLngs([]); } - #drawTargets() { + #drawDetectedUnits() { for (let index in this.getMissionData().targets) { var targetData = this.getMissionData().targets[index]; - var target = getUnitsManager().getUnitByID(targetData.object["id_"]) - if (target != null) { - var startLatLng = new LatLng(this.getFlightData().latitude, this.getFlightData().longitude) - var endLatLng = new LatLng(target.getFlightData().latitude, target.getFlightData().longitude) + if (targetData.object != undefined){ + var target = getUnitsManager().getUnitByID(targetData.object["id_"]) + if (target != null) { + var startLatLng = new LatLng(this.getFlightData().latitude, this.getFlightData().longitude) + var endLatLng = new LatLng(target.getFlightData().latitude, target.getFlightData().longitude) - var color; - if (targetData.detectionMethod === "RADAR") - color = "#FFFF00"; - else if (targetData.detectionMethod === "VISUAL") - color = "#FF00FF"; - else if (targetData.detectionMethod === "RWR") - color = "#00FF00"; - else - color = "#FFFFFF"; - var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1 }); - targetPolyline.addTo(getMap()); - this.#targetsPolylines.push(targetPolyline) + var color; + if (targetData.detectionMethod === "RADAR") + color = "#FFFF00"; + else if (targetData.detectionMethod === "VISUAL") + color = "#FF00FF"; + else if (targetData.detectionMethod === "RWR") + color = "#00FF00"; + else + color = "#FFFFFF"; + var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1, dashArray: "4, 8" }); + targetPolyline.addTo(getMap()); + this.#targetsPolylines.push(targetPolyline) + } } } } - #clearTargets() { + #clearDetectedUnits() { for (let index in this.#targetsPolylines) { getMap().removeLayer(this.#targetsPolylines[index]) } } + + #drawTarget() { + const targetLocation = this.getTaskData().targetLocation; + + if (targetLocation.latitude && targetLocation.longitude && targetLocation.latitude != 0 && targetLocation.longitude != 0) { + const lat = targetLocation.latitude; + const lng = targetLocation.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else if (this.getTaskData().targetID != 0 && getUnitsManager().getUnitByID(this.getTaskData().targetID)) { + const flightData = getUnitsManager().getUnitByID(this.getTaskData().targetID)?.getFlightData(); + const lat = flightData?.latitude; + const lng = flightData?.longitude; + if (lat && lng) + this.#drawTargetLocation(new LatLng(lat, lng)); + } + else + this.#clearTarget(); + } + + #drawTargetLocation(targetLocation: LatLng) { + if (!getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.addTo(getMap()); + if (!getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.addTo(getMap()); + this.#targetLocationMarker.setLatLng(new LatLng(targetLocation.lat, targetLocation.lng)); + this.#targetLocationPolyline.setLatLngs([new LatLng(this.getFlightData().latitude, this.getFlightData().longitude), new LatLng(targetLocation.lat, targetLocation.lng)]) + } + + #clearTarget() { + if (getMap().hasLayer(this.#targetLocationMarker)) + this.#targetLocationMarker.removeFrom(getMap()); + + if (getMap().hasLayer(this.#targetLocationPolyline)) + this.#targetLocationPolyline.removeFrom(getMap()); + } } export class AirUnit extends Unit { @@ -850,7 +947,7 @@ export class GroundUnit extends Unit { showVvi: false, showHotgroup: true, showUnitIcon: true, - showShortLabel: true, + showShortLabel: false, showFuel: false, showAmmo: false, showSummary: false, diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 95fbb744..dc3fd58b 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -2,8 +2,8 @@ import { LatLng, LatLngBounds } from "leaflet"; import { getHotgroupPanel, getInfoPopup, getMap, getUnitDataTable } from ".."; import { Unit } from "./unit"; import { cloneUnit } from "../server/server"; +import { deg2rad, keyEventWasInInput, latLngToMercator, mToFt, mercatorToLatLng, msToKnots } from "../other/utils"; import { IDLE, MOVE_UNIT } from "../map/map"; -import { deg2rad, keyEventWasInInput, latLngToMercator, mercatorToLatLng } from "../other/utils"; export class UnitsManager { #units: { [ID: number]: Unit }; @@ -20,8 +20,9 @@ export class UnitsManager { document.addEventListener('paste', () => this.pasteUnits()); document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); + document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); + document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); document.addEventListener('keyup', (event) => this.#onKeyUp(event)); - document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()) } getSelectableAircraft() { @@ -51,10 +52,12 @@ export class UnitsManager { } addUnit(ID: number, data: UnitData) { + if (data.baseData && data.baseData.category){ /* The name of the unit category is exactly the same as the constructor name */ - var constructor = Unit.getConstructor(data.baseData.category); - if (constructor != undefined) { - this.#units[ID] = new constructor(ID, data); + var constructor = Unit.getConstructor(data.baseData.category); + if (constructor != undefined) { + this.#units[ID] = new constructor(ID, data); + } } } @@ -145,35 +148,26 @@ export class UnitsManager { this.getUnitsByHotgroup(hotgroup).forEach((unit: Unit) => unit.setSelected(true)) } - getSelectedUnitsType() { + getSelectedUnitsTypes() { if (this.getSelectedUnits().length == 0) - return undefined; + return []; return this.getSelectedUnits().map((unit: Unit) => { return unit.constructor.name + })?.filter((value: any, index: any, array: string[]) => { + return array.indexOf(value) === index; + }); + }; + + getSelectedUnitsVariable(variableGetter: CallableFunction) { + if (this.getSelectedUnits().length == 0) + return undefined; + return this.getSelectedUnits().map((unit: Unit) => { + return variableGetter(unit); })?.reduce((a: any, b: any) => { return a == b ? a : undefined }); }; - 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 - }); - }; - - 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 - }); - }; getSelectedUnitsCoalition() { if (this.getSelectedUnits().length == 0) @@ -258,7 +252,15 @@ export class UnitsManager { for (let idx in selectedUnits) { selectedUnits[idx].setSpeed(speed); } - this.#showActionMessage(selectedUnits, `setting speed to ${speed * 1.94384} kts`); + this.#showActionMessage(selectedUnits, `setting speed to ${msToKnots(speed)} kts`); + } + + selectedUnitsSetSpeedType(speedType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setSpeedType(speedType); + } + this.#showActionMessage(selectedUnits, `setting speed type to ${speedType}`); } selectedUnitsSetAltitude(altitude: number) { @@ -266,7 +268,15 @@ export class UnitsManager { for (let idx in selectedUnits) { selectedUnits[idx].setAltitude(altitude); } - this.#showActionMessage(selectedUnits, `setting altitude to ${altitude / 0.3048} ft`); + this.#showActionMessage(selectedUnits, `setting altitude to ${mToFt(altitude)} ft`); + } + + selectedUnitsSetAltitudeType(altitudeType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setAltitudeType(altitudeType); + } + this.#showActionMessage(selectedUnits, `setting altitude type to ${altitudeType}`); } selectedUnitsSetROE(ROE: string) { @@ -290,7 +300,23 @@ export class UnitsManager { for (let idx in selectedUnits) { selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure); } - this.#showActionMessage(selectedUnits, `reaction to threat set to ${emissionCountermeasure}`); + this.#showActionMessage(selectedUnits, `emissions & countermeasures set to ${emissionCountermeasure}`); + } + + selectedUnitsSetOnOff(onOff: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setOnOff(onOff); + } + this.#showActionMessage(selectedUnits, `unit active set to ${onOff}`); + } + + selectedUnitsSetFollowRoads(followRoads: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setFollowRoads(followRoads); + } + this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`); } @@ -302,10 +328,18 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getBaseData().unitName}`); } - selectedUnitsDelete() { + selectedUnitsDelete(explosion: boolean = false) { var selectedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ + const selectionContainsAHuman = selectedUnits.some( ( unit:Unit ) => { + return unit.getMissionData().flags.Human === true; + }); + + if (selectionContainsAHuman && !confirm( "Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?" ) ) { + return; + } + for (let idx in selectedUnits) { - selectedUnits[idx].delete(); + selectedUnits[idx].delete(explosion); } this.#showActionMessage(selectedUnits, `deleted`); } @@ -406,6 +440,38 @@ export class UnitsManager { return unitDestinations; } + selectedUnitsBombPoint(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombPoint(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsCarpetBomb(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].carpetBomb(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsBombBuilding(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].bombBuilding(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + + selectedUnitsFireAtArea(mouseCoordinates: LatLng) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].fireAtArea(mouseCoordinates); + } + this.#showActionMessage(selectedUnits, `unit bombing point`); + } + /***********************************************/ copyUnits() { this.#copiedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ @@ -428,16 +494,7 @@ export class UnitsManager { /***********************************************/ #onKeyUp(event: KeyboardEvent) { if (!keyEventWasInInput(event) && event.key === "Delete" ) { - - const selectedUnits = this.getSelectedUnits(); - const selectionContainsAHuman = selectedUnits.some( ( unit:Unit ) => { - return unit.getMissionData().flags.Human === true; - }); - - if ( !selectionContainsAHuman || confirm( "Your selection includes a human player. Deleting humans causes their vehicle to crash.\n\nAre you sure you want to do this?" ) ) { - this.selectedUnitsDelete(); - } - + this.selectedUnitsDelete(); } } diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index 30628b6c..0940f076 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -1,15 +1,15 @@
- +
+
@@ -38,6 +38,17 @@
+
+
+
Spawn altitude +
+
+
+
+
+ + +
@@ -72,6 +83,12 @@
+
+ + + + +
diff --git a/client/views/panels/mouseinfo.ejs b/client/views/panels/mouseinfo.ejs index 5127e4a4..c4434cc6 100644 --- a/client/views/panels/mouseinfo.ejs +++ b/client/views/panels/mouseinfo.ejs @@ -1,24 +1,28 @@
-
+
-
+
+ +
-
+
+ +
-
+
-
-
-
-
+
+
+
+
-
+
diff --git a/client/views/panels/navbar.ejs b/client/views/panels/navbar.ejs index 3ae2453a..922fe414 100644 --- a/client/views/panels/navbar.ejs +++ b/client/views/panels/navbar.ejs @@ -6,7 +6,7 @@

DCS Olympus

-
version v0.2.1
+
version v0.3.0
Discord @@ -15,7 +15,7 @@ Github
diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index fe67082c..22fffd52 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -1,4 +1,4 @@ -
+

Selected Units

@@ -6,66 +6,78 @@
- -
- -
- -
-

Flight controls

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

Controls

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

Rules of engagement

- +

Reaction to threat

- +

Radar & ECM

- +
+
+

Unit active

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

Follow roads

+
+
+
- -
+ + +
- - - -
+ +
\ No newline at end of file diff --git a/client/views/panels/unitinfo.ejs b/client/views/panels/unitinfo.ejs index 603f4988..3c03ca9c 100644 --- a/client/views/panels/unitinfo.ejs +++ b/client/views/panels/unitinfo.ejs @@ -14,7 +14,7 @@
-
+
diff --git a/installer/olympus.iss b/installer/olympus.iss index dfc937f3..55664142 100644 --- a/installer/olympus.iss +++ b/installer/olympus.iss @@ -1,5 +1,5 @@ #define nwjsFolder "C:\Users\dpass\Documents\nwjs\" -#define version "v0.2.1-alpha" +#define version "v0.3.0-alpha" [Setup] AppName=DCS Olympus diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index bd61f8d2..71007d8d 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,4 +1,4 @@ -local version = "v0.2.1-alpha" +local version = "v0.3.0-alpha" local debug = true @@ -139,29 +139,73 @@ function Olympus.buildTask(options) pattern = options['pattern'] or "Circle" } } + elseif options['id'] == 'Bombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'Bombing', + params = { + point = {x = point.x, y = point.z}, + attackQty = 1 + } + } + elseif options['id'] == 'CarpetBombing' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'CarpetBombing', + params = { + x = point.x, + y = point.z, + carpetLength = 1000, + attackType = 'Carpet', + expend = "All", + attackQty = 1, + attackQtyLimit = true + } + } + elseif options['id'] == 'AttackMapObject' and options['lat'] and options['lng'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'AttackMapObject', + params = { + point = {x = point.x, y = point.z} + } + } + elseif options['id'] == 'FireAtPoint' and options['lat'] and options['lng'] and options['radius'] then + local point = coord.LLtoLO(options['lat'], options['lng'], 0) + task = { + id = 'FireAtPoint', + params = { + point = {x = point.x, y = point.z}, + radius = options['radius'] + } + } end end return task end -- Move a unit. Since many tasks in DCS are Enroute tasks, this function is an important way to control the unit AI -function Olympus.move(ID, lat, lng, altitude, speed, category, taskOptions) - Olympus.debug("Olympus.move " .. ID .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. speed .. "m/s " .. category, 2) - local unit = Olympus.getUnitByID(ID) - if unit then +function Olympus.move(groupName, lat, lng, altitude, altitudeType, speed, speedType, category, taskOptions) + Olympus.debug("Olympus.move " .. groupName .. " (" .. lat .. ", " .. lng ..") " .. altitude .. "m " .. altitudeType .. " ".. speed .. "m/s " .. category .. " " .. Olympus.serializeTable(taskOptions), 2) + local group = Group.getByName(groupName) + if group then if category == "Aircraft" then - local startPoint = mist.getLeadPos(unit:getGroup()) + local startPoint = mist.getLeadPos(group) local endPoint = coord.LLtoLO(lat, lng, 0) + if altitudeType == "AGL" then + altitude = land.getHeight({x = endPoint.x, y = endPoint.z}) + altitude + end + local path = {} if taskOptions and taskOptions['id'] == 'Land' then path = { - [1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO'), + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), [2] = mist.fixedWing.buildWP(endPoint, landing, speed, 0, 'AGL') } else path = { - [1] = mist.fixedWing.buildWP(startPoint, flyOverPoint, speed, altitude, 'BARO'), + [1] = mist.fixedWing.buildWP(startPoint, turningPoint, speed, altitude, 'BARO'), [2] = mist.fixedWing.buildWP(endPoint, turningPoint, speed, altitude, 'BARO') } end @@ -184,7 +228,6 @@ function Olympus.move(ID, lat, lng, altitude, speed, category, taskOptions) }, }, } - group = unit:getGroup() local groupCon = group:getController() if groupCon then groupCon:setTask(missionTask) @@ -193,20 +236,26 @@ function Olympus.move(ID, lat, lng, altitude, speed, category, taskOptions) elseif category == "GroundUnit" then vars = { - group = unit:getGroup(), + group = group, point = coord.LLtoLO(lat, lng, 0), - form = "Off Road", heading = 0, - speed = speed, - disableRoads = true + speed = speed } + + if taskOptions and taskOptions['id'] == 'FollowRoads' and taskOptions['value'] == true then + vars["disableRoads"] = false + else + vars["form"] = "Off Road" + vars["disableRoads"] = true + end + mist.groupToRandomPoint(vars) Olympus.debug("Olympus.move executed succesfully on a ground unit", 2) else Olympus.debug("Olympus.move not implemented yet for " .. category, 2) end else - Olympus.debug("Error in Olympus.move " .. ID, 2) + Olympus.debug("Error in Olympus.move " .. groupName, 2) end end @@ -228,6 +277,12 @@ function Olympus.smoke(color, lat, lng) trigger.action.smoke(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), colorEnum) end +-- Creates an explosion on the ground +function Olympus.explosion(intensity, lat, lng) + Olympus.debug("Olympus.explosion " .. intensity .. " (" .. lat .. ", " .. lng ..")", 2) + trigger.action.explosion(mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)), intensity) +end + -- Spawns a single ground unit function Olympus.spawnGroundUnit(coalition, unitType, lat, lng) Olympus.debug("Olympus.spawnGroundUnit " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) @@ -279,12 +334,12 @@ end -- payloadName: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType -- airbaseName: a string, if present the aircraft will spawn on the ground of the selected airbase -- payload: a table, if present the unit will receive this specific payload. Overrides payloadName -function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) +function Olympus.spawnAircraft(coalition, unitType, lat, lng, alt, spawnOptions) local payloadName = spawnOptions["payloadName"] local airbaseName = spawnOptions["airbaseName"] local payload = spawnOptions["payload"] - Olympus.debug("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..")", 2) + Olympus.debug("Olympus.spawnAircraft " .. coalition .. " " .. unitType .. " (" .. lat .. ", " .. lng ..", " .. alt .. ")", 2) local spawnLocation = mist.utils.makeVec3GL(coord.LLtoLO(lat, lng, 0)) if payload == nil then @@ -304,7 +359,7 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) ["type"] = unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, - ["alt"] = 20000 * 0.3048, + ["alt"] = alt, ["alt_type"] = "BARO", ["skill"] = "Excellent", ["payload"] = @@ -359,8 +414,61 @@ function Olympus.spawnAircraft(coalition, unitType, lat, lng, spawnOptions) }, } end + else + route = { + ["points"] = + { + [1] = + { + ["alt"] = alt, + ["alt_type"] = "BARO", + ["task"] = + { + ["id"] = "ComboTask", + ["params"] = + { + ["tasks"] = + { + [1] = + { + ["number"] = 1, + ["auto"] = true, + ["id"] = "WrappedAction", + ["enabled"] = true, + ["params"] = + { + ["action"] = + { + ["id"] = "EPLRS", + ["params"] = + { + ["value"] = true + }, + }, + }, + }, + [2] = + { + ["number"] = 2, + ["auto"] = false, + ["id"] = "Orbit", + ["enabled"] = true, + ["params"] = + { + ["pattern"] = "Circle" + }, + }, + }, + }, + }, + ["type"] = "Turning Point", + ["x"] = spawnLocation.x, + ["y"] = spawnLocation.z, + }, -- end of [1] + }, -- end of ["points"] + } -- end of ["route"] end - + local vars = { units = unitTable, @@ -390,7 +498,7 @@ function Olympus.clone(ID, lat, lng, category) local spawnOptions = { payload = Olympus.payloadRegistry[unit:getName()] } - Olympus.spawnAircraft(coalition, unit:getTypeName(), lat, lng, spawnOptions) + Olympus.spawnAircraft(coalition, unit:getTypeName(), lat, lng, unit:getPoint().y, spawnOptions) elseif category == "GroundUnit" then Olympus.spawnGroundUnit(coalition, unit:getTypeName(), lat, lng) end @@ -398,11 +506,11 @@ function Olympus.clone(ID, lat, lng, category) Olympus.debug("Olympus.clone completed successfully", 2) end -function Olympus.delete(ID, lat, lng) - Olympus.debug("Olympus.delete " .. ID, 2) +function Olympus.delete(ID, explosion) + Olympus.debug("Olympus.delete " .. ID .. " " .. tostring(explosion), 2) local unit = Olympus.getUnitByID(ID) if unit then - if unit:getPlayerName() then + if unit:getPlayerName() or explosion then trigger.action.explosion(unit:getPoint() , 250 ) --consider replacing with forcibly deslotting the player, however this will work for now Olympus.debug("Olympus.delete completed successfully", 2) else @@ -412,46 +520,55 @@ function Olympus.delete(ID, lat, lng) end end -function Olympus.setTask(ID, taskOptions) - Olympus.debug("Olympus.setTask " .. ID .. " " .. Olympus.serializeTable(taskOptions), 2) - local unit = Olympus.getUnitByID(ID) - if unit then +function Olympus.setTask(groupName, taskOptions) + Olympus.debug("Olympus.setTask " .. groupName .. " " .. Olympus.serializeTable(taskOptions), 2) + local group = Group.getByName(groupName) + if group then local task = Olympus.buildTask(taskOptions); Olympus.debug("Olympus.setTask " .. Olympus.serializeTable(task), 20) if task then - unit:getGroup():getController():setTask(task) + group:getController():setTask(task) Olympus.debug("Olympus.setTask completed successfully", 2) end end end -function Olympus.resetTask(ID) - Olympus.debug("Olympus.resetTask " .. ID, 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():resetTask() +function Olympus.resetTask(groupName) + Olympus.debug("Olympus.resetTask " .. groupName, 2) + local group = Group.getByName(groupName) + if group then + group:getController():resetTask() Olympus.debug("Olympus.resetTask completed successfully", 2) end end -function Olympus.setCommand(ID, command) - Olympus.debug("Olympus.setCommand " .. ID .. " " .. Olympus.serializeTable(command), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setCommand(command) +function Olympus.setCommand(groupName, command) + Olympus.debug("Olympus.setCommand " .. groupName .. " " .. Olympus.serializeTable(command), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setCommand(command) Olympus.debug("Olympus.setCommand completed successfully", 2) end end -function Olympus.setOption(ID, optionID, optionValue) - Olympus.debug("Olympus.setOption " .. ID .. " " .. optionID .. " " .. tostring(optionValue), 2) - local unit = Olympus.getUnitByID(ID) - if unit then - unit:getGroup():getController():setOption(optionID, optionValue) +function Olympus.setOption(groupName, optionID, optionValue) + Olympus.debug("Olympus.setOption " .. groupName .. " " .. optionID .. " " .. tostring(optionValue), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOption(optionID, optionValue) Olympus.debug("Olympus.setOption completed successfully", 2) end end +function Olympus.setOnOff(groupName, onOff) + Olympus.debug("Olympus.setOnOff " .. groupName .. " " .. tostring(onOff), 2) + local group = Group.getByName(groupName) + if group then + group:getController():setOnOff(onOff) + Olympus.debug("Olympus.setOnOff completed successfully", 2) + end +end + function Olympus.serializeTable(val, name, skipnewlines, depth) skipnewlines = skipnewlines or false depth = depth or 0 diff --git a/scripts/OlympusHook.lua b/scripts/OlympusHook.lua index 7215d3cd..b87c8926 100644 --- a/scripts/OlympusHook.lua +++ b/scripts/OlympusHook.lua @@ -1,4 +1,4 @@ -local version = 'v0.2.1-alpha' +local version = 'v0.3.0-alpha' Olympus = {} Olympus.OlympusDLL = nil diff --git a/src/core/include/aircraft.h b/src/core/include/aircraft.h index 6d15e4f6..7fd3f615 100644 --- a/src/core/include/aircraft.h +++ b/src/core/include/aircraft.h @@ -10,12 +10,4 @@ public: virtual void changeSpeed(wstring change); virtual void changeAltitude(wstring change); - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; - virtual void setTargetSpeed(double newTargetSpeed); - virtual void setTargetAltitude(double newTargetAltitude); - -protected: - double targetSpeed = 300 / 1.94384; - double targetAltitude = 20000 * 0.3048; }; \ No newline at end of file diff --git a/src/core/include/airunit.h b/src/core/include/airunit.h index a46e9909..cabd7b9e 100644 --- a/src/core/include/airunit.h +++ b/src/core/include/airunit.h @@ -12,17 +12,12 @@ class AirUnit : public Unit public: AirUnit(json::value json, int ID); - virtual wstring getCategory() = 0; - virtual void changeSpeed(wstring change) {}; - virtual void changeAltitude(wstring change) {}; - virtual void setTargetSpeed(double newTargetSpeed) {}; - virtual void setTargetAltitude(double newTargetAltitude) {}; + virtual void setState(int newState); + virtual wstring getCategory() = 0; + virtual void changeSpeed(wstring change) = 0; + virtual void changeAltitude(wstring change) = 0; + protected: virtual void AIloop(); - virtual void setState(int newState); - bool isDestinationReached(); - bool setActiveDestination(); - bool updateActivePath(bool looping); - void goToDestination(wstring enrouteTask = L"nil"); }; \ No newline at end of file diff --git a/src/core/include/commands.h b/src/core/include/commands.h index 7752eea8..d4e0bab3 100644 --- a/src/core/include/commands.h +++ b/src/core/include/commands.h @@ -81,8 +81,6 @@ namespace ECMUse { }; } - - /* Base command class */ class Command { @@ -99,12 +97,15 @@ protected: class Move : public Command { public: - Move(int ID, Coords destination, double speed, double altitude, wstring taskOptions): - ID(ID), + Move(wstring groupName, Coords destination, double speed, wstring speedType, double altitude, wstring altitudeType, wstring taskOptions, wstring category): + groupName(groupName), destination(destination), speed(speed), + speedType(speedType), altitude(altitude), - taskOptions(taskOptions) + altitudeType(altitudeType), + taskOptions(taskOptions), + category(category) { priority = CommandPriority::HIGH; }; @@ -112,11 +113,14 @@ public: virtual int getLoad() { return 5; } private: - const int ID; + const wstring groupName; const Coords destination; const double speed; + const wstring speedType; const double altitude; + const wstring altitudeType; const wstring taskOptions; + const wstring category; }; /* Smoke command */ @@ -203,8 +207,9 @@ private: class Delete : public Command { public: - Delete(int ID) : - ID(ID) + Delete(int ID, bool explosion) : + ID(ID), + explosion(explosion) { priority = CommandPriority::HIGH; }; @@ -213,14 +218,15 @@ public: private: const int ID; + const bool explosion; }; /* Follow command */ class SetTask : public Command { public: - SetTask(int ID, wstring task) : - ID(ID), + SetTask(wstring groupName, wstring task) : + groupName(groupName), task(task) { priority = CommandPriority::MEDIUM; @@ -229,7 +235,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring task; }; @@ -237,8 +243,8 @@ private: class ResetTask : public Command { public: - ResetTask(int ID) : - ID(ID) + ResetTask(wstring groupName) : + groupName(groupName) { priority = CommandPriority::HIGH; }; @@ -246,15 +252,15 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; }; /* Set command */ class SetCommand : public Command { public: - SetCommand(int ID, wstring command) : - ID(ID), + SetCommand(wstring groupName, wstring command) : + groupName(groupName), command(command) { priority = CommandPriority::HIGH; @@ -263,7 +269,7 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const wstring command; }; @@ -271,8 +277,8 @@ private: class SetOption : public Command { public: - SetOption(int ID, int optionID, int optionValue) : - ID(ID), + SetOption(wstring groupName, int optionID, int optionValue) : + groupName(groupName), optionID(optionID), optionValue(optionValue), optionBool(false), @@ -281,8 +287,8 @@ public: priority = CommandPriority::HIGH; }; - SetOption(int ID, int optionID, bool optionBool) : - ID(ID), + SetOption(wstring groupName, int optionID, bool optionBool) : + groupName(groupName), optionID(optionID), optionValue(0), optionBool(optionBool), @@ -294,9 +300,45 @@ public: virtual int getLoad() { return 10; } private: - const int ID; + const wstring groupName; const int optionID; const int optionValue; const bool optionBool; const bool isBoolean; -}; \ No newline at end of file +}; + +/* Set on off */ +class SetOnOff : public Command +{ +public: + SetOnOff(wstring groupName, bool onOff) : + groupName(groupName), + onOff(onOff) + { + priority = CommandPriority::HIGH; + }; + virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } + +private: + const wstring groupName; + const bool onOff; +}; + +/* Make a ground explosion */ +class Explosion : public Command +{ +public: + Explosion(int intensity, Coords location) : + location(location), + intensity(intensity) + { + priority = CommandPriority::MEDIUM; + }; + virtual wstring getString(lua_State* L); + virtual int getLoad() { return 10; } + +private: + const Coords location; + const int intensity; +}; diff --git a/src/core/include/groundunit.h b/src/core/include/groundunit.h index 6edb1a3b..6b5930b0 100644 --- a/src/core/include/groundunit.h +++ b/src/core/include/groundunit.h @@ -7,13 +7,14 @@ class GroundUnit : public Unit { public: GroundUnit(json::value json, int ID); - virtual void AIloop(); - virtual wstring getCategory() { return L"GroundUnit"; }; + + virtual void setState(int newState); + virtual void changeSpeed(wstring change); - virtual void changeAltitude(wstring change) {}; - virtual double getTargetSpeed() { return targetSpeed; }; + virtual void setOnOff(bool newOnOff); + virtual void setFollowRoads(bool newFollowRoads); protected: - double targetSpeed = 10; + virtual void AIloop(); }; \ No newline at end of file diff --git a/src/core/include/helicopter.h b/src/core/include/helicopter.h index 50a0e9c2..097990b3 100644 --- a/src/core/include/helicopter.h +++ b/src/core/include/helicopter.h @@ -10,12 +10,4 @@ public: virtual void changeSpeed(wstring change); virtual void changeAltitude(wstring change); - virtual double getTargetSpeed() { return targetSpeed; }; - virtual double getTargetAltitude() { return targetAltitude; }; - virtual void setTargetSpeed(double newTargetSpeed); - virtual void setTargetAltitude(double newTargetAltitude); - -protected: - double targetSpeed = 100 / 1.94384; - double targetAltitude = 5000 * 0.3048; }; \ No newline at end of file diff --git a/src/core/include/navyunit.h b/src/core/include/navyunit.h index d70d86d2..a27b5a31 100644 --- a/src/core/include/navyunit.h +++ b/src/core/include/navyunit.h @@ -9,9 +9,5 @@ public: virtual wstring getCategory() { return L"NavyUnit"; }; virtual void changeSpeed(wstring change); - virtual void changeAltitude(wstring change) {}; - virtual double getTargetSpeed() { return targetSpeed; }; -protected: - double targetSpeed = 10; }; \ No newline at end of file diff --git a/src/core/include/unit.h b/src/core/include/unit.h index 2d6d6bcf..5d6dafb3 100644 --- a/src/core/include/unit.h +++ b/src/core/include/unit.h @@ -6,6 +6,8 @@ #include "measure.h" #include "logger.h" +#define TASK_CHECK_INIT_VALUE 10 + namespace State { enum States @@ -18,7 +20,11 @@ namespace State LAND, REFUEL, AWACS, - TANKER + TANKER, + BOMB_POINT, + CARPET_BOMB, + BOMB_BUILDING, + FIRE_AT_AREA }; }; @@ -59,7 +65,7 @@ public: int getID() { return ID; } void updateExportData(json::value json); void updateMissionData(json::value json); - json::value getData(long long time); + json::value getData(long long time, bool getAll = false); virtual wstring getCategory() { return L"No category"; }; /********** Base data **********/ @@ -70,6 +76,7 @@ public: void setAlive(bool newAlive) { alive = newAlive; addMeasure(L"alive", json::value(newAlive));} void setType(json::value newType) { type = newType; addMeasure(L"type", newType);} void setCountry(int newCountry) { country = newCountry; addMeasure(L"country", json::value(newCountry));} + bool getAI() { return AI; } wstring getName() { return name; } wstring getUnitName() { return unitName; } @@ -84,6 +91,7 @@ public: void setAltitude(double newAltitude) {altitude = newAltitude; addMeasure(L"altitude", json::value(newAltitude));} void setHeading(double newHeading) {heading = newHeading; addMeasure(L"heading", json::value(newHeading));} void setSpeed(double newSpeed) {speed = newSpeed; addMeasure(L"speed", json::value(newSpeed));} + double getLatitude() { return latitude; } double getLongitude() { return longitude; } double getAltitude() { return altitude; } @@ -94,9 +102,10 @@ public: void setFuel(double newFuel) { fuel = newFuel; addMeasure(L"fuel", json::value(newFuel));} void setAmmo(json::value newAmmo) { ammo = newAmmo; addMeasure(L"ammo", json::value(newAmmo));} void setTargets(json::value newTargets) {targets = newTargets; addMeasure(L"targets", json::value(newTargets));} - void setHasTask(bool newHasTask) { hasTask = newHasTask; addMeasure(L"hasTask", json::value(newHasTask)); } + void setHasTask(bool newHasTask); void setCoalitionID(int newCoalitionID); void setFlags(json::value newFlags) { flags = newFlags; addMeasure(L"flags", json::value(newFlags));} + double getFuel() { return fuel; } json::value getAmmo() { return ammo; } json::value getTargets() { return targets; } @@ -108,31 +117,38 @@ public: /********** Formation data **********/ void setLeaderID(int newLeaderID) { leaderID = newLeaderID; addMeasure(L"leaderID", json::value(newLeaderID)); } void setFormationOffset(Offset formationOffset); + int getLeaderID() { return leaderID; } Offset getFormationoffset() { return formationOffset; } /********** Task data **********/ - void setCurrentTask(wstring newCurrentTask) { currentTask = newCurrentTask;addMeasure(L"currentTask", json::value(newCurrentTask)); } - virtual void setTargetSpeed(double newTargetSpeed) { targetSpeed = newTargetSpeed; addMeasure(L"targetSpeed", json::value(newTargetSpeed));} - virtual void setTargetAltitude(double newTargetAltitude) { targetAltitude = newTargetAltitude; addMeasure(L"targetAltitude", json::value(newTargetAltitude));} //TODO fix, double definition + void setCurrentTask(wstring newCurrentTask) { currentTask = newCurrentTask; addMeasure(L"currentTask", json::value(newCurrentTask)); } + void setTargetSpeed(double newTargetSpeed); + void setTargetAltitude(double newTargetAltitude); + void setTargetSpeedType(wstring newTargetSpeedType); + void setTargetAltitudeType(wstring newTargetAltitudeType); void setActiveDestination(Coords newActiveDestination) { activeDestination = newActiveDestination; addMeasure(L"activeDestination", json::value("")); } // TODO fix void setActivePath(list newActivePath); - void clearActivePath(); - void pushActivePathFront(Coords newActivePathFront); - void pushActivePathBack(Coords newActivePathBack); - void popActivePathFront(); void setTargetID(int newTargetID) { targetID = newTargetID; addMeasure(L"targetID", json::value(newTargetID));} + void setTargetLocation(Coords newTargetLocation); void setIsTanker(bool newIsTanker); void setIsAWACS(bool newIsAWACS); + virtual void setOnOff(bool newOnOff) { onOff = newOnOff; addMeasure(L"onOff", json::value(newOnOff));}; + virtual void setFollowRoads(bool newFollowRoads) { followRoads = newFollowRoads; addMeasure(L"followRoads", json::value(newFollowRoads)); }; wstring getCurrentTask() { return currentTask; } virtual double getTargetSpeed() { return targetSpeed; }; virtual double getTargetAltitude() { return targetAltitude; }; + virtual wstring getTargetSpeedType() { return targetSpeedType; }; + virtual wstring getTargetAltitudeType() { return targetAltitudeType; }; Coords getActiveDestination() { return activeDestination; } list getActivePath() { return activePath; } int getTargetID() { return targetID; } + Coords getTargetLocation() { return targetLocation; } bool getIsTanker() { return isTanker; } bool getIsAWACS() { return isAWACS; } + bool getOnOff() { return onOff; }; + bool getFollowRoads() { return followRoads; }; /********** Options data **********/ void setROE(wstring newROE); @@ -142,6 +158,7 @@ public: void setRadio(Options::Radio newradio); void setGeneralSettings(Options::GeneralSettings newGeneralSettings); void setEPLRS(bool newEPLRS); + wstring getROE() { return ROE; } wstring getReactionToThreat() { return reactionToThreat; } wstring getEmissionsCountermeasures() { return emissionsCountermeasures; }; @@ -152,16 +169,21 @@ public: /********** Control functions **********/ void landAt(Coords loc); - virtual void changeSpeed(wstring change){}; - virtual void changeAltitude(wstring change){}; + virtual void changeSpeed(wstring change) {}; + virtual void changeAltitude(wstring change) {}; void resetActiveDestination(); virtual void setState(int newState) { state = newState; }; void resetTask(); + void clearActivePath(); + void pushActivePathFront(Coords newActivePathFront); + void pushActivePathBack(Coords newActivePathBack); + void popActivePathFront(); protected: int ID; map measures; + int taskCheckCounter = 0; /********** Base data **********/ bool AI = false; @@ -196,11 +218,16 @@ protected: wstring currentTask = L""; double targetSpeed = 0; double targetAltitude = 0; + wstring targetSpeedType = L"GS"; + wstring targetAltitudeType = L"AGL"; list activePath; - Coords activeDestination = Coords(0); + Coords activeDestination = Coords(NULL); int targetID = NULL; + Coords targetLocation = Coords(NULL); bool isTanker = false; bool isAWACS = false; + bool onOff = true; + bool followRoads = false; /********** Options data **********/ wstring ROE = L"Designated"; @@ -224,4 +251,10 @@ protected: bool isLeaderAlive(); virtual void AIloop() = 0; void addMeasure(wstring key, json::value value); + bool isDestinationReached(double threshold); + bool setActiveDestination(); + bool updateActivePath(bool looping); + void goToDestination(wstring enrouteTask = L"nil"); + bool checkTaskFailed(); + void resetTaskFailedCounter(); }; diff --git a/src/core/include/unitsmanager.h b/src/core/include/unitsmanager.h index a851a388..d3d5d65b 100644 --- a/src/core/include/unitsmanager.h +++ b/src/core/include/unitsmanager.h @@ -11,10 +11,15 @@ public: ~UnitsManager(); Unit* getUnit(int ID); + bool isUnitInGroup(Unit* unit); + bool isUnitGroupLeader(Unit* unit); + Unit* getGroupLeader(int ID); + Unit* getGroupLeader(Unit* unit); + vector getGroupMembers(wstring groupName); void updateExportData(lua_State* L); void updateMissionData(json::value missionData); void getData(json::value& answer, long long time); - void deleteUnit(int ID); + void deleteUnit(int ID, bool explosion); private: map units; diff --git a/src/core/src/aircraft.cpp b/src/core/src/aircraft.cpp index 79f2694d..f03c616b 100644 --- a/src/core/src/aircraft.cpp +++ b/src/core/src/aircraft.cpp @@ -17,6 +17,9 @@ Aircraft::Aircraft(json::value json, int ID) : AirUnit(json, ID) { log("New Aircraft created with ID: " + to_string(ID)); addMeasure(L"category", json::value(getCategory())); + + double targetSpeed = knotsToMs(300); + double targetAltitude = ftToM(20000); setTargetSpeed(targetSpeed); setTargetAltitude(targetAltitude); }; @@ -24,16 +27,14 @@ Aircraft::Aircraft(json::value json, int ID) : AirUnit(json, ID) void Aircraft::changeSpeed(wstring change) { if (change.compare(L"stop") == 0) - { setState(State::IDLE); - } else if (change.compare(L"slow") == 0) - setTargetSpeed(getTargetSpeed() - 25 / 1.94384); + setTargetSpeed(getTargetSpeed() - knotsToMs(25)); else if (change.compare(L"fast") == 0) - setTargetSpeed(getTargetSpeed() + 25 / 1.94384); + setTargetSpeed(getTargetSpeed() + knotsToMs(25)); - if (getTargetSpeed() < 50 / 1.94384) - setTargetSpeed(50 / 1.94384); + if (getTargetSpeed() < knotsToMs(50)) + setTargetSpeed(knotsToMs(50)); goToDestination(); /* Send the command to reach the destination */ } @@ -43,33 +44,19 @@ void Aircraft::changeAltitude(wstring change) if (change.compare(L"descend") == 0) { if (getTargetAltitude() > 5000) - setTargetAltitude(getTargetAltitude() - 2500 / 3.28084); + setTargetAltitude(getTargetAltitude() - ftToM(2500)); else if (getTargetAltitude() > 0) - setTargetAltitude(getTargetAltitude() - 500 / 3.28084); + setTargetAltitude(getTargetAltitude() - ftToM(500)); } else if (change.compare(L"climb") == 0) { if (getTargetAltitude() > 5000) - setTargetAltitude(getTargetAltitude() + 2500 / 3.28084); + setTargetAltitude(getTargetAltitude() + ftToM(2500)); else if (getTargetAltitude() >= 0) - setTargetAltitude(getTargetAltitude() + 500 / 3.28084); + setTargetAltitude(getTargetAltitude() + ftToM(500)); } if (getTargetAltitude() < 0) setTargetAltitude(0); goToDestination(); /* Send the command to reach the destination */ -} - -void Aircraft::setTargetSpeed(double newTargetSpeed) { - targetSpeed = newTargetSpeed; - addMeasure(L"targetSpeed", json::value(targetSpeed)); - if (activeDestination != NULL) - goToDestination(); -} - -void Aircraft::setTargetAltitude(double newTargetAltitude) { - targetAltitude = newTargetAltitude; - addMeasure(L"targetAltitude", json::value(targetAltitude)); - if (activeDestination != NULL) - goToDestination(); } \ No newline at end of file diff --git a/src/core/src/airunit.cpp b/src/core/src/airunit.cpp index 78d87cdf..331e3ff8 100644 --- a/src/core/src/airunit.cpp +++ b/src/core/src/airunit.cpp @@ -20,7 +20,7 @@ AirUnit::AirUnit(json::value json, int ID) : Unit(json, ID) void AirUnit::setState(int newState) { - /************ Perform any action required when LEAVING a certain state ************/ + /************ Perform any action required when LEAVING a state ************/ if (newState != state) { switch (state) { case State::IDLE: { @@ -43,12 +43,18 @@ void AirUnit::setState(int newState) case State::REFUEL: { break; } + case State::BOMB_POINT: + case State::CARPET_BOMB: + case State::BOMB_BUILDING: { + setTargetLocation(Coords(NULL)); + break; + } default: break; } } - /************ Perform any action required when ENTERING a certain state ************/ + /************ Perform any action required when ENTERING a state ************/ switch (newState) { case State::IDLE: { clearActivePath(); @@ -90,6 +96,24 @@ void AirUnit::setState(int newState) addMeasure(L"currentState", json::value(L"Refuel")); break; } + case State::BOMB_POINT: { + addMeasure(L"currentState", json::value(L"Bombing point")); + clearActivePath(); + resetActiveDestination(); + break; + } + case State::CARPET_BOMB: { + addMeasure(L"currentState", json::value(L"Carpet bombing")); + clearActivePath(); + resetActiveDestination(); + break; + } + case State::BOMB_BUILDING: { + addMeasure(L"currentState", json::value(L"Bombing building")); + clearActivePath(); + resetActiveDestination(); + break; + } default: break; } @@ -100,69 +124,6 @@ void AirUnit::setState(int newState) state = newState; } -bool AirUnit::isDestinationReached() -{ - if (activeDestination != NULL) - { - double dist = 0; - Geodesic::WGS84().Inverse(latitude, longitude, activeDestination.lat, activeDestination.lng, dist); - if (dist < AIR_DEST_DIST_THR) - { - log(unitName + L" destination reached"); - return true; - } - else { - return false; - } - } - else - return true; -} - -bool AirUnit::setActiveDestination() -{ - if (activePath.size() > 0) - { - activeDestination = activePath.front(); - log(unitName + L" active destination set to queue front"); - return true; - } - else - { - activeDestination = Coords(0); - log(unitName + L" active destination set to NULL"); - return false; - } -} - -bool AirUnit::updateActivePath(bool looping) -{ - if (activePath.size() > 0) - { - /* Push the next destination in the queue to the front */ - if (looping) - pushActivePathBack(activePath.front()); - popActivePathFront(); - log(unitName + L" active path front popped"); - return true; - } - else { - return false; - } -} - -void AirUnit::goToDestination(wstring enrouteTask) -{ - if (activeDestination != NULL) - { - Command* command = dynamic_cast(new Move(ID, activeDestination, getTargetSpeed(), getTargetAltitude(), enrouteTask)); - scheduler->appendCommand(command); - setHasTask(true); - } - else - log(unitName + L" error, no active destination!"); -} - void AirUnit::AIloop() { /* State machine */ @@ -170,7 +131,7 @@ void AirUnit::AIloop() case State::IDLE: { currentTask = L"Idle"; - if (!hasTask) + if (!getHasTask()) { std::wostringstream taskSS; if (isTanker) { @@ -182,7 +143,7 @@ void AirUnit::AIloop() else { taskSS << "{ id = 'Orbit', pattern = 'Circle' }"; } - Command* command = dynamic_cast(new SetTask(ID, taskSS.str())); + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); setHasTask(true); } @@ -208,7 +169,7 @@ void AirUnit::AIloop() currentTask = L"Reaching destination"; } - if (activeDestination == NULL || !hasTask) + if (activeDestination == NULL || !getHasTask()) { if (!setActiveDestination()) setState(State::IDLE); @@ -216,7 +177,7 @@ void AirUnit::AIloop() goToDestination(enrouteTask); } else { - if (isDestinationReached()) { + if (isDestinationReached(AIR_DEST_DIST_THR)) { if (updateActivePath(looping) && setActiveDestination()) goToDestination(enrouteTask); else @@ -253,7 +214,7 @@ void AirUnit::AIloop() wstring enrouteTask = enrouteTaskSS.str(); currentTask = L"Attacking " + getTargetName(); - if (!hasTask) + if (!getHasTask()) { setActiveDestination(); goToDestination(enrouteTask); @@ -274,7 +235,7 @@ void AirUnit::AIloop() currentTask = L"Following " + getTargetName(); Unit* leader = unitsManager->getUnit(leaderID); - if (!hasTask) { + if (!getHasTask()) { if (leader != nullptr && leader->getAlive() && formationOffset != NULL) { std::wostringstream taskSS; @@ -287,7 +248,7 @@ void AirUnit::AIloop() << "z = " << formationOffset.z << "}," << "}"; - Command* command = dynamic_cast(new SetTask(ID, taskSS.str())); + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); setHasTask(true); } @@ -297,13 +258,13 @@ void AirUnit::AIloop() case State::REFUEL: { currentTask = L"Refueling"; - if (!hasTask) { + if (!getHasTask()) { if (fuel <= initialFuel) { std::wostringstream taskSS; taskSS << "{" << "id = 'Refuel'" << "}"; - Command* command = dynamic_cast(new SetTask(ID, taskSS.str())); + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); setHasTask(true); } @@ -312,9 +273,45 @@ void AirUnit::AIloop() } } } + case State::BOMB_POINT: { + currentTask = L"Bombing point"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'Bombing', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + } + case State::CARPET_BOMB: { + currentTask = L"Carpet bombing"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'CarpetBombing', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + break; + } + case State::BOMB_BUILDING: { + currentTask = L"Bombing building"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'AttackMapObject', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << "}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); + scheduler->appendCommand(command); + setHasTask(true); + } + break; + } default: break; } addMeasure(L"currentTask", json::value(currentTask)); -} + +} \ No newline at end of file diff --git a/src/core/src/commands.cpp b/src/core/src/commands.cpp index 662e62ab..87f33218 100644 --- a/src/core/src/commands.cpp +++ b/src/core/src/commands.cpp @@ -9,25 +9,20 @@ extern UnitsManager* unitsManager; /* Move command */ wstring Move::getString(lua_State* L) { - Unit* unit = unitsManager->getUnit(ID); - if (unit != nullptr) - { - std::wostringstream commandSS; - commandSS.precision(10); - commandSS << "Olympus.move, " - << ID << ", " - << destination.lat << ", " - << destination.lng << ", " - << altitude << ", " - << speed << ", " - << "\"" << unit->getCategory() << "\"" << ", " - << taskOptions; - return commandSS.str(); - } - else - { - return L""; - } + + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.move, " + << "\"" << groupName << "\"" << ", " + << destination.lat << ", " + << destination.lng << ", " + << altitude << ", " + << "\"" << altitudeType << "\"" << ", " + << speed << ", " + << "\"" << speedType << "\"" << ", " + << "\"" << category << "\"" << ", " + << taskOptions; + return commandSS.str(); } /* Smoke command */ @@ -72,6 +67,7 @@ wstring SpawnAircraft::getString(lua_State* L) << "\"" << unitType << "\"" << ", " << location.lat << ", " << location.lng << ", " + << location.alt << ", " << optionsSS.str(); return commandSS.str(); } @@ -103,7 +99,8 @@ wstring Delete::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.delete, " - << ID; + << ID << ", " + << (explosion ? "true" : "false"); return commandSS.str(); } @@ -113,7 +110,7 @@ wstring SetTask::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.setTask, " - << ID << ", " + << "\"" << groupName << "\"" << ", " << task; return commandSS.str(); @@ -125,7 +122,7 @@ wstring ResetTask::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.resetTask, " - << ID; + << "\"" << groupName << "\""; return commandSS.str(); } @@ -136,7 +133,7 @@ wstring SetCommand::getString(lua_State* L) std::wostringstream commandSS; commandSS.precision(10); commandSS << "Olympus.setCommand, " - << ID << ", " + << "\"" << groupName << "\"" << ", " << command; return commandSS.str(); @@ -150,14 +147,39 @@ wstring SetOption::getString(lua_State* L) if (!isBoolean) { commandSS << "Olympus.setOption, " - << ID << ", " + << "\"" << groupName << "\"" << ", " << optionID << ", " << optionValue; } else { commandSS << "Olympus.setOption, " - << ID << ", " + << "\"" << groupName << "\"" << ", " << optionID << ", " << (optionBool? "true": "false"); } return commandSS.str(); +} + +/* Set onOff command */ +wstring SetOnOff::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + + commandSS << "Olympus.setOnOff, " + << "\"" << groupName << "\"" << ", " + << (onOff ? "true" : "false"); + + return commandSS.str(); +} + +/* Explosion command */ +wstring Explosion::getString(lua_State* L) +{ + std::wostringstream commandSS; + commandSS.precision(10); + commandSS << "Olympus.explosion, " + << intensity << ", " + << location.lat << ", " + << location.lng; + return commandSS.str(); } \ No newline at end of file diff --git a/src/core/src/groundunit.cpp b/src/core/src/groundunit.cpp index 545aefc5..c75ea2c0 100644 --- a/src/core/src/groundunit.cpp +++ b/src/core/src/groundunit.cpp @@ -17,58 +17,134 @@ GroundUnit::GroundUnit(json::value json, int ID) : Unit(json, ID) { log("New Ground Unit created with ID: " + to_string(ID)); addMeasure(L"category", json::value(getCategory())); + + double targetSpeed = 10; setTargetSpeed(targetSpeed); - setTargetAltitude(targetAltitude); }; +void GroundUnit::setState(int newState) +{ + /************ Perform any action required when LEAVING a state ************/ + if (newState != state) { + switch (state) { + case State::IDLE: { + break; + } + case State::REACH_DESTINATION: { + break; + } + case State::FIRE_AT_AREA: { + setTargetLocation(Coords(NULL)); + break; + } + default: + break; + } + } + + /************ Perform any action required when ENTERING a state ************/ + switch (newState) { + case State::IDLE: { + clearActivePath(); + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Idle")); + break; + } + case State::REACH_DESTINATION: { + resetActiveDestination(); + addMeasure(L"currentState", json::value(L"Reach destination")); + break; + } + case State::FIRE_AT_AREA: { + addMeasure(L"currentState", json::value(L"Firing at area")); + clearActivePath(); + resetActiveDestination(); + break; + } + default: + break; + } + + resetTask(); + + log(unitName + L" setting state from " + to_wstring(state) + L" to " + to_wstring(newState)); + state = newState; +} + void GroundUnit::AIloop() { - /* Set the active destination to be always equal to the first point of the active path. This is in common with all AI units */ - if (activePath.size() > 0) - { - if (activeDestination != activePath.front()) + switch (state) { + case State::IDLE: { + currentTask = L"Idle"; + if (getHasTask()) + resetTask(); + break; + } + case State::REACH_DESTINATION: { + wstring enrouteTask = L""; + bool looping = false; + + std::wostringstream taskSS; + taskSS << "{ id = 'FollowRoads', value = " << (getFollowRoads() ? "true" : "false") << " }"; + enrouteTask = taskSS.str(); + + if (activeDestination == NULL || !getHasTask()) { - activeDestination = activePath.front(); - Command* command = dynamic_cast(new Move(ID, activeDestination, getTargetSpeed(), getTargetAltitude(), L"nil")); + if (!setActiveDestination()) + setState(State::IDLE); + else + goToDestination(enrouteTask); + } + else { + if (isDestinationReached(GROUND_DEST_DIST_THR)) { + if (updateActivePath(looping) && setActiveDestination()) + goToDestination(enrouteTask); + else + setState(State::IDLE); + } + } + break; + } + case State::FIRE_AT_AREA: { + currentTask = L"Firing at area"; + + if (!getHasTask()) { + std::wostringstream taskSS; + taskSS << "{id = 'FireAtPoint', lat = " << targetLocation.lat << ", lng = " << targetLocation.lng << ", radius = 1000}"; + Command* command = dynamic_cast(new SetTask(groupName, taskSS.str())); scheduler->appendCommand(command); + setHasTask(true); } } - else - { - if (activeDestination != NULL) - { - log(unitName + L" no more points in active path"); - activeDestination = Coords(0); // Set the active path to NULL - currentTask = L"Idle"; - } + default: + break; } - /* Ground unit AI Loop */ - if (activeDestination != NULL) - { - double newDist = 0; - Geodesic::WGS84().Inverse(latitude, longitude, activeDestination.lat, activeDestination.lng, newDist); - if (newDist < GROUND_DEST_DIST_THR) - { - /* Destination reached */ - popActivePathFront(); - log(unitName + L" destination reached"); - } - } + addMeasure(L"currentTask", json::value(currentTask)); } void GroundUnit::changeSpeed(wstring change) { if (change.compare(L"stop") == 0) - { - - } + setState(State::IDLE); else if (change.compare(L"slow") == 0) - { - - } + setTargetSpeed(getTargetSpeed() - knotsToMs(5)); else if (change.compare(L"fast") == 0) - { + setTargetSpeed(getTargetSpeed() + knotsToMs(5)); - } + if (getTargetSpeed() < 0) + setTargetSpeed(0); +} + +void GroundUnit::setOnOff(bool newOnOff) +{ + Unit::setOnOff(newOnOff); + Command* command = dynamic_cast(new SetOnOff(groupName, onOff)); + scheduler->appendCommand(command); +} + +void GroundUnit::setFollowRoads(bool newFollowRoads) +{ + Unit::setFollowRoads(newFollowRoads); + resetActiveDestination(); /* Reset active destination to apply option*/ } \ No newline at end of file diff --git a/src/core/src/helicopter.cpp b/src/core/src/helicopter.cpp index 7d2d1e20..f952dc4f 100644 --- a/src/core/src/helicopter.cpp +++ b/src/core/src/helicopter.cpp @@ -17,6 +17,9 @@ Helicopter::Helicopter(json::value json, int ID) : AirUnit(json, ID) { log("New Helicopter created with ID: " + to_string(ID)); addMeasure(L"category", json::value(getCategory())); + + double targetSpeed = knotsToMs(100); + double targetAltitude = ftToM(5000); setTargetSpeed(targetSpeed); setTargetAltitude(targetAltitude); }; @@ -29,9 +32,9 @@ void Helicopter::changeSpeed(wstring change) clearActivePath(); } else if (change.compare(L"slow") == 0) - targetSpeed -= 10 / 1.94384; + targetSpeed -= knotsToMs(10); else if (change.compare(L"fast") == 0) - targetSpeed += 10 / 1.94384; + targetSpeed += knotsToMs(10); if (targetSpeed < 0) targetSpeed = 0; @@ -43,32 +46,19 @@ void Helicopter::changeAltitude(wstring change) if (change.compare(L"descend") == 0) { if (targetAltitude > 100) - targetAltitude -= 100 / 3.28084; + targetAltitude -= ftToM(100); else if (targetAltitude > 0) - targetAltitude -= 10 / 3.28084; + targetAltitude -= ftToM(10); } else if (change.compare(L"climb") == 0) { if (targetAltitude > 100) - targetAltitude += 100 / 3.28084; + targetAltitude += ftToM(100); else if (targetAltitude >= 0) - targetAltitude += 10 / 3.28084; + targetAltitude += ftToM(10); } if (targetAltitude < 0) targetAltitude = 0; goToDestination(); /* Send the command to reach the destination */ } - - -void Helicopter::setTargetSpeed(double newTargetSpeed) { - targetSpeed = newTargetSpeed; - addMeasure(L"targetSpeed", json::value(targetSpeed)); - goToDestination(); -} - -void Helicopter::setTargetAltitude(double newTargetAltitude) { - targetAltitude = newTargetAltitude; - addMeasure(L"targetAltitude", json::value(targetAltitude)); - goToDestination(); -} \ No newline at end of file diff --git a/src/core/src/navyunit.cpp b/src/core/src/navyunit.cpp index 25f835a6..9898b24d 100644 --- a/src/core/src/navyunit.cpp +++ b/src/core/src/navyunit.cpp @@ -17,8 +17,9 @@ NavyUnit::NavyUnit(json::value json, int ID) : Unit(json, ID) { log("New Navy Unit created with ID: " + to_string(ID)); addMeasure(L"category", json::value(getCategory())); + + double targetSpeed = 10; setTargetSpeed(targetSpeed); - setTargetAltitude(targetAltitude); }; void NavyUnit::AIloop() diff --git a/src/core/src/scheduler.cpp b/src/core/src/scheduler.cpp index ad46db11..d6fc9f51 100644 --- a/src/core/src/scheduler.cpp +++ b/src/core/src/scheduler.cpp @@ -59,7 +59,8 @@ void Scheduler::handleRequest(wstring key, json::value value) if (key.compare(L"setPath") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) { wstring unitName = unit->getUnitName(); @@ -75,15 +76,9 @@ void Scheduler::handleRequest(wstring key, json::value value) newPath.push_back(dest); } - Unit* unit = unitsManager->getUnit(ID); - if (unit != nullptr) - { - unit->setActivePath(newPath); - unit->setState(State::REACH_DESTINATION); - log(unitName + L" new path set successfully"); - } - else - log(unitName + L" not found, request will be discarded"); + unit->setActivePath(newPath); + unit->setState(State::REACH_DESTINATION); + log(unitName + L" new path set successfully"); } } else if (key.compare(L"smoke") == 0) @@ -111,7 +106,8 @@ void Scheduler::handleRequest(wstring key, json::value value) wstring type = value[L"type"].as_string(); double lat = value[L"location"][L"lat"].as_double(); double lng = value[L"location"][L"lng"].as_double(); - Coords loc; loc.lat = lat; loc.lng = lng; + double altitude = value[L"altitude"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; loc.alt = altitude; wstring payloadName = value[L"payloadName"].as_string(); wstring airbaseName = value[L"airbaseName"].as_string(); log(L"Spawning " + coalition + L" air unit of type " + type + L" with payload " + payloadName + L" at (" + to_wstring(lat) + L", " + to_wstring(lng) + L" " + airbaseName + L")"); @@ -122,7 +118,7 @@ void Scheduler::handleRequest(wstring key, json::value value) int ID = value[L"ID"].as_integer(); int targetID = value[L"targetID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); Unit* target = unitsManager->getUnit(targetID); wstring unitName; @@ -150,7 +146,7 @@ void Scheduler::handleRequest(wstring key, json::value value) int offsetY = value[L"offsetY"].as_integer(); int offsetZ = value[L"offsetZ"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); Unit* leader = unitsManager->getUnit(leaderID); wstring unitName; @@ -174,31 +170,45 @@ void Scheduler::handleRequest(wstring key, json::value value) else if (key.compare(L"changeSpeed") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) unit->changeSpeed(value[L"change"].as_string()); } else if (key.compare(L"changeAltitude") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) unit->changeAltitude(value[L"change"].as_string()); } else if (key.compare(L"setSpeed") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) unit->setTargetSpeed(value[L"speed"].as_double()); } + else if (key.compare(L"setSpeedType") == 0) + { + int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setTargetSpeedType(value[L"speedType"].as_string()); + } else if (key.compare(L"setAltitude") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) unit->setTargetAltitude(value[L"altitude"].as_double()); } + else if (key.compare(L"setAltitudeType") == 0) + { + int ID = value[L"ID"].as_integer(); + Unit* unit = unitsManager->getGroupLeader(ID); + if (unit != nullptr) + unit->setTargetAltitudeType(value[L"altitudeType"].as_string()); + } else if (key.compare(L"cloneUnit") == 0) { int ID = value[L"ID"].as_integer(); @@ -211,28 +221,28 @@ void Scheduler::handleRequest(wstring key, json::value value) else if (key.compare(L"setROE") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); wstring ROE = value[L"ROE"].as_string(); unit->setROE(ROE); } else if (key.compare(L"setReactionToThreat") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); wstring reactionToThreat = value[L"reactionToThreat"].as_string(); unit->setReactionToThreat(reactionToThreat); } else if (key.compare(L"setEmissionsCountermeasures") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); wstring emissionsCountermeasures = value[L"emissionsCountermeasures"].as_string(); unit->setEmissionsCountermeasures(emissionsCountermeasures); } else if (key.compare(L"landAt") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); double lat = value[L"location"][L"lat"].as_double(); double lng = value[L"location"][L"lng"].as_double(); Coords loc; loc.lat = lat; loc.lng = lng; @@ -241,18 +251,19 @@ void Scheduler::handleRequest(wstring key, json::value value) else if (key.compare(L"deleteUnit") == 0) { int ID = value[L"ID"].as_integer(); - unitsManager->deleteUnit(ID); + bool explosion = value[L"explosion"].as_bool(); + unitsManager->deleteUnit(ID, explosion); } else if (key.compare(L"refuel") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); unit->setState(State::REFUEL); } else if (key.compare(L"setAdvancedOptions") == 0) { int ID = value[L"ID"].as_integer(); - Unit* unit = unitsManager->getUnit(ID); + Unit* unit = unitsManager->getGroupLeader(ID); if (unit != nullptr) { /* Advanced tasking */ @@ -286,6 +297,69 @@ void Scheduler::handleRequest(wstring key, json::value value) unit->resetActiveDestination(); } } + else if (key.compare(L"setFollowRoads") == 0) + { + int ID = value[L"ID"].as_integer(); + bool followRoads = value[L"followRoads"].as_bool(); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setFollowRoads(followRoads); + } + else if (key.compare(L"setOnOff") == 0) + { + int ID = value[L"ID"].as_integer(); + bool onOff = value[L"onOff"].as_bool(); + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setOnOff(onOff); + } + else if (key.compare(L"explosion") == 0) + { + int intensity = value[L"intensity"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + log(L"Adding " + to_wstring(intensity) + L" explosion at (" + to_wstring(lat) + L", " + to_wstring(lng) + L")"); + Coords loc; loc.lat = lat; loc.lng = lng; + command = dynamic_cast(new Explosion(intensity, loc)); + } + else if (key.compare(L"bombPoint") == 0) + { + int ID = value[L"ID"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::BOMB_POINT); + unit->setTargetLocation(loc); + } + else if (key.compare(L"carpetBomb") == 0) + { + int ID = value[L"ID"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::CARPET_BOMB); + unit->setTargetLocation(loc); + } + else if (key.compare(L"bombBuilding") == 0) + { + int ID = value[L"ID"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::BOMB_BUILDING); + unit->setTargetLocation(loc); + } + else if (key.compare(L"fireAtArea") == 0) + { + int ID = value[L"ID"].as_integer(); + double lat = value[L"location"][L"lat"].as_double(); + double lng = value[L"location"][L"lng"].as_double(); + Coords loc; loc.lat = lat; loc.lng = lng; + Unit* unit = unitsManager->getGroupLeader(ID); + unit->setState(State::FIRE_AT_AREA); + unit->setTargetLocation(loc); + } else { log(L"Unknown command: " + key); diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp index b32dc785..b0a86a73 100644 --- a/src/core/src/unit.cpp +++ b/src/core/src/unit.cpp @@ -116,8 +116,21 @@ void Unit::updateExportData(json::value json) setAI(getUnitName().find(L"Olympus") != wstring::npos); /* If the unit is alive and it is not a human, run the AI Loop that performs the requested commands and instructions (moving, attacking, etc) */ - if (getAI() && getAlive() && getFlags()[L"Human"].as_bool() == false) + // TODO at the moment groups will stop moving correctly if the leader dies + const bool isUnitControlledByOlympus = getAI(); + const bool isUnitAlive = getAlive(); + const bool isUnitLeader = unitsManager->isUnitGroupLeader(this); + const bool isUnitLeaderOfAGroupWithOtherUnits = unitsManager->isUnitInGroup(this) && unitsManager->isUnitGroupLeader(this); + const bool isUnitHuman = getFlags()[L"Human"].as_bool(); + + // Keep running the AI loop even if the unit is dead if it is the leader of a group which has other members in it + if (isUnitControlledByOlympus && (isUnitAlive || isUnitLeaderOfAGroupWithOtherUnits) && isUnitLeader && !isUnitHuman) + { + if (checkTaskFailed() && state != State::IDLE && State::LAND) + setState(State::IDLE); + AIloop(); + } } void Unit::updateMissionData(json::value json) @@ -132,10 +145,14 @@ void Unit::updateMissionData(json::value json) setHasTask(json[L"hasTask"].as_bool()); } -json::value Unit::getData(long long time) +json::value Unit::getData(long long time, bool sendAll) { auto json = json::value::object(); + /* If the unit is in a group, task & option data is given by the group leader */ + if (unitsManager->isUnitInGroup(this) && !unitsManager->isUnitGroupLeader(this)) + json = unitsManager->getGroupLeader(this)->getData(time, true); + /********** Base data **********/ json[L"baseData"] = json::value::object(); for (auto key : { L"AI", L"name", L"unitName", L"groupName", L"alive", L"category"}) @@ -146,7 +163,7 @@ json::value Unit::getData(long long time) if (json[L"baseData"].size() == 0) json.erase(L"baseData"); - if (alive) { + if (alive || sendAll) { /********** Flight data **********/ json[L"flightData"] = json::value::object(); for (auto key : { L"latitude", L"longitude", L"altitude", L"speed", L"heading" }) @@ -177,25 +194,28 @@ json::value Unit::getData(long long time) if (json[L"formationData"].size() == 0) json.erase(L"formationData"); - /********** Task data **********/ - json[L"taskData"] = json::value::object(); - for (auto key : { L"currentState", L"currentTask", L"targetSpeed", L"targetAltitude", L"activePath", L"isTanker", L"isAWACS" }) - { - if (measures.find(key) != measures.end() && measures[key]->getTime() > time) - json[L"taskData"][key] = measures[key]->getValue(); - } - if (json[L"taskData"].size() == 0) - json.erase(L"taskData"); + /* If the unit is in a group, task & option data is given by the group leader */ + if (unitsManager->isUnitGroupLeader(this)) { + /********** Task data **********/ + json[L"taskData"] = json::value::object(); + for (auto key : { L"currentState", L"currentTask", L"targetSpeed", L"targetAltitude", L"targetSpeedType", L"targetAltitudeType", L"activePath", L"isTanker", L"isAWACS", L"onOff", L"followRoads", L"targetID", L"targetLocation" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"taskData"][key] = measures[key]->getValue(); + } + if (json[L"taskData"].size() == 0) + json.erase(L"taskData"); - /********** Options data **********/ - json[L"optionsData"] = json::value::object(); - for (auto key : { L"ROE", L"reactionToThreat", L"emissionsCountermeasures", L"TACAN", L"radio", L"generalSettings"}) - { - if (measures.find(key) != measures.end() && measures[key]->getTime() > time) - json[L"optionsData"][key] = measures[key]->getValue(); + /********** Options data **********/ + json[L"optionsData"] = json::value::object(); + for (auto key : { L"ROE", L"reactionToThreat", L"emissionsCountermeasures", L"TACAN", L"radio", L"generalSettings" }) + { + if (measures.find(key) != measures.end() && measures[key]->getTime() > time) + json[L"optionsData"][key] = measures[key]->getValue(); + } + if (json[L"optionsData"].size() == 0) + json.erase(L"optionsData"); } - if (json[L"optionsData"].size() == 0) - json.erase(L"optionsData"); } return json; @@ -322,8 +342,10 @@ void Unit::resetActiveDestination() void Unit::resetTask() { - Command* command = dynamic_cast(new ResetTask(ID)); + Command* command = dynamic_cast(new ResetTask(groupName)); scheduler->appendCommand(command); + setHasTask(false); + resetTaskFailedCounter(); } void Unit::setFormationOffset(Offset newFormationOffset) @@ -352,7 +374,7 @@ void Unit::setROE(wstring newROE) { else return; - Command* command = dynamic_cast(new SetOption(ID, SetCommandType::ROE, ROEEnum)); + Command* command = dynamic_cast(new SetOption(groupName, SetCommandType::ROE, ROEEnum)); scheduler->appendCommand(command); } } @@ -377,7 +399,7 @@ void Unit::setReactionToThreat(wstring newReactionToThreat) { else return; - Command* command = dynamic_cast(new SetOption(ID, SetCommandType::REACTION_ON_THREAT, reactionToThreatEnum)); + Command* command = dynamic_cast(new SetOption(groupName, SetCommandType::REACTION_ON_THREAT, reactionToThreatEnum)); scheduler->appendCommand(command); } } @@ -420,13 +442,13 @@ void Unit::setEmissionsCountermeasures(wstring newEmissionsCountermeasures) { Command* command; - command = dynamic_cast(new SetOption(ID, SetCommandType::RADAR_USING, radarEnum)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::RADAR_USING, radarEnum)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::FLARE_USING, flareEnum)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::FLARE_USING, flareEnum)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::ECM_USING, ECMEnum)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::ECM_USING, ECMEnum)); scheduler->appendCommand(command); } } @@ -473,7 +495,7 @@ void Unit::setTACAN(Options::TACAN newTACAN) { << "frequency = " << TACANChannelToFrequency(TACAN.channel, TACAN.XY) << "," << "}" << "}"; - Command* command = dynamic_cast(new SetCommand(ID, commandSS.str())); + Command* command = dynamic_cast(new SetCommand(groupName, commandSS.str())); scheduler->appendCommand(command); } else { @@ -483,7 +505,7 @@ void Unit::setTACAN(Options::TACAN newTACAN) { << "params = {" << "}" << "}"; - Command* command = dynamic_cast(new SetCommand(ID, commandSS.str())); + Command* command = dynamic_cast(new SetCommand(groupName, commandSS.str())); scheduler->appendCommand(command); } } @@ -511,7 +533,7 @@ void Unit::setRadio(Options::Radio newRadio) { << "frequency = " << radio.frequency << "," << "}" << "}"; - command = dynamic_cast(new SetCommand(ID, commandSS.str())); + command = dynamic_cast(new SetCommand(groupName, commandSS.str())); scheduler->appendCommand(command); // Clear the stringstream @@ -524,7 +546,7 @@ void Unit::setRadio(Options::Radio newRadio) { << "number = " << radio.callsignNumber << "," << "}" << "}"; - command = dynamic_cast(new SetCommand(ID, commandSS.str())); + command = dynamic_cast(new SetCommand(groupName, commandSS.str())); scheduler->appendCommand(command); } } @@ -563,16 +585,131 @@ void Unit::setGeneralSettings(Options::GeneralSettings newGeneralSettings) { generalSettings = newGeneralSettings; Command* command; - command = dynamic_cast(new SetOption(ID, SetCommandType::PROHIBIT_AA, generalSettings.prohibitAA)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AA, generalSettings.prohibitAA)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::PROHIBIT_AG, generalSettings.prohibitAG)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AG, generalSettings.prohibitAG)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::PROHIBIT_JETT, generalSettings.prohibitJettison)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_JETT, generalSettings.prohibitJettison)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::PROHIBIT_AB, generalSettings.prohibitAfterburner)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::PROHIBIT_AB, generalSettings.prohibitAfterburner)); scheduler->appendCommand(command); - command = dynamic_cast(new SetOption(ID, SetCommandType::ENGAGE_AIR_WEAPONS, !generalSettings.prohibitAirWpn)); + command = dynamic_cast(new SetOption(groupName, SetCommandType::ENGAGE_AIR_WEAPONS, !generalSettings.prohibitAirWpn)); scheduler->appendCommand(command); } } +void Unit::setTargetSpeed(double newTargetSpeed) { + targetSpeed = newTargetSpeed; + addMeasure(L"targetSpeed", json::value(newTargetSpeed)); + goToDestination(); +} + +void Unit::setTargetAltitude(double newTargetAltitude) { + targetAltitude = newTargetAltitude; + addMeasure(L"targetAltitude", json::value(newTargetAltitude)); + goToDestination(); +} + +void Unit::setTargetSpeedType(wstring newTargetSpeedType) { + targetSpeedType = newTargetSpeedType; + addMeasure(L"targetSpeedType", json::value(newTargetSpeedType)); + goToDestination(); +} + +void Unit::setTargetAltitudeType(wstring newTargetAltitudeType) { + targetAltitudeType = newTargetAltitudeType; + addMeasure(L"targetAltitudeType", json::value(newTargetAltitudeType)); + goToDestination(); +} + +void Unit::goToDestination(wstring enrouteTask) +{ + if (activeDestination != NULL) + { + Command* command = dynamic_cast(new Move(groupName, activeDestination, getTargetSpeed(), getTargetSpeedType(), getTargetAltitude(), getTargetAltitudeType(), enrouteTask, getCategory())); + scheduler->appendCommand(command); + setHasTask(true); + } +} + +bool Unit::isDestinationReached(double threshold) +{ + if (activeDestination != NULL) + { + /* Check if any unit in the group has reached the point */ + for (auto const& p: unitsManager->getGroupMembers(groupName)) + { + double dist = 0; + Geodesic::WGS84().Inverse(p->getLatitude(), p->getLongitude(), activeDestination.lat, activeDestination.lng, dist); + if (dist < threshold) + { + log(unitName + L" destination reached"); + return true; + } + else { + return false; + } + } + } + else + return true; +} + +bool Unit::setActiveDestination() +{ + if (activePath.size() > 0) + { + activeDestination = activePath.front(); + log(unitName + L" active destination set to queue front"); + return true; + } + else + { + activeDestination = Coords(0); + log(unitName + L" active destination set to NULL"); + return false; + } +} + +bool Unit::updateActivePath(bool looping) +{ + if (activePath.size() > 0) + { + /* Push the next destination in the queue to the front */ + if (looping) + pushActivePathBack(activePath.front()); + popActivePathFront(); + log(unitName + L" active path front popped"); + return true; + } + else { + return false; + } +} + +void Unit::setTargetLocation(Coords newTargetLocation) { + targetLocation = newTargetLocation; + auto json = json::value(); + json[L"latitude"] = json::value(newTargetLocation.lat); + json[L"longitude"] = json::value(newTargetLocation.lng); + addMeasure(L"targetLocation", json::value(json)); +} + +bool Unit::checkTaskFailed() { + if (getHasTask()) + return false; + else { + if (taskCheckCounter > 0) + taskCheckCounter--; + return taskCheckCounter == 0; + } +} + +void Unit::resetTaskFailedCounter() { + taskCheckCounter = TASK_CHECK_INIT_VALUE; +} + +void Unit::setHasTask(bool newHasTask) { + hasTask = newHasTask; + addMeasure(L"hasTask", json::value(newHasTask)); +} \ No newline at end of file diff --git a/src/core/src/unitsmanager.cpp b/src/core/src/unitsmanager.cpp index 00b1b8cd..b5ec63be 100644 --- a/src/core/src/unitsmanager.cpp +++ b/src/core/src/unitsmanager.cpp @@ -32,6 +32,67 @@ Unit* UnitsManager::getUnit(int ID) } } +bool UnitsManager::isUnitInGroup(Unit* unit) +{ + if (unit != nullptr) { + wstring groupName = unit->getGroupName(); + for (auto const& p : units) + { + if (p.second->getGroupName().compare(groupName) == 0 && p.second != unit) + return true; + } + } + return false; +} + +bool UnitsManager::isUnitGroupLeader(Unit* unit) +{ + if (unit != nullptr) + return unit == getGroupLeader(unit); + else + return false; +} + +// The group leader is the unit with the lowest ID that is part of the group. This is different from DCS's concept of leader, which will change if the leader is destroyed +Unit* UnitsManager::getGroupLeader(Unit* unit) +{ + if (unit != nullptr) { + wstring groupName = unit->getGroupName(); + + /* Get the unit IDs in order */ + std::vector keys; + for (auto const& p : units) + keys.push_back(p.first); + sort(keys.begin(), keys.end()); + + /* Find the first unit that has the same groupName */ + for (auto const& tempID : keys) + { + Unit* tempUnit = getUnit(tempID); + if (tempUnit != nullptr && tempUnit->getGroupName().compare(groupName) == 0) + return tempUnit; + } + } + return nullptr; +} + +vector UnitsManager::getGroupMembers(wstring groupName) +{ + vector members; + for (auto const& p : units) + { + if (p.second->getGroupName().compare(groupName) == 0) + members.push_back(p.second); + } + return members; +} + +Unit* UnitsManager::getGroupLeader(int ID) +{ + Unit* unit = getUnit(ID); + return getGroupLeader(unit); +} + void UnitsManager::updateExportData(lua_State* L) { map unitJSONs = getAllUnits(L); @@ -107,11 +168,11 @@ void UnitsManager::getData(json::value& answer, long long time) answer[L"units"] = unitsJson; } -void UnitsManager::deleteUnit(int ID) +void UnitsManager::deleteUnit(int ID, bool explosion) { if (getUnit(ID) != nullptr) { - Command* command = dynamic_cast(new Delete(ID)); + Command* command = dynamic_cast(new Delete(ID, explosion)); scheduler->appendCommand(command); } } diff --git a/src/utils/include/utils.h b/src/utils/include/utils.h index cd8bd7be..4534a8de 100644 --- a/src/utils/include/utils.h +++ b/src/utils/include/utils.h @@ -29,3 +29,8 @@ bool DllExport operator!= (const Offset& a, const Offset& b); bool DllExport operator== (const Offset& a, const int& b); bool DllExport operator!= (const Offset& a, const int& b); +double DllExport knotsToMs(const double knots); +double DllExport msToKnots(const double ms); +double DllExport ftToM(const double ft); +double DllExport mToFt(const double m); + diff --git a/src/utils/src/utils.cpp b/src/utils/src/utils.cpp index afd8ebf2..9d91fbe7 100644 --- a/src/utils/src/utils.cpp +++ b/src/utils/src/utils.cpp @@ -63,3 +63,20 @@ bool operator== (const Offset& a, const Offset& b) { return a.x == b.x && a.y == bool operator!= (const Offset& a, const Offset& b) { return !(a == b); } bool operator== (const Offset& a, const int& b) { return a.x == b && a.y == b && a.z == b; } bool operator!= (const Offset& a, const int& b) { return !(a == b); } + + +double knotsToMs(const double knots) { + return knots / 1.94384; +} + +double msToKnots(const double ms) { + return ms * 1.94384; +} + +double ftToM(const double ft) { + return ft * 0.3048; +} + +double mToFt(const double m) { + return m / 0.3048; +} \ No newline at end of file