diff --git a/client/demo.js b/client/demo.js index 11cd10a4..3550e864 100644 --- a/client/demo.js +++ b/client/demo.js @@ -8,7 +8,7 @@ const DEMO_UNIT_DATA = { formationOffset: { x: 0, y: 0, z: 0 }, targetID: 2, targetPosition: { lat: 0, lng: 0, alt: 0 }, - ROE: 2, + ROE: 1, reactionToThreat: 1, emissionsCountermeasures: 1, TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, @@ -24,7 +24,7 @@ const DEMO_UNIT_DATA = { formationOffset: { x: 0, y: 0, z: 0 }, targetID: 0, targetPosition: { lat: 0, lng: 0, alt: 0 }, - ROE: 2, + ROE: 1, reactionToThreat: 1, emissionsCountermeasures: 1, TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, @@ -39,7 +39,7 @@ const DEMO_UNIT_DATA = { formationOffset: { x: 0, y: 0, z: 0 }, targetID: 0, targetPosition: { lat: 0, lng: 0, alt: 0 }, - ROE: 2, + ROE: 1, reactionToThreat: 1, emissionsCountermeasures: 1, TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, @@ -54,7 +54,7 @@ const DEMO_UNIT_DATA = { formationOffset: { x: 0, y: 0, z: 0 }, targetID: 0, targetPosition: { lat: 0, lng: 0, alt: 0 }, - ROE: 2, + ROE: 1, reactionToThreat: 1, emissionsCountermeasures: 1, TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, @@ -64,13 +64,13 @@ const DEMO_UNIT_DATA = { contacts: [{ID: 1001, detectionMethod: 16}], activePath: [ ], isLeader: true - }, ["5"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 1, country: 0, name: "Gepard", unitName: "Cool guy 2-2", groupName: "Cool group 4", state: 1, task: "Being cool", + }, ["5"]:{ category: "GroundUnit", alive: true, human: false, controlled: true, coalition: 1, country: 0, name: "S_75M_Volhov", unitName: "Cool guy 2-2", groupName: "Cool group 4", state: 1, task: "Being cool", hasTask: false, position: { lat: 37.21, lng: -116.1, alt: 1000 }, speed: 200, heading: 315 * Math.PI / 180, isTanker: false, isAWACS: false, onOff: true, followRoads: false, fuel: 50, desiredSpeed: 300, desiredSpeedType: 1, desiredAltitude: 1000, desiredAltitudeType: 1, leaderID: 0, formationOffset: { x: 0, y: 0, z: 0 }, targetID: 0, targetPosition: { lat: 0, lng: 0, alt: 0 }, - ROE: 2, + ROE: 1, reactionToThreat: 1, emissionsCountermeasures: 1, TACAN: { isOn: false, XY: 'Y', callsign: 'TKR', channel: 40 }, @@ -99,9 +99,9 @@ class DemoDataGenerator { app.use('/demo', basicAuth({ users: { - 'admin': 'socks', - 'blue': 'bluesocks', - 'red': 'redsocks' + 'admin': 'password', + 'blue': 'bluepassword', + 'red': 'redpassword' }, })) diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 782d35e3..592eaffa 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -691,6 +691,11 @@ nav.ol-panel> :last-child { width: 30px; } +#reaction-to-threat-buttons-container button:not(:first-child) svg { + width: 150%; + margin: -5px; +} + #unit-control-panel .ol-option-button button.selected { background-color: white; border-color: white; @@ -700,6 +705,28 @@ nav.ol-panel> :last-child { fill: var(--background-steel); } +#rapid-controls { + display: flex; + flex-direction: column; + row-gap: 5px; + position: absolute; + height: fit-content; + width: fit-content; + left: calc(100% + 10px); + top: 0px; +} + +#rapid-controls button { + padding: 4px; +} + +#rapid-controls svg { + height: 20px; + width: 20px; + fill: white; + stroke: white; +} + /****************************************************************************************/ #splash-screen { background-image: url("/resources/theme/images/splash/1.png"); @@ -959,6 +986,10 @@ nav.ol-panel> :last-child { font-size: 10px; } +#command-mode-toolbar { + min-width: fit-content ; +} + #command-mode-toolbar .ol-button { border: 1px solid white; } diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 426a5790..ee3e4145 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -100,6 +100,10 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { margin-top: 15px; } +#advanced-settings-dialog .ol-text-input input { + height: 40px; +} + #general-settings-grid { display: grid; grid-template-columns: 1fr 1fr; diff --git a/client/public/themes/olympus/images/icons/climb.svg b/client/public/themes/olympus/images/icons/climb.svg new file mode 100644 index 00000000..85299ba7 --- /dev/null +++ b/client/public/themes/olympus/images/icons/climb.svg @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/descent.svg b/client/public/themes/olympus/images/icons/descent.svg new file mode 100644 index 00000000..67df6b2d --- /dev/null +++ b/client/public/themes/olympus/images/icons/descent.svg @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/hand-solid.svg b/client/public/themes/olympus/images/icons/hand-solid.svg new file mode 100644 index 00000000..4a6ba3aa --- /dev/null +++ b/client/public/themes/olympus/images/icons/hand-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/public/themes/olympus/images/icons/speed-decrease.svg b/client/public/themes/olympus/images/icons/speed-decrease.svg new file mode 100644 index 00000000..94ba5dcc --- /dev/null +++ b/client/public/themes/olympus/images/icons/speed-decrease.svg @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/client/public/themes/olympus/images/icons/speed-increase.svg b/client/public/themes/olympus/images/icons/speed-increase.svg new file mode 100644 index 00000000..f6bc0bdc --- /dev/null +++ b/client/public/themes/olympus/images/icons/speed-increase.svg @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/client/resources/theme/images/icons/speed-increase.svg b/client/resources/theme/images/icons/speed-increase.svg new file mode 100644 index 00000000..e69de29b diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts index fc208481..8d29bbb7 100644 --- a/client/src/constants/constants.ts +++ b/client/src/constants/constants.ts @@ -13,7 +13,7 @@ export const RWR = 16; export const DLINK = 32; export const states: string[] = ["none", "idle", "reach-destination", "attack", "follow", "land", "refuel", "AWACS", "tanker", "bomb-point", "carpet-bomb", "bomb-building", "fire-at-area"]; -export const ROEs: string[] = ["free", "designated", "return", "hold"]; +export const ROEs: string[] = ["free", "designated", "", "return", "hold"]; export const reactionsToThreat: string[] = ["none", "manoeuvre", "passive", "evade"]; export const emissionsCountermeasures: string[] = ["silent", "attack", "defend", "free"]; diff --git a/client/src/index.ts b/client/src/index.ts index fb53c5a5..8a86d75e 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -205,11 +205,13 @@ function setupEvents() { document.querySelectorAll("[inject-svg]").forEach((el: Element) => { var img = el as HTMLImageElement; - var isLoaded = img.complete && img.naturalHeight !== 0; + var isLoaded = img.complete; if (isLoaded) SVGInjector(img); else - img.onload = () => SVGInjector(img); + img.addEventListener("load", () => { + SVGInjector(img); + }); }) } diff --git a/client/src/map/map.ts b/client/src/map/map.ts index 51574d35..f727ce93 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -1,5 +1,5 @@ import * as L from "leaflet" -import { getUnitsManager } from ".."; +import { getMissionHandler, getUnitsManager } from ".."; import { BoxSelect } from "./boxselect"; import { MapContextMenu } from "../controls/mapcontextmenu"; import { UnitContextMenu } from "../controls/unitcontextmenu"; @@ -117,11 +117,20 @@ export class Map extends L.Map { Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); }); - document.addEventListener("toggleUnitVisibility", (ev: CustomEventInit) => { + document.addEventListener("toggleMarkerVisibility", (ev: CustomEventInit) => { const el = ev.detail._element; el?.classList.toggle("off"); ev.detail.types.forEach((type: string) => getUnitsManager().setHiddenType(type, !el?.classList.contains("off"))); Object.values(getUnitsManager().getUnits()).forEach((unit: Unit) => unit.updateVisibility()); + + if (ev.detail.types.includes("airbase")) { + Object.values(getMissionHandler().getAirbases()).forEach((airbase: Airbase) => { + if (el?.classList.contains("off")) + airbase.removeFrom(this); + else + airbase.addTo(this); + }) + } }); @@ -150,7 +159,9 @@ export class Map extends L.Map { /* Option buttons */ this.#optionButtons["visibility"] = visibilityControls.map((option: string, index: number) => { - return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleUnitVisibility", `{"types": "${visibilityControlsTypes[index]}"}`); + var typesArrayString = `"${visibilityControlsTypes[index][0]}"`; + visibilityControlsTypes[index].forEach((type: string, idx: number) => {if (idx > 0) typesArrayString = `${typesArrayString}, "${type}"`}); + return this.#createOptionButton(option, `visibility/${option.toLowerCase()}.svg`, visibilityControlsTootlips[index], "toggleMarkerVisibility", `{"types": [${typesArrayString}]}`); }); document.querySelector("#unit-visibility-control")?.append(...this.#optionButtons["visibility"]); diff --git a/client/src/mission/airbase.ts b/client/src/mission/airbase.ts index 4128001b..2510d0da 100644 --- a/client/src/mission/airbase.ts +++ b/client/src/mission/airbase.ts @@ -38,6 +38,7 @@ export class Airbase extends CustomMarker img.onload = () => SVGInjector(img); el.appendChild(img); this.getElement()?.appendChild(el); + el.dataset.coalition = this.#coalition; } setCoalition(coalition: string) diff --git a/client/src/panels/logpanel.ts b/client/src/panels/logpanel.ts index efa398c6..92fbb702 100644 --- a/client/src/panels/logpanel.ts +++ b/client/src/panels/logpanel.ts @@ -1,3 +1,4 @@ +import { getMouseInfoPanel } from ".."; import { Panel } from "./panel"; export class LogPanel extends Panel { @@ -23,8 +24,21 @@ export class LogPanel extends Panel { if (scrollEl) { scrollEl.addEventListener("scroll", () => { this.#scrolledDown = Math.abs(scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight) < 1 - }) + }); } + + window.addEventListener("resize", () => { + this.#calculateHeight(); + }); + + + const mouseInfoPanel = getMouseInfoPanel(); + new ResizeObserver(() => this.#calculateHeight()).observe(mouseInfoPanel.getElement()) + } + + show() { + super.show(); + this.#calculateHeight(); } appendLogs(logs: {[key: string]: string}) { @@ -68,4 +82,9 @@ export class LogPanel extends Panel { scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.clientHeight; } } + + #calculateHeight() { + const mouseInfoPanel = getMouseInfoPanel(); + this.getElement().style.height = `${mouseInfoPanel.getElement().offsetTop - this.getElement().offsetTop - 10}px`; + } } \ No newline at end of file diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index d2e90a59..a26be730 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -39,7 +39,7 @@ export class UnitControlPanel extends Panel { // Reversing the ROEs so that the least "aggressive" option is always on the left this.#optionButtons["ROE"] = ROEs.slice(0).reverse().map((option: string, index: number) => { return this.#createOptionButton(option, `roe/${option.toLowerCase()}.svg`, ROEDescriptions.slice(0).reverse()[index], () => { getUnitsManager().selectedUnitsSetROE(option); }); - }); + }).filter((button: HTMLButtonElement, index: number) => {return ROEs[index] !== "";}); this.#optionButtons["reactionToThreat"] = reactionsToThreat.map((option: string, index: number) => { return this.#createOptionButton(option, `threat/${option.toLowerCase()}.svg`, reactionsToThreatDescriptions[index],() => { getUnitsManager().selectedUnitsSetReactionToThreat(option); }); diff --git a/client/src/unit/aircraftdatabase.ts b/client/src/unit/aircraftdatabase.ts index e9293f92..39acb187 100644 --- a/client/src/unit/aircraftdatabase.ts +++ b/client/src/unit/aircraftdatabase.ts @@ -2847,7 +2847,7 @@ export class AircraftDatabase extends UnitDatabase { "roles": [ "CAP" ], - "code": "R-73*2,R-27R*2,Fuel-1500", + "code": "R-73*4,R-27R*2,Fuel-1500", "name": "Heavy / Fox 1, HOBS Fox 2 / Medium Range" }, { @@ -2935,7 +2935,7 @@ export class AircraftDatabase extends UnitDatabase { "CAP" ], "code": "R-73*2,R-27R*2,Fuel-1500", - "name": "Heavy / Fox 1 / Medium Range" + "name": "Heavy / Fox 1, HOBS Fox 2 / Medium Range" }, { "fuel": 1, diff --git a/client/src/unit/unitsmanager.ts b/client/src/unit/unitsmanager.ts index 047c64ec..c7c34a55 100644 --- a/client/src/unit/unitsmanager.ts +++ b/client/src/unit/unitsmanager.ts @@ -36,6 +36,8 @@ export class UnitsManager { document.addEventListener('importFromFile', () => this.importFromFile()); document.addEventListener('contactsUpdated', (e: CustomEvent) => {this.#requestDetectionUpdate = true}); document.addEventListener('commandModeOptionsChanged', () => {Object.values(this.#units).forEach((unit: Unit) => unit.updateVisibility())}); + document.addEventListener('selectedUnitsChangeSpeed', (e: any) => {this.selectedUnitsChangeSpeed(e.detail.type)}); + document.addEventListener('selectedUnitsChangeAltitude', (e: any) => {this.selectedUnitsChangeAltitude(e.detail.type)}); } getSelectableAircraft() { diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index 22fffd52..f7e8c4ad 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -79,5 +79,13 @@ + +
+ + + + + +
\ No newline at end of file diff --git a/client/views/toolbars/primary.ejs b/client/views/toolbars/primary.ejs index de1a6eae..f31a9633 100644 --- a/client/views/toolbars/primary.ejs +++ b/client/views/toolbars/primary.ejs @@ -28,9 +28,7 @@
-
- ArcGIS Satellite -
+
ArcGIS Satellite
diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 799497e8..710997cc 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -427,6 +427,10 @@ function Olympus.generateAirUnitsTable(units) local loadout = unit.loadout -- loadout: a string, one of the names defined in unitPayloads.lua. Must be compatible with the unitType local payload = unit.payload -- payload: a table, if present the unit will receive this specific payload. Overrides loadout + if unit.heading == nil then + unit.heading = 0 + end + if payload == nil then if loadout and loadout ~= "" and Olympus.unitPayloads[unit.unitType][loadout] then payload = Olympus.unitPayloads[unit.unitType][loadout] @@ -445,10 +449,13 @@ function Olympus.generateAirUnitsTable(units) ["alt_type"] = "BARO", ["skill"] = "Excellent", ["payload"] = { ["pylons"] = payload, ["fuel"] = 999999, ["flare"] = 60, ["ammo_type"] = 1, ["chaff"] = 60, ["gun"] = 100, }, - ["heading"] = 0, + ["heading"] = unit.heading, ["callsign"] = { [1] = 1, [2] = 1, [3] = 1, ["name"] = "Olympus" .. Olympus.unitCounter.. "-" .. #unitTable + 1 }, ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1 } + + -- Add the payload to the registry, used for unit cloning + Olympus.payloadRegistry[unitTable[#unitTable].name] = payload end return unitTable end @@ -532,7 +539,7 @@ function Olympus.generateGroundUnitsTable(units) ["type"] = unit.unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, - ["heading"] = 0, + ["heading"] = unit.heading, ["skill"] = "High", ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1 } @@ -566,7 +573,7 @@ function Olympus.generateNavyUnitsTable(units) ["type"] = unit.unitType, ["x"] = spawnLocation.x, ["y"] = spawnLocation.z, - ["heading"] = 0, + ["heading"] = unit.heading, ["skill"] = "High", ["name"] = "Olympus-" .. Olympus.unitCounter .. "-" .. #unitTable + 1, ["transportable"] = { ["randomTransportable"] = false } @@ -582,6 +589,9 @@ function Olympus.clone(ID, lat, lng, category) Olympus.debug("Olympus.clone " .. ID .. ", " .. category, 2) local unit = Olympus.getUnitByID(ID) if unit then + local position = unit:getPosition() + local heading = math.atan2( position.x.z, position.x.x ) + -- TODO: understand category in this script local spawnTable = { coalition = Olympus.getCoalitionByCoalitionID(unit:getCoalition()), @@ -591,6 +601,7 @@ function Olympus.clone(ID, lat, lng, category) lat = lat, lng = lng, alt = unit:getPoint().y, + heading = heading, unitType = unit:getTypeName(), payload = Olympus.payloadRegistry[unit:getName()] }