Merge pull request #302 from Pax1601/Advanced-spawn-and-control-options

Advanced spawn and control options
This commit is contained in:
Pax1601 2023-06-12 10:00:29 +02:00 committed by GitHub
commit 27b56deb45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2879 additions and 1342 deletions

View File

@ -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

View File

@ -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",
},

View File

@ -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",

View File

@ -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",

View File

@ -130,7 +130,7 @@
padding: 10px;
position: absolute;
width: fit-content;
z-index: 1000;
z-index: 9999;
}
.aic-enabled #aic-teleprompt {

View File

@ -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 {

View File

@ -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,

View File

@ -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));
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 32 31.999584"
version="1.1"
id="svg4"
sodipodi:docname="explosion.svg"
width="32"
height="31.999584"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="13.078125"
inkscape:cx="19.53644"
inkscape:cy="12.157706"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="m 23.013304,7.2180185 c 0.209962,-0.3739974 0.64242,-0.5068189 0.993401,-0.2971008 0.350981,0.2097182 0.495133,0.6815841 0.332178,1.0835439 L 20.042501,18.658144 c 0.06894,0.08039 0.134753,0.164279 0.197427,0.248166 l 3.046013,-1.911931 c 0.329044,-0.206222 0.739567,-0.108354 0.968331,0.2237 0.228764,0.332054 0.197427,0.803919 -0.06894,1.10102 l -2.726369,3.040913 h -2.240637 c -0.413656,-1.303747 -1.52614,-2.236993 -2.836051,-2.236993 -1.309911,0 -2.425529,0.933246 -2.83605,2.236993 H 11.048617 L 8.6826296,19.535465 C 8.3786552,19.30128 8.2721074,18.850386 8.4350628,18.476388 8.5980181,18.102391 8.9803366,17.920635 9.3344512,18.042971 l 3.0460128,1.062571 c 0.09401,-0.136317 0.191159,-0.269138 0.294573,-0.394969 L 10.722706,15.08245 c -0.191159,-0.353027 -0.122217,-0.807416 0.159822,-1.073059 0.282038,-0.265643 0.695694,-0.262148 0.974598,0.0034 l 3.208969,3.072372 c 0.047,-0.01398 0.09401,-0.02796 0.141019,-0.03845 l 0.426191,-4.987745 c 0.0376,-0.429921 0.360382,-0.75848 0.748968,-0.75848 0.388586,0 0.711362,0.328559 0.748967,0.75848 l 0.423058,4.959835 z M 9.362655,22.482004 v 0 H 23.40189 v 0 h 1.002802 c 0.554675,0 1.002803,0.499829 1.002803,1.118498 0,0.618669 -0.448128,1.118496 -1.002803,1.118496 H 8.3598525 c -0.554675,0 -1.0028024,-0.499827 -1.0028024,-1.118496 0,-0.618669 0.4481274,-1.118498 1.0028024,-1.118498 z M 16.382273,6.8230493 c 0.416789,0 0.752101,0.3739974 0.752101,0.8388726 v 1.6777453 c 0,0.4648752 -0.335312,0.8388728 -0.752101,0.8388728 -0.41679,0 -0.752102,-0.3739976 -0.752102,-0.8388728 V 7.6619219 c 0,-0.4648752 0.335312,-0.8388726 0.752102,-0.8388726 z"
fill="#ffffff"
stroke="#ffffff"
id="path2"
style="fill:#ffffff;fill-opacity:1;stroke-width:0.0330959" />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M459.1 52.4L442.6 6.5C440.7 2.6 436.5 0 432.1 0s-8.5 2.6-10.4 6.5L405.2 52.4l-46 16.8c-4.3 1.6-7.3 5.9-7.2 10.4c0 4.5 3 8.7 7.2 10.2l45.7 16.8 16.8 45.8c1.5 4.4 5.8 7.5 10.4 7.5s8.9-3.1 10.4-7.5l16.5-45.8 45.7-16.8c4.2-1.5 7.2-5.7 7.2-10.2c0-4.6-3-8.9-7.2-10.4L459.1 52.4zm-132.4 53c-12.5-12.5-32.8-12.5-45.3 0l-2.9 2.9C256.5 100.3 232.7 96 208 96C93.1 96 0 189.1 0 304S93.1 512 208 512s208-93.1 208-208c0-24.7-4.3-48.5-12.2-70.5l2.9-2.9c12.5-12.5 12.5-32.8 0-45.3l-80-80zM200 192c-57.4 0-104 46.6-104 104v8c0 8.8-7.2 16-16 16s-16-7.2-16-16v-8c0-75.1 60.9-136 136-136h8c8.8 0 16 7.2 16 16s-7.2 16-16 16h-8z"/></svg>

After

Width:  |  Height:  |  Size: 854 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M499.6 11.3c6.7-10.7 20.5-14.5 31.7-8.5s15.8 19.5 10.6 31L404.8 338.6c2.2 2.3 4.3 4.7 6.3 7.1l97.2-54.7c10.5-5.9 23.6-3.1 30.9 6.4s6.3 23-2.2 31.5l-87 87H378.5c-13.2-37.3-48.7-64-90.5-64s-77.4 26.7-90.5 64H117.8L42.3 363.7c-9.7-6.7-13.1-19.6-7.9-30.3s17.4-15.9 28.7-12.4l97.2 30.4c3-3.9 6.1-7.7 9.4-11.3L107.4 236.3c-6.1-10.1-3.9-23.1 5.1-30.7s22.2-7.5 31.1 .1L246 293.6c1.5-.4 3-.8 4.5-1.1l13.6-142.7c1.2-12.3 11.5-21.7 23.9-21.7s22.7 9.4 23.9 21.7l13.5 141.9L499.6 11.3zM64 448v0H512v0h32c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H64zM288 0c13.3 0 24 10.7 24 24V72c0 13.3-10.7 24-24 24s-24-10.7-24-24V24c0-13.3 10.7-24 24-24z"/></svg>

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="35"
height="35"
viewBox="0 0 35 35"
fill="none"
version="1.1"
id="svg4"
sodipodi:docname="pin.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="23.914286"
inkscape:cx="19.967145"
inkscape:cy="20.573477"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<path
d="m 26.00224,11.876792 c 0,4.375 -3.5547,7.875001 -7.875,7.875001 -4.375,0 -7.875,-3.500001 -7.875,-7.875001 0,-4.32031 3.5,-7.875 7.875,-7.875 4.3203,0 7.875,3.55469 7.875,7.875 z m -9.625,18.375001 v -8.8594 c 0.5469,0.0547 1.1484,0.1094 1.75,0.1094 0.5469,0 1.1484,-0.0547 1.75,-0.1094 v 8.8594 c 0,0.9844 -0.8203,1.75 -1.75,1.75 -0.9844,0 -1.75,-0.7656 -1.75,-1.75 z"
fill="#247BE2"
id="path2-3"
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1;stroke-width:4;stroke-dasharray:none"
sodipodi:nodetypes="sssssscscsss" />
<path
d="M26 11.875C26 16.25 22.4453 19.75 18.125 19.75C13.75 19.75 10.25 16.25 10.25 11.875C10.25 7.55469 13.75 4 18.125 4C22.4453 4 26 7.55469 26 11.875ZM18.125 8.375C18.5625 8.375 19 7.99219 19 7.5C19 7.0625 18.5625 6.625 18.125 6.625C15.2266 6.625 12.875 8.97656 12.875 11.875C12.875 12.3672 13.2578 12.75 13.75 12.75C14.1875 12.75 14.625 12.3672 14.625 11.875C14.625 9.96094 16.1562 8.375 18.125 8.375ZM16.375 30.25V21.3906C16.9219 21.4453 17.5234 21.5 18.125 21.5C18.6719 21.5 19.2734 21.4453 19.875 21.3906V30.25C19.875 31.2344 19.0547 32 18.125 32C17.1406 32 16.375 31.2344 16.375 30.25Z"
fill="#247BE2"
id="path2" />
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M27.418 14C29.2109 14 32.375 15.5293 32.375 17.375C32.375 19.2734 29.2109 20.75 27.418 20.75H21.248L15.9746 30.0312C15.6582 30.5586 15.0781 30.875 14.498 30.875H11.5449C10.9648 30.875 10.543 30.3477 10.7012 29.8203L13.2852 20.75H7.90625L5.58594 23.8086C5.42773 24.0195 5.2168 24.125 4.95312 24.125H2.73828C2.31641 24.125 2 23.8086 2 23.3867C2 23.334 2 23.2812 2 23.2285L3.6875 17.375L2 11.5742C2 11.5215 2 11.4688 2 11.3633C2 10.9941 2.31641 10.625 2.73828 10.625H4.95312C5.2168 10.625 5.42773 10.7832 5.58594 10.9941L7.90625 14H13.2852L10.7012 4.98242C10.543 4.45508 10.9648 3.875 11.5449 3.875H14.498C15.0781 3.875 15.6582 4.24414 15.9746 4.77148L21.248 14H27.418Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M170.5 51.6L151.5 80h145l-19-28.4c-1.5-2.2-4-3.6-6.7-3.6H177.1c-2.7 0-5.2 1.3-6.7 3.6zm147-26.6L354.2 80H368h48 8c13.3 0 24 10.7 24 24s-10.7 24-24 24h-8V432c0 44.2-35.8 80-80 80H112c-44.2 0-80-35.8-80-80V128H24c-13.3 0-24-10.7-24-24S10.7 80 24 80h8H80 93.8l36.7-55.1C140.9 9.4 158.4 0 177.1 0h93.7c18.7 0 36.2 9.4 46.6 24.9zM80 128V432c0 17.7 14.3 32 32 32H336c17.7 0 32-14.3 32-32V128H80zm80 64V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16zm80 0V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16zm80 0V400c0 8.8-7.2 16-16 16s-16-7.2-16-16V192c0-8.8 7.2-16 16-16s16 7.2 16 16z"/></svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52"
height="52"
viewBox="0 0 13.758333 13.758333"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="move.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
width="52mm"
units="px"
inkscape:zoom="8.3856039"
inkscape:cx="45.73314"
inkscape:cy="10.673054"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="star"
style="mix-blend-mode:screen;fill:#247be2;fill-opacity:1;stroke:#ffffff;stroke-width:1.43972;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path1252"
inkscape:flatsided="false"
sodipodi:sides="3"
sodipodi:cx="21.080975"
sodipodi:cy="4.8064618"
sodipodi:r1="2.6154003"
sodipodi:r2="1.3077002"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 21.080975,7.4218621 -1.132502,-1.9615502 -1.132502,-1.9615502 2.265004,0 2.265003,-1e-7 -1.132502,1.9615503 z"
transform="matrix(0.38984496,0.38984496,-0.38984496,0.38984496,2.4850522,-5.1648778)"
inkscape:transform-center-y="-0.18659976"
inkscape:transform-center-x="-0.18659993" />
<path
sodipodi:type="star"
style="mix-blend-mode:screen;fill:#247be2;fill-opacity:1;stroke:#ffffff;stroke-width:1.43972;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path1252-3"
inkscape:flatsided="false"
sodipodi:sides="3"
sodipodi:cx="21.080975"
sodipodi:cy="4.8064618"
sodipodi:r1="2.6154003"
sodipodi:r2="1.3077002"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:rounded="0"
inkscape:randomized="0"
transform="matrix(-0.38984496,-0.38984496,0.38984496,-0.38984496,11.276723,18.916693)"
inkscape:transform-center-y="0.18660057"
d="m 21.080975,7.4218621 -1.132502,-1.9615502 -1.132502,-1.9615502 2.265004,0 2.265003,-1e-7 -1.132502,1.9615503 z"
inkscape:transform-center-x="0.18660032" />
<path
sodipodi:type="star"
style="mix-blend-mode:screen;fill:#247be2;fill-opacity:1;stroke:#ffffff;stroke-width:1.43972;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path1252-9"
inkscape:flatsided="false"
sodipodi:sides="3"
sodipodi:cx="21.080975"
sodipodi:cy="4.8064618"
sodipodi:r1="2.6154003"
sodipodi:r2="1.3077002"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:rounded="0"
inkscape:randomized="0"
transform="matrix(-0.38984496,0.38984496,-0.38984496,-0.38984496,18.917375,2.4977722)"
d="m 21.080975,7.4218621 -1.132502,-1.9615502 -1.132502,-1.9615502 2.265004,0 2.265003,-1e-7 -1.132502,1.9615503 z"
inkscape:transform-center-x="-0.1865998"
inkscape:transform-center-y="0.1866003" />
<path
sodipodi:type="star"
style="mix-blend-mode:normal;fill:#247be2;fill-opacity:1;stroke:#ffffff;stroke-width:1.43972;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path1252-3-0"
inkscape:flatsided="false"
sodipodi:sides="3"
sodipodi:cx="21.080975"
sodipodi:cy="4.8064618"
sodipodi:r1="2.6154003"
sodipodi:r2="1.3077002"
sodipodi:arg1="1.5707963"
sodipodi:arg2="2.6179939"
inkscape:rounded="0"
inkscape:randomized="0"
transform="matrix(0.38984496,-0.38984496,0.38984496,0.38984496,-5.187759,11.287231)"
d="m 21.080975,7.4218621 -1.132502,-1.9615502 -1.132502,-1.9615502 2.265004,0 2.265003,-1e-7 -1.132502,1.9615503 z"
inkscape:transform-center-x="0.18659972"
inkscape:transform-center-y="-0.18659965" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52"
height="52"
viewBox="0 0 13.758333 13.758333"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="target.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
width="52mm"
units="px"
inkscape:zoom="16.771208"
inkscape:cx="31.989348"
inkscape:cy="27.63665"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
id="linearGradient4717"
inkscape:swatch="solid">
<stop
style="stop-color:#0cffff;stop-opacity:1;"
offset="0"
id="stop4715" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:1.03188;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path5477"
cx="6.8599777"
cy="6.8209338"
rx="1.9410306"
ry="1.948356" />
<rect
style="fill:#ff5858;fill-opacity:1;stroke:#ffffff;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="rect3980"
width="0.54935014"
height="2.6236348"
x="6.5846372"
y="3.3855996"
rx="0.29515001" />
<rect
style="fill:#ff5858;fill-opacity:1;stroke:#ffffff;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="rect3980-4"
width="0.54935014"
height="2.6236348"
x="6.5858645"
y="7.6347723"
rx="0.29515001" />
<rect
style="fill:#ff5858;fill-opacity:1;stroke:#ffffff;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="rect3980-47"
width="0.54935014"
height="2.6236348"
x="6.5517845"
y="-10.252322"
rx="0.29515001"
transform="rotate(90)" />
<rect
style="fill:#ff5858;fill-opacity:1;stroke:#ffffff;stroke-width:0.79375;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="rect3980-4-3"
width="0.54935014"
height="2.6236348"
x="6.5530119"
y="-6.003149"
rx="0.29515001"
transform="rotate(90)" />
<ellipse
style="fill:none;fill-opacity:1;stroke:#ff5858;stroke-width:0.449792;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="path5477-2"
cx="6.8613305"
cy="6.8167095"
rx="1.9410306"
ry="1.948356" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 19 15"
version="1.1"
id="svg4"
sodipodi:docname="no-task.svg"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
width="19"
height="15"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8" />
<sodipodi:namedview
id="namedview6"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="18.384776"
inkscape:cx="5.575265"
inkscape:cy="6.9350858"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg4" />
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
d="m 9.4628581,2.8509123 c 0.2704795,0 0.5200078,0.14155 0.6571519,0.3736899 l 4.114361,6.9453658 c 0.13905,0.234028 0.13905,0.522789 0.004,0.756817 -0.135238,0.234029 -0.388579,0.379351 -0.660964,0.379351 H 5.3487067 c -0.2723857,0 -0.5257221,-0.145322 -0.6609641,-0.379351 -0.1352419,-0.234028 -0.1333356,-0.524677 0.00402,-0.756817 L 8.8061218,3.2246022 C 8.9432655,2.9924623 9.1927939,2.8509123 9.4632778,2.8509123 Z m 0,2.4157789 c -0.2533409,0 -0.4571502,0.2019433 -0.4571502,0.4529583 v 2.1138038 c 0,0.251015 0.2038093,0.4529582 0.4571502,0.4529582 0.2533365,0 0.4571503,-0.2019432 0.4571503,-0.4529582 V 5.7196495 c 0,-0.251015 -0.2038138,-0.4529583 -0.4571503,-0.4529583 z m 0.6095349,4.2276119 a 0.60953517,0.60394517 0 1 0 -1.21907,0 0.60953517,0.60394517 0 1 0 1.21907,0 z"
id="path2"
style="fill:#ff5858;fill-opacity:1;stroke:#262626;stroke-width:2.24801;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.14086;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:5.6;stroke-opacity:1;paint-order:stroke fill markers"
id="rect1282"
width="2.0395968"
height="5.7258606"
x="8.4665051"
y="4.7092009"
rx="0.021248572" />
<path
d="m 9.4628581,2.8509123 c 0.2704795,0 0.5200078,0.14155 0.6571519,0.3736899 l 4.114361,6.9453658 c 0.13905,0.234028 0.13905,0.522789 0.004,0.756817 -0.135238,0.234029 -0.388579,0.379351 -0.660964,0.379351 H 5.3487067 c -0.2723857,0 -0.5257221,-0.145322 -0.6609641,-0.379351 -0.1352419,-0.234028 -0.1333356,-0.524677 0.00402,-0.756817 L 8.8061218,3.2246022 C 8.9432655,2.9924623 9.1927939,2.8509123 9.4632778,2.8509123 Z m 0,2.4157789 c -0.2533409,0 -0.4571502,0.2019433 -0.4571502,0.4529583 v 2.1138038 c 0,0.251015 0.2038093,0.4529582 0.4571502,0.4529582 0.2533365,0 0.4571503,-0.2019432 0.4571503,-0.4529582 V 5.7196495 c 0,-0.251015 -0.2038138,-0.4529583 -0.4571503,-0.4529583 z m 0.6095349,4.2276119 a 0.60953517,0.60394517 0 1 0 -1.21907,0 0.60953517,0.60394517 0 1 0 1.21907,0 z"
id="path2-9"
style="fill:#ff5858;fill-opacity:1;stroke:none;stroke-width:1.14086;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1,4 +1,4 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.7733 41.3423L25.9481 7.63951C25.5228 6.91648 24.4772 6.91646 24.0519 7.63951L4.22671 41.3423C3.79536 42.0756 4.32409 43 5.17484 43H44.8252C45.6759 43 46.2046 42.0756 45.7733 41.3423Z" fill="#3BB9FF" stroke="white" stroke-width="2"/>
<path d="M45.7733 41.3423L25.9481 7.63951C25.5228 6.91648 24.4772 6.91646 24.0519 7.63951L4.22671 41.3423C3.79536 42.0756 4.32409 43 5.17484 43H44.8252C45.6759 43 46.2046 42.0756 45.7733 41.3423Z" fill="#3BB9FF" stroke="none" stroke-width="2"/>
<path d="M6.74842 41L25 9.97231L43.2516 41H6.74842Z" fill="none" stroke="#082E44" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 451 B

After

Width:  |  Height:  |  Size: 450 B

View File

@ -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;

View File

@ -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;
}

View File

@ -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 ) {

View File

@ -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 &copy; Esri &mdash; 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 <a href="https://usgs.gov/">U.S. Geological Survey</a>'
},
"OpenStreetMap Mapnik": {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
"OPENVKarte": {
urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 18,
attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 &copy; Esri &mdash; Copyright: &copy;2012 DeLorme',
},
"CyclOSM": {
urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 20,
attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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 *********/

View File

@ -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 = <HTMLInputElement>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 = <HTMLElement>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());
}
}
}
}

View File

@ -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());
}
}
}

View File

@ -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((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value) * 0.3048;
var upDown = parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value) * 0.3048;
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
// X: front-rear, positive front
// Y: top-bottom, positive top

View File

@ -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() {

View File

@ -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");

View File

@ -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 &copy; Esri &mdash; 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 <a href="https://usgs.gov/">U.S. Geological Survey</a>'
});
}
else if (layerName == "OpenStreetMap Mapnik") {
this.#layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
}
else if (layerName == "OPENVKarte") {
this.#layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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 &copy; Esri &mdash; Copyright: &copy;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: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> 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");
}
}

View File

@ -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);
}
}

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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<Unit[]>) => this.#onUnitsSelection(e.detail));
document.addEventListener('clearSelection', () => this.#onClearSelection());
document.addEventListener('unitsSelection', (e: CustomEvent<Unit[]>) => 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 = <HTMLElement>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 = <HTMLElement>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 = <HTMLElement>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 = <HTMLElement>document.getElementById( "ref-mouse-position-latitude" );
const mouseLat = <HTMLElement>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 = <HTMLElement>document.getElementById( "ref-mouse-position-longitude" );
const mouseLng = <HTMLElement>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(<LatLng>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];
}
}

View File

@ -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 = <HTMLElement> 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<Unit[]>) => { 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)

View File

@ -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 = <HTMLElement>(this.getElement().querySelector("#altitude"));
this.#currentTask = <HTMLElement>(this.getElement().querySelector("#current-task"));
this.#groundSpeed = <HTMLElement>(this.getElement().querySelector("#ground-speed"));
this.#fuelBar = <HTMLElement>(this.getElement().querySelector("#fuel-bar"));
this.#fuelPercentage = <HTMLElement>(this.getElement().querySelector("#fuel-percentage"));
this.#groupName = <HTMLElement>(this.getElement().querySelector("#group-name"));
this.#heading = <HTMLElement>(this.getElement().querySelector("#heading"));
this.#name = <HTMLElement>(this.getElement().querySelector("#name"));
this.#latitude = <HTMLElement>(this.getElement().querySelector("#latitude"));
this.#loadoutContainer = <HTMLElement>(this.getElement().querySelector("#loadout-container"));
this.#longitude = <HTMLElement>(this.getElement().querySelector("#longitude"));
this.#silhouette = <HTMLElement>(this.getElement().querySelector("#loadout-silhouette"));
this.#unitControl = <HTMLElement>(this.getElement().querySelector("#unit-control"));
this.#unitLabel = <HTMLElement>(this.getElement().querySelector("#unit-label"));
this.#unitName = <HTMLElement>(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<Unit[]>) => this.#onUnitsSelection(e.detail));
document.addEventListener("unitsDeselection", (e: CustomEvent<Unit[]>) => 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 = <HTMLElement>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();
}

View File

@ -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 };

View File

@ -2603,7 +2603,7 @@ export class AircraftDatabase extends UnitDatabase {
}
],
"roles": [
"Recon"
"Reconnaissance"
],
"code": "R-60M*2",
"name": "Heavy / Fox 2 / Long Range"

View File

@ -123,7 +123,7 @@ export class GroundUnitsDatabase extends UnitDatabase {
}
],
"filename": ""
}
},
"2B11 mortar": {
"name": "2B11 mortar",
"label": "2B11 mortar",

View File

@ -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"))
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 100));
(<HTMLElement>element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(mToFt(this.getFlightData().altitude) / 100));
if (element.querySelector(".unit-speed"))
(<HTMLElement>element.querySelector(".unit-speed")).innerText = String(Math.floor(this.getFlightData().speed * 1.94384));
(<HTMLElement>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,

View File

@ -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();
}
}

View File

@ -1,15 +1,15 @@
<div id="map-contextmenu" oncontextmenu="return false;">
<div id="active-coalition-label" data-active-coalition="blue"></div>
<div id="upper-bar" class="ol-panel">
<label id="context-menu-switch" class="toggle" for="context-menu-toggle">
<div data-active-coalition="blue" class="toggle-fill"></div>
</label>
<div id="coalition-switch" class="ol-switch"></div>
<button data-active-coalition="blue" id="aircraft-spawn-button" title="Spawn aircraft" data-on-click="contextMenuShow"
data-on-click-params='{ "type": "aircraft" }' class="unit-spawn-button"></button>
<button data-active-coalition="blue" id="ground-unit-spawn-button" title="Spawn ground unit" data-on-click="contextMenuShow"
data-on-click-params='{ "type": "ground-unit" }' class="unit-spawn-button"></button>
<button data-active-coalition="blue" id="smoke-spawn-button" title="Spawn smoke" data-on-click="contextMenuShow"
data-on-click-params='{ "type": "smoke" }' class="unit-spawn-button"></button>
<button data-active-coalition="blue" id="explosion-spawn-button" title="Explosion" data-on-click="contextMenuShow"
data-on-click-params='{ "type": "explosion" }' class="unit-spawn-button"></button>
</div>
<div id="aircraft-spawn-menu" class="ol-panel hide">
<div class="ol-select-container">
@ -38,6 +38,17 @@
</div>
</div>
</div>
<div id="aircraft-spawn-altitude-slider" class="ol-slider-container flight-control-ol-slider">
<dl class="ol-data-grid">
<dt> Spawn altitude
</dt>
<dd>
<div class="ol-slider-value"></div>
</dd>
</dl>
<input type="range" min="0" max="100" value="0" class="ol-slider">
</div>
<div id="loadout-preview">
<img id="unit-image" class="hide">
<div id="loadout-list">
@ -72,6 +83,12 @@
<button class="smoke-button" title="" data-smoke-color="green" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "green" }'>Green smoke</button>
<button class="smoke-button" title="" data-smoke-color="orange" data-on-click="contextMenuDeploySmoke" data-on-click-params='{ "color": "orange" }'>Orange smoke</button>
</div>
<div id="explosion-menu" class="ol-panel hide">
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "strength": 50 }'>Small explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "strength": 100 }'>Medium explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "strength": 200 }'>Big explosion</button>
<button class="explosion-button" title="" data-on-click="contextMenuExplosion" data-on-click-params='{ "strength": 400 }'>Huge explosion</button>
</div>
</div>
<div id="unit-contextmenu" class="ol-panel" oncontextmenu="return false;">

View File

@ -1,24 +1,28 @@
<div id="mouse-info-panel" class="ol-panel" oncontextmenu="return false;">
<div>
<div id="measuring-tool" class="hide">
<dl class="ol-data-grid">
<dt id="ref-measure-position" data-tooltip="CTRL-click on the map to activate the measuring tool."></dt>
<dt id="ref-measure-position" data-tooltip="CTRL-click on the map to activate the measuring tool">
<img src="/resources/theme/images/icons/pin.svg" inject-svg>
</dt>
<dd id="measure-position" class="br-info" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
<dt id="ref-unit-position" data-tooltip="Bearing/range from selected unit."></dt>
<dt id="ref-unit-position" data-tooltip="Bearing/range from selected unit">
<img src="/resources/theme/images/icons/plane.svg" inject-svg>
</dt>
<dd id="unit-position" class="br-info" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
</dl>
</div>
<div>
<div id="bullseyes-tool">
<dl class="ol-data-grid">
<dt id="ref-bullseye-2" data-tooltip="Bearing/range from this bullseye." data-label="BE" data-coalition="blue"></dt>
<dd id="bullseye-2" class="br-info" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
<dt id="ref-bullseye-1" data-tooltip="Bearing/range from this bullseye." data-label="BE" data-coalition="red"></dt>
<dd id="bullseye-1" class="br-info" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
<dt id="ref-bullseye-2" data-tooltip="Bearing/range from this bullseye" data-label="BE" data-coalition="blue"></dt>
<dd id="bullseye-2" class="br-info" data-coalition="blue" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
<dt id="ref-bullseye-1" data-tooltip="Bearing/range from this bullseye" data-label="BE" data-coalition="red"></dt>
<dd id="bullseye-1" class="br-info" data-coalition="red" data-bearing="---" data-distance="---" data-distance-units="NM"></dd>
</dl>
</div>
<div>
<div id="coordinates-tool">
<dl class="ol-data-grid">
<dt id="ref-mouse-position-latitude" data-label="?"></dt>
<dd id="mouse-position-latitude" class="coordinates" data-dd="---" data-mm="---" data-ss="--" data-sss="--"></dd>

View File

@ -6,7 +6,7 @@
<div class="ol-select-options">
<div id="toolbar-summary">
<h3>DCS Olympus</h3>
<div class="accent-green app-version-number">version v0.2.1</div>
<div class="accent-green app-version-number">version v0.3.0</div>
</div>
<div>
<a href="https://www.discord.com" target="_blank">Discord</a>
@ -15,7 +15,7 @@
<a href="https://github.com/Pax1601/DCSOlympus" target="_blank">Github</a>
</div>
<div data-on-click="reloadPage">
<a href="" target="_blank" data-on-click="reloadPage">Restart Olyumpus</a>
<a href="" target="_blank" data-on-click="reloadPage">Restart Olympus</a>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div id="unit-control-panel" class="ol-panel ol-panel-padding-lg" oncontextmenu="return false;">
<div id="unit-control-panel" class="ol-panel ol-panel-padding-lg" oncontextmenu="return false;">
<h3>Selected Units</h3>
@ -6,66 +6,78 @@
<div id="selected-units-container" class="ol-scrollable">
<!-- This is where all the unit selection buttons will be shown-->
<!-- <button class="pill highlight-coalition" data-coalition="blue" data-short-label="18">Olympus 1-1</button> -->
</div>
<hr />
<div id="flight-data">
<h4>Flight controls</h4>
<div class="slider-container flight-control-slider" id="airspeed-slider">
<dl class="ol-data-grid">
<dt>Speed</dt>
<dd class="flight-control-value" id="value"></dd>
</dl>
<input type="range" min="0" max="100" value="0" class="slider">
</div>
<div class="slider-container flight-control-slider" id="altitude-slider">
<dl class="ol-data-grid">
<dt>Altitude</dt>
<dd class="flight-control-value" id="value"></dd>
</dl>
<input type="range" min="0" max="100" value="0" class="slider">
</div>
<h5 id="categories-tooltip" class="hide">Multiple categories selected</h5>
<!-- <button class="pill highlight-coalition" data-coalition="blue" data-label="18">Olympus 1-1</button> -->
</div>
</div>
<!-- <h3>Formation</h3> -->
<!-- <div id="formation-buttons-container"> -->
<!-- This is where all the formation control buttons will be shown -->
<!-- </div> -->
<hr />
<div id="flight-data">
<h4>Controls</h4>
<div id="speed-slider" class="ol-slider-container flight-control-ol-slider">
<dl class="ol-data-grid">
<dt>Speed</dt>
<dd>
<div class="ol-slider-value"></div>
<div id="speed-type-switch" class="ol-switch"></div>
</dd>
</dl>
<input type="range" min="0" max="100" value="0" class="ol-slider">
<div class="ol-slider-min-max"></div>
</div>
<div id="altitude-slider" class="ol-slider-container flight-control-ol-slider">
<dl class="ol-data-grid">
<dt> Altitude
</dt>
<dd>
<div class="ol-slider-value"></div>
<div id="altitude-type-switch" class="ol-switch"></div>
</dd>
</dl>
<input type="range" min="0" max="100" value="0" class="ol-slider">
<div class="ol-slider-min-max"></div>
</div>
<h5 id="categories-tooltip">Multiple categories selected</h5>
</div>
<div id="roe">
<h4>Rules of engagement</h4>
<div id="roe-buttons-container" class="ol-group ol-button-box ol-option-button">
<!-- This is where the roe buttons will be shown -->
<!-- This is where the roe buttons will be shown -->
</div>
</div>
<div id="threat">
<h4>Reaction to threat</h4>
<div id="reaction-to-threat-buttons-container" class="ol-group ol-button-box ol-option-button">
<!-- This is where the reaction to threat buttons will be shown -->
<!-- This is where the reaction to threat buttons will be shown -->
</div>
</div>
<div id="emissions-countermeasures">
<h4>Radar & ECM</h4>
<div id="emissions-countermeasures-buttons-container" class="ol-group ol-button-box ol-option-button">
<!-- This is where the emissions/countermeasures buttons will be shown -->
<!-- This is where the emissions/countermeasures buttons will be shown -->
</div>
</div>
<div id="ai-on-off" class="switch-control">
<h4>Unit active</h4>
<div id="on-off-switch" class="ol-switch"></div>
<div>Toggling this disables unit AI completely. It will no longer move, react or emit radio waves.</div>
</div>
<div id="follow-roads" class="switch-control">
<h4>Follow roads</h4>
<div id="follow-roads-switch" class="ol-switch"></div>
</div>
<hr />
<div id="advanced-settings-div">
<button id="advanced-settings-button" class="ol-button-settings" data-on-click="showAdvancedSettings">Adjust settings</button>
<hr />
<button id="advanced-settings-button" class="ol-button-settings" data-on-click="showAdvancedSettings">Settings</button>
<button class="ol-button-warning" data-on-click="deleteSelectedUnits"><img src="/resources/theme/images/icons/trash-can-regular.svg" inject-svg>Delete</button>
<button class="ol-button-warning" data-on-click="explodeSelectedUnits"><img src="/resources/theme/images/icons/explosion-solid.svg" inject-svg></button>
</div>
<button class="ol-button-warning" data-on-click="deleteSelectedUnits">Delete unit</button>
</div>
</div>

View File

@ -14,7 +14,7 @@
<div id="loadout-container" class="panel-section">
<div id="loadout">
<div id="loadout-silhouette"></div>
<img id="loadout-silhouette"/>
<div id="loadout-items">
</div>
</div>

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
local version = 'v0.2.1-alpha'
local version = 'v0.3.0-alpha'
Olympus = {}
Olympus.OlympusDLL = nil

View File

@ -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;
};

View File

@ -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");
};

View File

@ -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;
};
};
/* 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;
};

View File

@ -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();
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<Coords> 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<Coords> 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<wstring, Measure*> 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<Coords> 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();
};

View File

@ -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<Unit*> 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<int, Unit*> units;

View File

@ -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();
}

View File

@ -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<Command*>(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<Command*>(new SetTask(ID, taskSS.str()));
Command* command = dynamic_cast<Command*>(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<Command*>(new SetTask(ID, taskSS.str()));
Command* command = dynamic_cast<Command*>(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<Command*>(new SetTask(ID, taskSS.str()));
Command* command = dynamic_cast<Command*>(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<Command*>(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<Command*>(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<Command*>(new SetTask(groupName, taskSS.str()));
scheduler->appendCommand(command);
setHasTask(true);
}
break;
}
default:
break;
}
addMeasure(L"currentTask", json::value(currentTask));
}
}

View File

@ -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();
}

View File

@ -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<Command*>(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<Command*>(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<Command*>(new SetOnOff(groupName, onOff));
scheduler->appendCommand(command);
}
void GroundUnit::setFollowRoads(bool newFollowRoads)
{
Unit::setFollowRoads(newFollowRoads);
resetActiveDestination(); /* Reset active destination to apply option*/
}

View File

@ -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();
}

View File

@ -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()

View File

@ -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<Command*>(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);

View File

@ -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<Command*>(new ResetTask(ID));
Command* command = dynamic_cast<Command*>(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<Command*>(new SetOption(ID, SetCommandType::ROE, ROEEnum));
Command* command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::ROE, ROEEnum));
scheduler->appendCommand(command);
}
}
@ -377,7 +399,7 @@ void Unit::setReactionToThreat(wstring newReactionToThreat) {
else
return;
Command* command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::REACTION_ON_THREAT, reactionToThreatEnum));
Command* command = dynamic_cast<Command*>(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<Command*>(new SetOption(ID, SetCommandType::RADAR_USING, radarEnum));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::RADAR_USING, radarEnum));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::FLARE_USING, flareEnum));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::FLARE_USING, flareEnum));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::ECM_USING, ECMEnum));
command = dynamic_cast<Command*>(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<Command*>(new SetCommand(ID, commandSS.str()));
Command* command = dynamic_cast<Command*>(new SetCommand(groupName, commandSS.str()));
scheduler->appendCommand(command);
}
else {
@ -483,7 +505,7 @@ void Unit::setTACAN(Options::TACAN newTACAN) {
<< "params = {"
<< "}"
<< "}";
Command* command = dynamic_cast<Command*>(new SetCommand(ID, commandSS.str()));
Command* command = dynamic_cast<Command*>(new SetCommand(groupName, commandSS.str()));
scheduler->appendCommand(command);
}
}
@ -511,7 +533,7 @@ void Unit::setRadio(Options::Radio newRadio) {
<< "frequency = " << radio.frequency << ","
<< "}"
<< "}";
command = dynamic_cast<Command*>(new SetCommand(ID, commandSS.str()));
command = dynamic_cast<Command*>(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<Command*>(new SetCommand(ID, commandSS.str()));
command = dynamic_cast<Command*>(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<Command*>(new SetOption(ID, SetCommandType::PROHIBIT_AA, generalSettings.prohibitAA));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::PROHIBIT_AA, generalSettings.prohibitAA));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::PROHIBIT_AG, generalSettings.prohibitAG));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::PROHIBIT_AG, generalSettings.prohibitAG));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::PROHIBIT_JETT, generalSettings.prohibitJettison));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::PROHIBIT_JETT, generalSettings.prohibitJettison));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::PROHIBIT_AB, generalSettings.prohibitAfterburner));
command = dynamic_cast<Command*>(new SetOption(groupName, SetCommandType::PROHIBIT_AB, generalSettings.prohibitAfterburner));
scheduler->appendCommand(command);
command = dynamic_cast<Command*>(new SetOption(ID, SetCommandType::ENGAGE_AIR_WEAPONS, !generalSettings.prohibitAirWpn));
command = dynamic_cast<Command*>(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<Command*>(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));
}

View File

@ -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<int> 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<Unit*> UnitsManager::getGroupMembers(wstring groupName)
{
vector<Unit*> 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<int, json::value> 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<Command*>(new Delete(ID));
Command* command = dynamic_cast<Command*>(new Delete(ID, explosion));
scheduler->appendCommand(command);
}
}

View File

@ -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);

View File

@ -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;
}