Pax1601 main (#52)

* GA initial data

* First commit of crude functionality.

* More AIC work so I don't lose it. (Best commit message ever.)

* Restructured to use 'phrases'.

* Set to a working state.

* Committing so I don't lose work.

* Added ai-formation feature swtich and UI kit stuff.

* Added plane units to UI kit.
This commit is contained in:
PeekabooSteam 2023-02-25 17:03:03 +00:00 committed by GitHub
parent abf5f40020
commit 1c1e60146d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 17524 additions and 43 deletions

2
client/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -20,9 +20,12 @@
"save": "^2.9.0"
},
"devDependencies": {
"@types/gtag.js": "^0.0.12",
"@types/sortablejs": "^1.15.0",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",
"nodemon": "^2.0.20",
"sortablejs": "^1.15.0",
"tsify": "^5.0.4",
"typescript": "^4.9.4",
"watchify": "^4.0.0"
@ -33,6 +36,12 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"node_modules/@types/gtag.js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
"integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==",
"dev": true
},
"node_modules/@types/leaflet": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
@ -41,6 +50,12 @@
"@types/geojson": "*"
}
},
"node_modules/@types/sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==",
"dev": true
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -2614,6 +2629,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==",
"dev": true
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@ -3227,6 +3248,12 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"@types/gtag.js": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
"integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==",
"dev": true
},
"@types/leaflet": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
@ -3235,6 +3262,12 @@
"@types/geojson": "*"
}
},
"@types/sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -5316,6 +5349,12 @@
}
}
},
"sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==",
"dev": true
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",

View File

@ -5,8 +5,8 @@
"version": "0.0.0",
"private": true,
"scripts": {
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"",
"copy": "copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet.css",
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"",
"watch": "watchify .\\src\\index.ts --debug -p [ tsify --noImplicitAny ] -o .\\public\\javascripts\\bundle.js"
},
"dependencies": {
@ -22,9 +22,12 @@
"save": "^2.9.0"
},
"devDependencies": {
"@types/gtag.js": "^0.0.12",
"@types/sortablejs": "^1.15.0",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",
"nodemon": "^2.0.20",
"sortablejs": "^1.15.0",
"tsify": "^5.0.4",
"typescript": "^4.9.4",
"watchify": "^4.0.0"

View File

@ -0,0 +1,27 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M285 590 c-11 -12 -31 -20 -51 -20 -23 0 -37 -6 -44 -20 -6 -11 -17
-20 -25 -20 -7 0 -16 -4 -20 -10 -10 -16 14 -174 28 -185 7 -5 16 -18 20 -27
3 -10 16 -18 27 -18 13 0 20 -7 20 -20 0 -11 5 -20 10 -20 6 0 10 -43 10 -110
l0 -110 60 0 60 0 0 110 c0 67 4 110 10 110 6 0 10 9 10 20 0 13 7 20 20 20
11 0 24 8 27 18 4 9 13 22 20 27 14 11 38 169 28 185 -4 6 -13 10 -20 10 -8 0
-19 9 -25 20 -7 14 -21 20 -44 20 -20 0 -40 8 -51 20 -10 11 -26 20 -35 20 -9
0 -25 -9 -35 -20z m60 -10 c4 -6 -7 -10 -25 -10 -18 0 -29 4 -25 10 3 6 15 10
25 10 10 0 22 -4 25 -10z m90 -40 c4 -6 -37 -10 -115 -10 -78 0 -119 4 -115
10 4 6 53 10 115 10 62 0 111 -4 115 -10z m-205 -110 c0 -78 -1 -80 -24 -80
-22 0 -25 5 -31 48 -3 26 -8 62 -11 80 -5 31 -4 32 31 32 l35 0 0 -80z m80 0
l0 -80 -30 0 -30 0 0 80 0 80 30 0 30 0 0 -80z m80 0 l0 -80 -30 0 -30 0 0 80
0 80 30 0 30 0 0 -80z m86 48 c-3 -18 -8 -54 -11 -80 -6 -43 -9 -48 -31 -48
-23 0 -24 2 -24 80 l0 80 35 0 c35 0 36 -1 31 -32z m-46 -158 c0 -6 -43 -10
-110 -10 -67 0 -110 4 -110 10 0 6 43 10 110 10 67 0 110 -4 110 -10z m-50
-40 c0 -6 -27 -10 -60 -10 -33 0 -60 4 -60 10 0 6 27 10 60 10 33 0 60 -4 60
-10z m-20 -130 l0 -100 -40 0 -40 0 0 100 0 100 40 0 40 0 0 -100z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,22 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M386 870 c-63 -16 -153 -70 -197 -117 -22 -24 -55 -74 -72 -111 -29
-61 -32 -76 -32 -163 0 -90 2 -99 37 -171 45 -91 103 -147 196 -191 61 -29 76
-32 162 -32 86 0 101 3 162 32 93 44 151 100 196 191 35 72 37 81 37 172 0 91
-2 100 -37 172 -68 136 -188 217 -336 224 -42 2 -94 -1 -116 -6z m229 -101
c63 -30 124 -90 155 -154 34 -72 34 -198 0 -270 -31 -64 -91 -124 -155 -155
-72 -34 -198 -34 -270 0 -260 126 -238 495 35 594 63 23 170 16 235 -15z"/>
<path d="M415 706 c-37 -16 -70 -52 -84 -89 -19 -49 -15 -57 29 -57 29 0 40 4
40 15 0 27 47 65 80 65 41 0 80 -39 80 -79 0 -23 -13 -44 -51 -83 -46 -48 -69
-84 -69 -109 0 -5 18 -9 40 -9 22 0 40 4 40 10 0 5 24 37 54 70 30 34 58 75
61 91 27 123 -104 226 -220 175z"/>
<path d="M440 280 l0 -40 40 0 40 0 0 40 0 40 -40 0 -40 0 0 -40z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M400 868 c-48 -16 -133 -104 -147 -151 -14 -45 -9 -64 18 -72 26 -8
45 8 59 50 6 19 27 48 45 64 81 71 202 45 249 -53 34 -72 16 -128 -69 -216
-29 -30 -65 -73 -79 -94 -32 -48 -35 -102 -6 -111 27 -8 37 -1 60 46 11 21 50
70 85 108 100 105 125 183 91 279 -35 97 -107 152 -209 158 -34 2 -78 -2 -97
-8z"/>
<path d="M444 146 c-3 -8 -4 -25 -2 -38 2 -19 9 -23 38 -23 33 0 35 2 35 35 0
32 -3 35 -33 38 -21 2 -34 -2 -38 -12z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 856 B

View File

@ -0,0 +1,28 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="50.000000pt" height="50.000000pt" viewBox="0 0 50.000000 50.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,50.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M149 442 c-149 -78 -158 -288 -16 -376 198 -122 423 103 301 301 -21
34 -44 55 -83 75 -71 37 -131 37 -202 0z m161 -2 c52 -15 76 -36 61 -53 -10
-13 -17 -12 -48 4 -47 24 -96 24 -147 0 -23 -10 -48 -21 -57 -24 -25 -8 -36
-132 -16 -179 14 -34 72 -88 94 -88 6 0 16 -7 23 -15 15 -18 46 -20 57 -2 4 7
23 19 41 27 77 31 114 136 74 212 -17 32 -18 39 -5 49 18 15 38 -10 54 -66 24
-84 -10 -174 -84 -223 -56 -37 -153 -39 -210 -3 -110 68 -128 220 -37 311 55
55 124 72 200 50z m2 -66 c27 -14 29 -17 15 -31 -11 -12 -19 -12 -35 -4 -62
33 -142 -18 -142 -90 0 -32 26 -74 55 -87 37 -17 65 -15 102 8 37 22 53 76 34
117 -10 21 -9 29 3 41 14 14 17 12 31 -21 26 -63 15 -117 -35 -162 -27 -25
-133 -30 -168 -8 -34 21 -62 73 -62 113 0 40 7 54 33 63 9 3 17 15 17 26 0 44
93 65 152 35z m-172 -34 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10
10 6 0 10 -4 10 -10z m148 -18 c15 -10 -12 -38 -43 -44 -14 -2 -20 -11 -20
-28 0 -20 5 -25 25 -25 17 0 26 6 28 20 6 31 34 58 44 43 13 -21 9 -65 -7 -72
-8 -3 -15 -12 -15 -20 0 -16 -21 -26 -56 -26 -35 0 -74 42 -74 80 0 57 72 101
118 72z m-28 -72 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0
10 -4 10 -10z m0 -150 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10
6 0 10 -4 10 -10z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,21 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M411 846 c-77 -78 -70 -86 69 -86 139 0 146 8 69 86 -29 30 -60 54
-69 54 -9 0 -40 -24 -69 -54z"/>
<path d="M132 668 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
<path d="M132 508 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
<path d="M132 348 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
<path d="M363 184 c-7 -18 92 -124 117 -124 25 0 124 106 117 124 -9 24 -225
24 -234 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,194 @@
/**************************************/
.olympus-dialog {
align-self: center;
background:white;
border-radius: 10px;
display: flex;
flex-direction: column;
justify-self: center;
padding:10px;
position:absolute;
width:fit-content;
z-index: 9999;
}
.olympus-dialog-close {
cursor:pointer;
position:absolute;
right:10px;
top:5px;
}
.olympus-dialog-header {
font-weight:bold;
}
/**************************************/
/***** AIC *****/
.aic-panel {
z-index: 9999;
}
#aic-control-panel {
bottom:30px;
position: absolute;
left:30px;
}
#aic-control-panel .olympus-button img {
max-width: 32px;
}
#aic-toolbox, #aic-callsign-panel {
align-items: flex-start;
align-self: center;
flex-direction: column;
row-gap: 10px;
display:none;
position:absolute;
}
.aic-panel {
background:#eaeaea;
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
justify-self: left;
padding:5px 10px;
}
.aic-enabled #aic-toolbox, .aic-enabled #aic-callsign-panel {
display:flex;
}
.aic-enabled #aic-callsign-panel {
align-self: auto;
top: 100px;
}
.aic-panel h2 {
font-size:90%;
margin:0;
padding:0;
text-align: center;
}
#aic-callsign-display {
text-align: center;
}
#aic-formation-list {
display:flex;
flex-direction: column;
justify-content: center;
}
#aic-formation-list > div {
align-items: center;
cursor: pointer;
display:flex;
flex-direction: column;
justify-content: center;
margin-top:10px;
position:relative;
}
#aic-formation-list .aic-formation-image img {
border: 1px solid #ccc;
border-radius: 10px;
max-width: 50px;
}
#aic-formation-list .aic-formation-name {
font-size:90%;
}
#aic-formation-list .aic-formation-descriptor {
background:white;
border-radius: 10px;
left:100px;
padding:5px;
position:absolute;
width: max-content;
}
#aic-teleprompt {
background-color: white;
border:2px solid black;
border-radius: 10px;
bottom: 50px;
color: black;
display: none;
justify-content: center;
justify-self: center;
padding: 10px;
position: absolute;
width: fit-content;
z-index: 1000;
}
.aic-enabled #aic-teleprompt {
display:flex;
}
#aic-descriptor {
display:flex;
flex-direction: row;
}
#aic-descriptor .aic-descriptor-section {
display:flex;
flex-direction: column;
margin:0 10px;
}
#aic-descriptor .aic-descriptor-section-label {
background-color:#eaeaea;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding:.25em;
text-align: center;
}
#aic-descriptor .aic-descriptor-phrase {
border-bottom: 1px solid #ccc;
display:flex;
flex-direction: row;
margin-bottom:5px;
padding-bottom:2px;
}
#aic-descriptor .aic-descriptor-phrase:last-of-type {
margin-bottom: 0;
}
#aic-descriptor .aic-descriptor-components .aic-descriptor-component {
margin:0 5px;
text-align: center;
}
#aic-descriptor .aic-descriptor-component-label {
display:none;
}
#aic-descriptor .aic-descriptor-component-value:after {
content:",";
margin-right:5px;
}
#aic-descriptor .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after {
content:"; ";
}
#aic-descriptor .aic-descriptor-section:last-of-type .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after {
content:".";
}
/**************************************/

View File

@ -0,0 +1,211 @@
/*** Control panel ***/
#atc-control-panel {
align-self: flex-end;
background: white;
border-radius: 10px;
display:flex;
margin: 0 0 50px 100px;
padding:5px;
position: absolute;
z-index: 9999;
}
.atc-tool {
align-self: center;
border-radius: 10px;
display:none;
justify-self: center;
padding: 10px;
position: absolute;
z-index: 9999;
}
.atc-enabled .atc-tool {
display:flex;
}
#atc-flight-list {
flex-direction: column;
}
#atc-flight-list table {
color:white;
}
#atc-flight-list table td {
padding:0 10px;
text-align: center;
}
#atc-flight-list table td:first-of-type {
text-align: left;
}
#atc-flight-list table tr[data-status='checkedIn'] td {
background-color:goldenrod;
}
#atc-flight-list table tr[data-status='readyToTaxi'] td {
background-color:darkgreen;
}
#atc-flight-list table button {
background-color: #666;
border:1px solid white;
color:white;
font-weight: bold;
margin:2px 0;
}
.atc-strip-board {
align-self: center;
display:flex;
justify-self: center;
position: absolute;
z-index: 9999 ;
}
.atc-strip-board-header {
display:none;
}
.atc-strip-board-strips {
display:flex;
flex-direction: column;
}
.atc-strip-board-strip {
display:flex;
flex-direction: row;
}
/*
.atc-strip-board-header {
background:black;
color:white;
display:none;
justify-content: right;
}
.atc-strip-board {
display:flex;
flex-direction: column;
row-gap: 5px;
}
.atc-strip-board-strips {
display:flex;
flex-direction: column;
padding:10px;
row-gap: 5px;
}
.atc-strip-board-strips > div {
align-items: center;
color:white;
column-gap: 2px;
display: flex;
flex-direction: row;
padding: 5px;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
text-align: center;
width: 75px;
}
.atc-strip-board-header > .name {
width:150px;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
text-align: center;
width: 75px;
}
.atc-strip-board-strips > div > .name {
text-align: left;
width:150px;
}
.atc-strip-board-strips > div {
align-items: center;
column-gap: 5px;
display: flex;
flex-direction: row;
font-size:12px;
font-weight: 600;
padding: 5px;
row-gap: 5px;
}
/*
.atc-strip-board-header, .atc-strip-board-strips > div {
align-items: center;
background:#FFF3;
color:white;
column-gap: 5px;
display: flex;
flex-direction: row;
font-size:12px;
font-weight: 600;
padding: 5px;
row-gap: 5px;
}
.atc-strip-board-header {
background:black;
color:white;
display:none;
justify-content: right;
}
.atc-strip-board-strips > div {
border-bottom:1px solid black;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
text-align: center;
width: 75px;
}
.atc-strip-board-header > .name {
width:150px;
}
.atc-strip-board-strips > div > .handle {
background: black;
border-radius: 50%;
cursor:grab;
height:10px;
width:10px;
}
.atc-strip-board-strips > div > .name {
text-align: left;
width:150px;
}
.atc-strip-board-strips > div > .warning {
background:red;
color: white;
font-weight: bold;
}
.atc-strip-board-strips > div > .link-warning {
border: 1px solid red;
color: red;
font-weight: bold;
}
*/

View File

@ -1,8 +1,9 @@
/* Page style */
body {
padding: 0;
display:grid;
margin: 0;
padding: 0;
}
html,
@ -108,4 +109,11 @@ body {
#unit-control-panel {
top: 50px;
}
}
.hide {
display:none !important;
}

View File

@ -0,0 +1,494 @@
/* Variables definitions */
:root {
--accent-green : #8bff63;
--accent-light-blue : #5ca7ff;
--background-grey : #3d4651;
--background-offwhite : #f2f2f3;
--background-steel : #202831;
--primary-blue : #247be2;
--primary-grey : #CFD9E8;
--primary-red : #ff5858;
--secondary-blue-outline : #082e44;
--secondary-dark-steel : #181e25;
--secondary-gunmetal-grey : #2f2f2f;
--secondary-light-grey : #797e83;
--secondary-red-outline : #262222;
--secondary-yellow : #ffd46893;
--border-radius-xs : 2px;
--border-radius-sm : 5px;
--border-radius-md : 10px;
--border-radius-lg : 15px;
--font-weight-bolder : 600;
}
:rootOLD {
--active-coalition-color: var(--blue-coalition-color);
--background-color-dark: #202831;
--background-color-light: #AAA;
--border-radius-sm:5px;
--border-radius-md:10px;
--border-radius-lg:15px;
--blue-coalition-color: #247be2;
--font-weight-bolder:600;
--highlight-color: #FFF5;
--neutral-coalition-color: whitesmoke;
--neutral-coalition-text: #202831;
--red-coalition-color: #f32121;
--text-color: white;
--title-color: #d3e9ff;
}
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-family: 'Open Sans', sans-serif;
}
button {
background-color:var(--background-steel);
border:1px solid var( --background-steel );
border-radius: var( --border-radius-sm );
color:whitesmoke;
cursor:pointer;
font-weight: var( --font-weight-bolder );
padding:8px;
}
button[disabled="disabled"] {
color: var( --highlight-color );
cursor:not-allowed;
}
.pill {
border-radius: var( --border-radius-sm );
display:inline-block;
padding:6px;
}
.ol-panel {
background-color: var(--background-steel);
border-radius: 15px;
box-shadow: 0px 2px 5px #000A;
color:white;
font-size: 12px;
height:fit-content;
padding:10px;
width:fit-content;
}
.ol-panel-list {
border-radius: var( --border-radius-sm );
display: flex;
flex-direction: column;
height: fit-content;
row-gap: 5px;
text-align: center;
width: fit-content;
}
.ol-panel-list .list-item {
border-radius: var( --border-radius-md );
display:flex;
justify-content: space-between;
padding: 6px 10px;
}
.ol-panel-list.sortable > .sortable-item {
align-items: center;
column-gap: 5px;
display:flex;
flex-direction: row;
}
.ol-panel-list.sortable > .sortable-item > .handle {
cursor:grab;
filter:invert(100);
}
.ol-panel-list.sortable > .sortable-item > .handle img {
max-width: 16px;
}
.ol-panel-board {
display:flex;
flex-direction: row;
justify-content: space-evenly;
}
.ol-panel-board > .panel-section {
border-right: 1px solid #555;
padding:10px;
}
.ol-panel-board > .panel-section:last-of-type {
border-right-width: 0;
}
.ol-panel-board h1, .ol-panel-board h2 {
font-size:18px;
font-weight: var( --font-weight-bolder );
margin: 0;
padding:0 0 5px 0;
}
.ol-panel-board h2 {
font-size:14px;
}
.highlight-primary {
background-color: var(--secondary-light-grey);
}
.highlight-bluefor {
background-color: var(--primary-blue);
color: var(--background-steel )
}
.highlight-redfor {
background-color: var(--primary-red);
}
.highlight-neutral {
background-color: var(--primary-grey);
color: var(--secondary-gunmetal-grey)
}
.unit {
border-radius: var( --border-radius-xs );
display:grid;
height: fit-content;
position:relative;
width:fit-content;
}
.unit .unit-id {
align-items: center;
background: var( --primary-grey );
border:3px solid var(--background-steel );
border-radius: var( --border-radius-xs );
color: var(--background-steel);
display: flex;
font-weight: bold;
height: 32px;
justify-content: center;
padding:4px;
position: relative;
text-align: center;
width:32px;
z-index:100;
}
.unit .unit-spotlight {
border-radius: 50%;
align-items: center;
display:flex;
height: fit-content;
justify-items: center;
padding:9px;
width:fit-content;
}
.unit .unit-hotgroup {
align-self: flex-start;
background: black;
color:white;
display:none;
height:fit-content;
justify-content: center;
justify-self: center;
line-height: 14px;
position: absolute;
text-align: center;
top:-8px;
transform:rotate(45deg);
width:14px;
}
.unit .unit-hotgroup-id {
font-size:11px;
transform: rotate(-45deg);
}
.unit .unit-vvi {
display:flex;
position:absolute;
left:50%;
transform:rotate(-90deg);
transform-origin:0 50%;
top:50%;
z-index: 0;
}
.unit .unit-vvi-heading {
border:1px solid var( --secondary-dark-steel );
padding-left:14px;
transform: rotate(90deg);
transform-origin:0 50%;
width:30px;
}
.unit .unit-selected-border {
border: 2px solid transparent;
border-radius: var( --border-radius-xs );
position: relative;
z-index:1;
}
.unit .unit-fuel {
background:white;
border:2px solid var( --secondary-dark-steel );
border-radius: var( --border-radius-xs );
display:none;
margin:0 auto;
position: relative;
top:-6px;
width: calc( 100% - 16px );
}
.unit .unit-fuel-empty {
align-self: center;
background: white;
border-radius: 50%;
color:red;
display:none;
font-weight: bold;
justify-self: center;
padding:1px;
position:absolute;
}
.unit[data-fuel-level="0"] .unit-fuel-empty {
display:flex;
}
@keyframes blinker {
50% {
opacity: 0;
}
}
.unit[data-fuel-level="10"] .unit-fuel,
.unit[data-fuel-level="20"] .unit-fuel,
.unit[data-fuel-level="30"] .unit-fuel {
animation: blinker 1.5s linear infinite;
}
.unit .unit-fuel-level {
background-color: var( --secondary-light-grey );
display:flex;
height: 4px;
visibility: hidden;
width:100%;
}
.unit[data-fuel-level="10"] .unit-fuel-level,
.unit[data-fuel-level="20"] .unit-fuel-level,
.unit[data-fuel-level="30"] .unit-fuel-level,
.unit[data-fuel-level="40"] .unit-fuel-level,
.unit[data-fuel-level="50"] .unit-fuel-level,
.unit[data-fuel-level="60"] .unit-fuel-level,
.unit[data-fuel-level="70"] .unit-fuel-level,
.unit[data-fuel-level="80"] .unit-fuel-level,
.unit[data-fuel-level="90"] .unit-fuel-level,
.unit[data-fuel-level="100"] .unit-fuel-level {
visibility: visible;
}
.unit[data-fuel-level="10"] .unit-fuel-level {
width:10%;
}
.unit[data-fuel-level="20"] .unit-fuel-level {
width:20%;
}
.unit[data-fuel-level="30"] .unit-fuel-level {
width:30%;
}
.unit[data-fuel-level="40"] .unit-fuel-level {
width:40%;
}
.unit[data-fuel-level="50"] .unit-fuel-level {
width:50%;
}
.unit[data-fuel-level="60"] .unit-fuel-level {
width:60%;
}
.unit[data-fuel-level="70"] .unit-fuel-level {
width:70%;
}
.unit[data-fuel-level="80"] .unit-fuel-level {
width:80%;
}
.unit[data-fuel-level="90"] .unit-fuel-level {
width:90%;
}
.unit[data-fuel-level="100"] .unit-fuel-level {
width:100%;
}
.unit-ammo {
column-gap: 2px;
display:none;
flex-direction: row;
flex-wrap:nowrap;
height:fit-content;
justify-content: center;
position: relative;
top:-2px;
width: 100%;
}
.unit-ammo > [data-ammo-type] {
background:white;
border:2px solid var( --secondary-dark-steel );
border-radius: 50%;
padding:3px;
}
.unit[data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
.unit[data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
.unit[data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
.unit[data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"] {
background-color: var( --secondary-light-grey );
}
/*** BLUEFOR ***/
.unit[data-coalition="blue"] .unit-hotgroup {
background-color: var( --secondary-blue-outline );
}
.unit[data-coalition="blue"] .unit-id,
.unit[data-coalition="blue"][data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
.unit[data-coalition="blue"][data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
.unit[data-coalition="blue"][data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
.unit[data-coalition="blue"][data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"],
.unit[data-coalition="blue"] .unit-fuel-level,
.unit[data-coalition="blue"] .unit-vvi-heading {
background: var( --primary-blue );
border-color: var( --secondary-blue-outline );
color: var( --secondary-blue-outline );
}
.unit[data-coalition="blue"][data-is-selected="true"] .unit-id {
border-color: var( --secondary-blue-outline );
color: var( --primary-blue );
}
.unit[data-coalition="blue"] .unit-ammo {
border-color: var( --secondary-blue-outline );
}
/*** REDFOR ***/
.unit[data-coalition="red"] .unit-hotgroup {
background-color: var( --secondary-red-outline );
}
.unit[data-coalition="red"] .unit-id,
.unit[data-coalition="red"][data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
.unit[data-coalition="red"][data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
.unit[data-coalition="red"][data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
.unit[data-coalition="red"][data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"],
.unit[data-coalition="red"] .unit-fuel-level,
.unit[data-coalition="red"] .unit-vvi-heading {
background: var( --primary-red );
border-color: var( --secondary-red-outline );
color: var( --secondary-red-outline );
}
.unit[data-coalition="red"][data-is-selected="true"] .unit-id {
color: var( --primary-red );
}
.unit[data-coalition="red"] .unit-ammo {
border-color: var( --secondary-red-outline );
}
/**************/
.unit[data-is-selected="true"] .unit-spotlight {
background: var(--secondary-yellow);
}
.unit[data-is-in-hotgroup="true"] .unit-hotgroup {
display:flex
}
.unit[data-is-selected="true"] .unit-id {
background:white;
}
.unit[data-is-selected="true"] .unit-selected-border {
border-color:white;
}
.unit[data-is-selected="true"] .unit-fuel {
display:grid;
}
.unit[data-is-selected="true"] .unit-ammo {
display:flex;
}
.unit[data-pilot="human"] .unit-hotgroup,
.unit[data-pilot="human"] .unit-fuel,
.unit[data-pilot="human"] .unit-ammo {
display:none;
}
.unit .unit-summary {
align-self: center;
column-gap: 8px;
color:white;
display: flex;
flex-flow: wrap;
font-size: 12px;
justify-content: flex-end;
justify-self: flex-end;
left: -62px;
position: absolute;
row-gap:2px;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
white-space: nowrap;
width: min-content;
}

View File

@ -16,6 +16,9 @@
@import url("mouseinfopanel.css");
@import url("logpanel.css");
@import url( "aic.css" );
@import url( "atc.css" );
@import url("layout.css");
@ -35,7 +38,7 @@
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
box-sizing: border-box;
}
html {

View File

@ -0,0 +1,61 @@
body {
background-color:#eaeaea;
}
#content-wrapper {
row-gap: 5px;
display: flex;
flex-direction: column;
flex-wrap: wrap;
height:100%;
width:100%;
}
section {
column-gap: 20px;
display:flex;
flex-direction: row;
flex-wrap: wrap;
}
.section-header {
font-size:125%;
font-weight: bold;
margin-bottom: 1vh;
}
.content {
background:white;
border-radius: 10px;
height:fit-content;
margin-bottom:4vh;
padding:20px;
width:fit-content;
}
.content-header {
color:#666;
letter-spacing:1px;
margin-bottom: 1vh;
}
.content-body {
column-gap: 20px;
display:flex;
flex-direction: row;
}
.example {
align-items: center;
display:flex;
flex-direction: column;
}
.caption {
margin:2vh 0 1vh 0;
}
#paragraph {
max-width: 750px;
}

1451
client/public/uikit.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,134 @@
export interface FeatureSwitchInterface {
"defaultEnabled": boolean, // default on/off state (if allowed by masterSwitch)
"label": string,
"masterSwitch": boolean, // on/off regardless of user preference
"name": string,
"options"?: object,
"removeArtifactsIfDisabled"?: boolean
}
class FeatureSwitch {
// From config param
defaultEnabled;
label;
masterSwitch;
name;
removeArtifactsIfDisabled = true;
// Self-set
userPreference;
constructor( config:FeatureSwitchInterface ) {
this.defaultEnabled = config.defaultEnabled;
this.label = config.label;
this.masterSwitch = config.masterSwitch;
this.name = config.name;
this.userPreference = this.getUserPreference();
}
getUserPreference() {
let preferences = JSON.parse( localStorage.getItem( "featureSwitches" ) || "{}" );
return ( preferences.hasOwnProperty( this.name ) ) ? preferences[ this.name ] : this.defaultEnabled;
}
isEnabled() {
if ( !this.masterSwitch ) {
return false;
}
return this.userPreference;
}
}
export class FeatureSwitches {
#featureSwitches:FeatureSwitch[] = [
new FeatureSwitch({
"defaultEnabled": false,
"label": "AIC",
"masterSwitch": true,
"name": "aic"
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "AI Formations",
"masterSwitch": true,
"name": "ai-formations",
"removeArtifactsIfDisabled": false
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "ATC",
"masterSwitch": true,
"name": "atc"
})
];
constructor() {
this.#removeArtifacts();
this.savePreferences();
}
getSwitch( switchName:string ) {
return this.#featureSwitches.find( featureSwitch => featureSwitch.name === switchName );
}
#removeArtifacts() {
for ( const featureSwitch of this.#featureSwitches ) {
if ( !featureSwitch.isEnabled() ) {
document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => {
if ( featureSwitch.removeArtifactsIfDisabled === false ) {
el.remove();
} else {
el.classList.add( "hide" );
}
});
}
}
}
savePreferences() {
let preferences:any = {};
for ( const featureSwitch of this.#featureSwitches ) {
preferences[ featureSwitch.name ] = featureSwitch.isEnabled();
}
localStorage.setItem( "featureSwitches", JSON.stringify( preferences ) );
}
}

View File

@ -0,0 +1,35 @@
export abstract class ToggleableFeature {
#status:boolean = false;
constructor( defaultStatus:boolean ) {
this.#status = defaultStatus;
this.onStatusUpdate();
}
getStatus() : boolean {
return this.#status;
}
protected onStatusUpdate() {}
toggleStatus( force?:boolean ) : void {
if ( force ) {
this.#status = force;
} else {
this.#status = !this.#status;
}
this.onStatusUpdate();
}
}

View File

@ -0,0 +1,54 @@
import { AICFormationContextDataInterface, AICFormationDescriptor } from "./AICFormationDescriptor";
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
export interface AICFormationInterface {
"icon" : string,
"label" : string,
"name" : string,
"numGroups" : number,
"summary" : string,
"unitBreakdown" : string[]
}
export abstract class AICFormation {
"icon" = "";
"label" = "";
"name" = "";
"numGroups" = 1;
"summary" = "";
"unitBreakdown":string[] = []
constructor() {
this.unitBreakdown = [];
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
return phrase;
}
getDescriptor( contextData: AICFormationContextDataInterface ) {
return new AICFormationDescriptor().generate( this, contextData );
}
hasUnitBreakdown() {
return this.unitBreakdown.length > 0;
}
showFormationNameInDescriptor() {
return true;
}
}

View File

@ -0,0 +1,38 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormation_Azimuth extends AICFormation implements AICFormationInterface {
"icon" = "azimuth.png";
"label" = "Azimuth";
"name" = "azimuth";
"numGroups" = 2;
"summary" = "Two contacts, side-by-side in a line perpedicular to the perspective.";
"unitBreakdown" = [ "<compass> group", "<compass> group" ];
constructor() {
super();
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
switch ( section.name ) {
case "formation":
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
}
return phrase;
}
}

View File

@ -0,0 +1,38 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormation_Range extends AICFormation implements AICFormationInterface {
"icon" = "range.png";
"label" = "Range";
"name" = "range";
"numGroups" = 2;
"summary" = "Two contacts, one behind the other";
"unitBreakdown" = [ "Lead group", "Trail group" ];
constructor() {
super();
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
switch ( section.name ) {
case "formation":
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
}
return phrase;
}
}

View File

@ -0,0 +1,24 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface, AICFormationDescriptor } from "../AICFormationDescriptor";
export class AICFormation_Single extends AICFormation implements AICFormationInterface {
"icon" = "single.png";
"label" = "Single";
"name" = "single";
"numGroups" = 1;
"summary" = "One contact on its own";
"unitBreakdown" = [];
constructor() {
super();
}
showFormationNameInDescriptor() {
return false;
}
}

View File

@ -0,0 +1,55 @@
import { AICFormation } from "./AICFormation";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
import { AICFormationDescriptorSection_Formation } from "./AICFormationDescriptorSection/Formation";
import { AICFormationDescriptorSection_Unit } from "./AICFormationDescriptorSection/Unit";
import { AICFormationDescriptorSection_NumGroups } from "./AICFormationDescriptorSection/NumGroups";
import { AICFormationDescriptorSection_Who } from "./AICFormationDescriptorSection/Who";
export interface AICFormationContextDataInterface {
"aicCallsign" : string,
"bullseyeName" : string,
"control" : "broadcast" | "tactical",
"numGroups" : number
}
export class AICFormationDescriptor {
#sections:AICFormationDescriptorSection[] = [
new AICFormationDescriptorSection_Who(),
new AICFormationDescriptorSection_NumGroups(),
new AICFormationDescriptorSection_Formation(),
new AICFormationDescriptorSection_Unit()
]
constructor() {
}
addSection( section:AICFormationDescriptorSection ) {
this.#sections.push( section );
}
getSections() {
return this.#sections;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let output:object[] = [];
for ( const section of this.#sections ) {
output.push(
section.generate( formation, contextData )
);
}
return output;
}
}

View File

@ -0,0 +1,18 @@
interface ComponentInterface {
"label" : string;
"value" : string;
}
export class AICFormationDescriptorComponent implements ComponentInterface {
label = "(not set)";
value = "(not set)";
constructor( value:any, label?:string ) {
this.label = label || "(not set)";
this.value = value;
}
}

View File

@ -0,0 +1,9 @@
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
export abstract class AICFormactionDescriptorComponent_Distance extends AICFormationDescriptorComponent {
constructor( value:string, label?:string ) {
super( value, label );
}
}

View File

@ -0,0 +1,9 @@
import { AICFormactionDescriptorComponent_Distance } from "../Distance";
export class AICFormationDescriptorComponent_Distance_Range extends AICFormactionDescriptorComponent_Distance {
constructor( value:string, label?:string ) {
super( value, label );
}
}

View File

@ -0,0 +1,40 @@
import { AICFormation } from "./AICFormation";
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
import { AICFormationDescriptorComponent } from "./AICFormationDescriptorComponent";
export interface AICFormationDescriptorPhraseInterface {
"generate" : CallableFunction,
"label" : string,
"name" : string
}
export class AICFormationDescriptorPhrase {
#components : AICFormationDescriptorComponent[] = [];
label = "";
name = "";
constructor() {
}
addComponent( component:AICFormationDescriptorComponent ) {
this.#components.push( component );
return this;
}
getComponents() {
return this.#components;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
return this;
}
}

View File

@ -0,0 +1,40 @@
import { AICFormation } from "./AICFormation";
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
export interface AICFormationDescriptorSectionInterface {
"generate" : CallableFunction,
"label" : string,
"name" : string,
"omitSection" : boolean
}
export abstract class AICFormationDescriptorSection {
#phrases : AICFormationDescriptorPhrase[] = [];
label = "";
name = "";
omitSection = false;
constructor() {
}
addPhrase( phrase:AICFormationDescriptorPhrase ) {
this.#phrases.push( phrase );
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
return this;
}
getPhrases() {
return this.#phrases;
}
}

View File

@ -0,0 +1,39 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_Formation extends AICFormationDescriptorSection {
label = "Formation";
name = "formation";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
if ( !formation.showFormationNameInDescriptor() ) {
this.omitSection = true;
return this;
}
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( formation.label, "Formation" ) );
phrase = formation.addToDescriptorPhrase( this, phrase, contextData );
this.addPhrase( phrase );
return this;
}
}

View File

@ -0,0 +1,35 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_NumGroups extends AICFormationDescriptorSection {
label = "Groups";
name = "numgroups";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let value = "Single group";
if ( contextData.numGroups > 1 ) {
value = contextData.numGroups + " groups";
}
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( value, "Number of groups" ) );
this.addPhrase( phrase );
return this;
}
}

View File

@ -0,0 +1,83 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
interface addUnitInformationInterface {
omitTrack?: boolean
}
export class AICFormationDescriptorSection_Unit extends AICFormationDescriptorSection {
label = "Unit";
name = "unit";
constructor() {
super();
}
addUnitInformation( formation:AICFormation, contextData: AICFormationContextDataInterface, phrase: AICFormationDescriptorPhrase, options?:addUnitInformationInterface ) {
options = options || {};
const originPoint = ( contextData.control === "broadcast" ) ? contextData.bullseyeName : "BRAA";
phrase.addComponent( new AICFormationDescriptorComponent( originPoint, "Bearing origin point" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<bearing>", "Bearing" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<range>", "Range" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>", "Altitude" ) );
if ( contextData.control === "broadcast" ) {
if ( !options.hasOwnProperty( "omitTrack" ) || options.omitTrack !== true ) {
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>", "Tracking" ) );
}
} else {
phrase.addComponent( new AICFormationDescriptorComponent( "[hot|flanking [left|right]|beam <compass>|cold]", "Azimuth" ) );
}
return phrase;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
if ( formation.hasUnitBreakdown() ) {
for ( const [ i, unitRef ] of formation.unitBreakdown.entries() ) {
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( unitRef, "Unit reference" ) );
if ( i === 0 ) {
this.addUnitInformation( formation, contextData, phrase, { "omitTrack": true } );
} else {
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>" ) );
}
phrase.addComponent( new AICFormationDescriptorComponent( "hostile" ) );
this.addPhrase( phrase );
}
} else {
this.addPhrase(
this.addUnitInformation( formation, contextData, new AICFormationDescriptorPhrase() )
);
}
return this;
}
}

View File

@ -0,0 +1,35 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_Who extends AICFormationDescriptorSection {
label = "Who";
name = "who";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let phrase = new AICFormationDescriptorPhrase();
if ( contextData.control === "tactical" ) {
phrase.addComponent( new AICFormationDescriptorComponent( "<their callsign>", "Their callsign" ) );
}
phrase.addComponent( new AICFormationDescriptorComponent( contextData.aicCallsign, "Your callsign" ) );
this.addPhrase( phrase );
return this;
}
}

172
client/src/aic/aic.ts Normal file
View File

@ -0,0 +1,172 @@
import { ToggleableFeature } from "../ToggleableFeature";
import { AICFormation_Azimuth } from "./AICFormation/Azimuth";
import { AICFormation_Range } from "./AICFormation/Range";
import { AICFormation_Single } from "./AICFormation/Single";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
export class AIC extends ToggleableFeature {
#formations = [
new AICFormation_Single(),
new AICFormation_Range(),
new AICFormation_Azimuth()
];
constructor() {
super( false );
this.onStatusUpdate();
// This feels kind of dirty
let $aicFormationList = document.getElementById( "aic-formation-list" );
if ( $aicFormationList ) {
this.getFormations().forEach( formation => {
// Image
let $imageDiv = document.createElement( "div" );
$imageDiv.classList.add( "aic-formation-image" );
let $img = document.createElement( "img" );
$img.src = "images/formations/" + formation.icon;
$imageDiv.appendChild( $img );
// Name
let $nameDiv = document.createElement( "div" );
$nameDiv.classList.add( "aic-formation-name" );
$nameDiv.innerText = formation.label;
// Wrapper
let $wrapperDiv = document.createElement( "div" );
$wrapperDiv.dataset.formationName = formation.name;
$wrapperDiv.appendChild( $imageDiv )
$wrapperDiv.appendChild( $nameDiv );
$wrapperDiv.addEventListener( "click", ( ev ) => {
const controlTypeInput = document.querySelector( "input[type='radio'][name='control-type']:checked" );
let controlTypeValue:any = ( controlTypeInput instanceof HTMLInputElement && [ "broadcast", "tactical" ].indexOf( controlTypeInput.value ) > -1 ) ? controlTypeInput.value : "broadcast";
// TODO: make this not an "any"
const output:any = formation.getDescriptor({
"aicCallsign" : "Magic",
"bullseyeName" : "Bullseye",
"control" : controlTypeValue,
"numGroups" : formation.numGroups
});
this.updateTeleprompt( output );
});
// Add to DOM
$aicFormationList?.appendChild( $wrapperDiv );
});
}
}
getFormations() {
return this.#formations;
}
onStatusUpdate() {
// Update the DOM
document.body.classList.toggle( "aic-enabled", this.getStatus() );
}
toggleHelp() {
document.getElementById( "aic-help" )?.classList.toggle( "hide" );
}
//*
updateTeleprompt<T extends AICFormationDescriptorSection>( descriptor:T[] ) {
let $teleprompt = document.getElementById( "aic-teleprompt" );
if ( $teleprompt instanceof HTMLElement ) {
// Clean slate
while ( $teleprompt.childNodes.length > 0 ) {
$teleprompt.childNodes[0].remove();
}
function newDiv() {
return document.createElement( "div" );
}
// Wrapper
let $descriptor = newDiv();
$descriptor.id = "aic-descriptor";
for ( const section of descriptor ) {
if ( section.omitSection ) {
continue;
}
let $section = newDiv();
$section.classList.add( "aic-descriptor-section" );
let $sectionLabel = newDiv();
$sectionLabel.classList.add( "aic-descriptor-section-label" );
$sectionLabel.innerText = section.label;
$section.appendChild( $sectionLabel );
for ( const phrase of section.getPhrases() ) {
let $phrase = newDiv();
$phrase.classList.add( "aic-descriptor-phrase" );
for ( const component of phrase.getComponents() ) {
let $component = newDiv();
$component.classList.add( "aic-descriptor-component" );
let $componentLabel = newDiv();
$componentLabel.classList.add( "aic-descriptor-component-label" );
$componentLabel.innerText = component.label;
let $componentValue = newDiv();
$componentValue.classList.add( "aic-descriptor-component-value" );
$componentValue.innerText = component.value;
$component.appendChild( $componentLabel );
$component.appendChild( $componentValue );
$phrase.appendChild( $component );
}
$section.appendChild( $phrase );
}
$descriptor.appendChild( $section );
}
$teleprompt.appendChild( $descriptor );
}
}
//*/
}

87
client/src/atc/ATC.ts Normal file
View File

@ -0,0 +1,87 @@
import { ToggleableFeature } from "../ToggleableFeature";
import Sortable from 'sortablejs';
import { ATCFLightList } from "./FlightList";
export class ATC extends ToggleableFeature {
constructor() {
super( true );
//this.#generateFlightList();
let $list = document.getElementById( "atc-strip-board-arrivals" );
if ( $list instanceof HTMLElement ) {
Sortable.create( $list, {
"handle": ".handle"
});
}
}
#generateFlightList() {
const flightList = new ATCFLightList();
const flights:any = flightList.getFlights( true );
const $tbody = document.getElementById( "atc-flight-list-table-body" );
if ( $tbody instanceof HTMLElement ) {
if ( flights.length > 0 ) {
let flight:any = {};
let $button, i;
for ( [ i, flight ] of flights.entries() ) {
const $row = document.createElement( "tr" );
$row.dataset.status = flight.status
let $td = document.createElement( "td" );
$td.innerText = flight.name;
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = flight.takeOffTime;
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = "00:0" + ( 5 + i );
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = flight.status;
$row.appendChild( $td );
$td = document.createElement( "td" );
$button = document.createElement( "button" );
$button.innerText = "...";
$td.appendChild( $button );
$row.appendChild( $td );
$tbody.appendChild( $row );
}
}
}
}
protected onStatusUpdate(): void {
document.body.classList.toggle( "atc-enabled", this.getStatus() );
}
}

View File

@ -0,0 +1,7 @@
export abstract class ATCMockAPI {
constructor() {}
generateMockData() {}
}

View File

@ -0,0 +1,40 @@
import { ATCMockAPI } from "../ATCMockAPI";
export class ATCMockAPI_Flights extends ATCMockAPI {
generateMockData() {
let data = [];
const statuses = [ "unknown", "checkedIn", "readyToTaxi" ]
for ( const [ i, flightName ] of [ "Shark", "Whale", "Dolphin" ].entries() ) {
data.push({
"name": flightName,
"status": statuses[ i ],
"takeOffTime": "18:0" + i
});
}
localStorage.setItem( "flightList", JSON.stringify( data ) );
}
get( generateMockDataIfEmpty?:boolean ) : object {
generateMockDataIfEmpty = generateMockDataIfEmpty || false;
let data = localStorage.getItem( "flightList" ) || "[]";
if ( data === "[]" && generateMockDataIfEmpty ) {
this.generateMockData();
}
return JSON.parse( data );
}
}

View File

@ -0,0 +1,18 @@
import { ATCMockAPI_Flights } from "./ATCMockAPI/Flights";
export class ATCFLightList {
constructor() {
}
getFlights( generateMockDataIfEmpty?:boolean ) {
let api = new ATCMockAPI_Flights();
return api.get( generateMockDataIfEmpty );
}
}

View File

@ -9,8 +9,14 @@ import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
import { MissionData } from "./missiondata/missiondata";
import { UnitControlPanel } from "./panels/unitcontrolpanel";
import { MouseInfoPanel } from "./panels/mouseInfoPanel";
import { Slider } from "./controls/slider";
import { AIC } from "./aic/AIC";
import { VisibilityControlPanel } from "./panels/visibilitycontrolpanel";
import { ATC } from "./atc/ATC";
import { FeatureSwitches } from "./FeatureSwitches";
import { LogPanel } from "./panels/logpanel";
import { Button } from "./controls/button";
/* TODO: should this be a class? */
var map: Map;
@ -29,10 +35,31 @@ var logPanel: LogPanel;
var mapSourceDropdown: Dropdown;
var slowButton: Button;
var fastButton: Button;
var climbButton: Button;
var descendButton: Button;
var aic: AIC;
var aicToggleButton: Button;
var aicHelpButton: Button;
var atc: ATC;
var atcToggleButton: Button;
var altitudeSlider: Slider;
var airspeedSlider: Slider;
var connected: boolean;
var activeCoalition: string;
var featureSwitches;
function setup() {
featureSwitches = new FeatureSwitches();
/* Initialize */
map = new Map('map-container');
unitsManager = new UnitsManager();
@ -43,11 +70,71 @@ function setup() {
unitInfoPanel = new UnitInfoPanel("unit-info-panel");
unitControlPanel = new UnitControlPanel("unit-control-panel");
//scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Marianas", "Nevada", "South Atlantic", "Syria", "The Channel"], () => { });
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel");
mouseInfoPanel = new MouseInfoPanel("mouse-info-panel");
visibilityControlPanel = new VisibilityControlPanel("visibility-control-panel");
logPanel = new LogPanel("log-panel");
missionData = new MissionData();
/* Unit control buttons */
slowButton = new Button("slow-button", ["images/buttons/slow.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("slow"); });
fastButton = new Button("fast-button", ["images/buttons/fast.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("fast"); });
climbButton = new Button("climb-button", ["images/buttons/climb.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("climb"); });
descendButton = new Button("descend-button", ["images/buttons/descend.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("descend"); });
/* Unit control sliders */
altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => getUnitsManager().selectedUnitsSetAltitude(value * 0.3048));
airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => getUnitsManager().selectedUnitsSetSpeed(value / 1.94384));
/* AIC */
let aicFeatureSwitch = featureSwitches.getSwitch( "aic" );
if ( aicFeatureSwitch?.isEnabled() ) {
aic = new AIC();
aicToggleButton = new Button( "toggle-aic-button", ["images/buttons/radar.svg"], () => {
aic.toggleStatus();
});
aicHelpButton = new Button( "aic-help-button", [ "images/buttons/question-mark.svg" ], () => {
aic.toggleHelp();
});
}
/* Generic clicks */
document.addEventListener( "click", ( ev ) => {
if ( ev instanceof PointerEvent && ev.target instanceof HTMLElement ) {
if ( ev.target.classList.contains( "olympus-dialog-close" ) ) {
ev.target.closest( "div.olympus-dialog" )?.classList.add( "hide" );
}
}
});
/*** ATC ***/
let atcFeatureSwitch = featureSwitches.getSwitch( "atc" );
if ( atcFeatureSwitch?.isEnabled() ) {
atc = new ATC();
atcToggleButton = new Button( "atc-toggle-button", [ "images/buttons/atc.svg" ], () => {
atc.toggleStatus();
} );
}
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
/* Default values */
@ -59,12 +146,15 @@ function setup() {
function requestUpdate() {
getDataFromDCS(update);
/* Main update rate = 250ms is minimum time, equal to server update time. */
setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000);
connectionStatusPanel.update(getConnected());
}
export function update(data: JSON) {
console.log( data );
unitsManager.update(data);
missionData.update(data);
logPanel.update(data);
@ -118,4 +208,9 @@ export function getConnected() {
return connected;
}
export function getUnitControlSliders() {
return {altitude: altitudeSlider, airspeed: airspeedSlider}
}
window.onload = setup;

View File

@ -1,3 +1,37 @@
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const λ1 = deg2rad(lon1); // φ, λ in radians
const λ2 = deg2rad(lon2);
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
const θ = Math.atan2(y, x);
const brng = (rad2deg(θ) + 360) % 360; // in degrees
return brng;
}
export function ConvertDDToDMS(D: number, lng: boolean) {
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
var dec = Math.round((sec - Math.floor(sec)) * 100);
var sec = Math.floor(sec);
if (lng)
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
else
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
}
export function deg2rad(deg: number) {
var pi = Math.PI;
return deg * (pi / 180);
}
export function distance(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371e3; // metres
const φ1 = deg2rad(lat1); // φ, λ in radians
@ -13,27 +47,24 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number)
return d;
}
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
const φ1 = deg2rad(lat1); // φ, λ in radians
const φ2 = deg2rad(lat2);
const λ1 = deg2rad(lon1); // φ, λ in radians
const λ2 = deg2rad(lon2);
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
const θ = Math.atan2(y, x);
const brng = (rad2deg(θ) + 360) % 360; // in degrees
return brng;
export function rad2deg(rad: number) {
var pi = Math.PI;
return rad / (pi / 180);
}
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
export function reciprocalHeading( heading:number ): number {
if ( heading > 180 ) {
return heading - 180;
}
return string;
return heading + 180;
}
export const zeroAppend = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
@ -42,25 +73,11 @@ export const zeroAppend = function (num: number, places: number) {
return string;
}
export function ConvertDDToDMS(D: number, lng: boolean) {
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
var min = 0 | (((D += 1e-9) % 1) * 60);
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
var dec = Math.round((sec - Math.floor(sec)) * 100);
var sec = Math.floor(sec);
if (lng)
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
else
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
}
export function deg2rad(deg: number) {
var pi = Math.PI;
return deg * (pi / 180);
}
export function rad2deg(rad: number) {
var pi = Math.PI;
return rad / (pi / 180);
export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
}
return string;
}

View File

@ -0,0 +1,17 @@
<div class="ol-panel aic-panel" id="aic-control-panel" data-feature-switch="aic">
<div class="olympus-button" id="toggle-aic-button"></div>
<div class="olympus-button" id="aic-help-button"></div>
</div>
<div id="aic-help" class="olympus-dialog hide" data-feature-switch="aic">
<div class="olympus-dialog-close">&times;</div>
<div class="olympus-dialog-header">AIC Help</div>
<div class="olympus-dialog-content">
<p>How to be a good AIC and get people to do stuff good, too.</p>
<div style="align-items: center; background:black; color:white; display:flex; height:250px; justify-content: center; justify-self: center; width:450px;">
<div>[DCS with Volvo video]</div>
</div>
</div>
</div>
<div id="aic-teleprompt"></div>

View File

@ -0,0 +1,33 @@
<div id="aic-callsign-panel" class="aic-panel" data-feature-switch="aic">
<div class="aic-panel">
<h2>My callsign</h2>
<div>Magic</div>
</div>
</div>
<div id="aic-toolbox" class="aic-panel" data-feature-switch="aic">
<div id="aic-control-type" class="aic-toolbox-panel">
<h2>Control</h2>
<div>
<input type="radio" name="control-type" id="control-type-broadcast" value="broadcast" checked="checked" />
<label for="control-type-broadcast">Broadcast</label>
</div>
<div>
<input type="radio" name="control-type" id="control-type-tactical" value="tactical" />
<label for="control-type-tactical">Tactical</label>
</div>
</div>
<div id="aic-formation-panel" class="aic-toolbox-panel">
<h2>Formations</h2>
<div id="aic-formation-list"></div>
</div>
</div>

105
client/views/atc.ejs Normal file
View File

@ -0,0 +1,105 @@
<div id="atc-control-panel" data-feature-switch="atc">
<div class="ol-button" id="atc-toggle-button"></div>
</div>
<div id="atc-flight-list" class="atc-tool hide" data-feature-switch="atc">
<table>
<thead>
<tr>
<th>Flight</th>
<th>T/O</th>
<th>TTG</th>
<th>Status</th>
<th> </th>
</tr>
</thead>
<tbody id="atc-flight-list-table-body"></tbody>
</table>
</div>
<div class="atc-strip-board" data-feature-switch="atc">
<div class="atc-strip-board-header">
<div class="name">Name</div>
<div class="bearing-range">BR</div>
<div class="target-altitude">t. Alt</div>
<div class="current-altitude">Alt</div>
<div class="target-speed">t. Spd</div>
<div class="current-speed">Speed</div>
<div class="runway">RWY</div>
<div class="line">Line</div>
</div>
<div id="atc-strip-board-arrivals" class="atc-strip-board-strips ol-panel">
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 3</div>
<div class="bearing-range">250 / 28</div>
<div class="target-altitude">-</div>
<div class="current-altitude">10000</div>
<div class="target-speed">-</div>
<div class="current-speed">421</div>
<div class="runway">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 2</div>
<div class="bearing-range">250 / 24</div>
<div class="target-altitude">6000</div>
<div class="current-altitude">6000</div>
<div class="target-speed">-</div>
<div class="current-speed">400</div>
<div class="runway">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 1</div>
<div class="bearing-range link-warning">262 / 12</div>
<div class="target-altitude">5000</div>
<div class="current-altitude">5100</div>
<div class="target-speed">-</div>
<div class="current-speed">367</div>
<div class="runway warning">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Dolphin 1</div>
<div class="bearing-range">250 / 4</div>
<div class="target-altitude link-warning">3000</div>
<div class="current-altitude warning">4100</div>
<div class="target-speed">-</div>
<div class="current-speed">511</div>
<div class="runway">25L</div>
<div class="line">2nd</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Whale 1</div>
<div class="bearing-range">070 / 2</div>
<div class="target-altitude">1500</div>
<div class="current-altitude">1650</div>
<div class="target-speed link-warning">350</div>
<div class="current-speed warning">312</div>
<div class="runway">25L</div>
<div class="line">1st</div>
</div>
</div>
</div>
</div>

View File

@ -7,6 +7,17 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;600&display=swap" rel="stylesheet">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z4L2TC3YX0"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-Z4L2TC3YX0');
</script>
</head>
<body>
@ -20,6 +31,12 @@
<%- include('visibilitycontrolpanel.ejs') %>
<%- include('connectionstatuspanel.ejs') %>
<%- include('mouseinfopanel.ejs') %>
<%- include('aiccontrolpanel.ejs') %>
<%- include('aicformationpanel.ejs') %>
<%- include( 'atc.ejs' ) %>
<%- include('logpanel.ejs') %>
<script src="javascripts/bundle.js"></script>
</body>

View File

@ -3,14 +3,15 @@
<div id="ol-title-label">Selected units</div>
<div id="selected-units-container" class="ol-scrollable">
-->
<!-- This is where all the unit selection buttons will be shown-->
<!--
</div>
<div id="formation-creation-container">
<div class="ol-rectangular-button white" id="create-formation"><img src="images\buttons\create.svg">Create formation</div>
<div class="ol-rectangular-button white" id="undo-formation"><img src="images\buttons\erase.svg">Undo formation</div>
<div id="formation-creation-container" data-feature-switch="ai-formations">
<div class="rectangular-button white" id="create-formation"><img src="images\buttons\create.svg">Create formation</div>
<div class="rectangular-button white" id="undo-formation"><img src="images\buttons\erase.svg">Undo formation</div>
</div>
<div class="ol-hl"></div>
-->

1004
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"devDependencies": {
"chai": "^4.3.7",
"mocha": "^10.2.0",
"sortablejs": "^1.15.0"
}
}

View File

@ -77,9 +77,11 @@ function Olympus.setMissionData(arg, time)
basesData[i] = info
end
-- Assemble missionData table
missionData["bullseye"] = bullseye
missionData["unitsData"] = unitsData
missionData["unitsData"] = unitsData
missionData["airbases"] = basesData
local command = "Olympus.missionData = " .. Olympus.serializeTable(missionData) .. "\n" .. "Olympus.OlympusDLL.setMissionData()"

File diff suppressed because it is too large Load Diff