diff --git a/client/demo.js b/client/demo.js index be18be2f..b26fe651 100644 --- a/client/demo.js +++ b/client/demo.js @@ -50,7 +50,15 @@ const DEMO_UNIT_DATA = { activePath: undefined, targetSpeed: 400, targetAltitude: 3000, - isTanker: true + isTanker: false, + TACANOn: false, + TACANChannel: 32, + TACANXY: "Y", + TACANCallsign: "ASD", + radioFrequency: 123.750, + radioCallsign: 2, + radioCallsignNumber: 3, + radioAMFM: "FM" }, optionsData: { ROE: "Designated", diff --git a/client/public/images/icons/follow.svg b/client/public/images/icons/follow.svg new file mode 100644 index 00000000..2ec555ac --- /dev/null +++ b/client/public/images/icons/follow.svg @@ -0,0 +1,57 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/public/images/icons/sword.svg b/client/public/images/icons/sword.svg new file mode 100644 index 00000000..2d925c03 --- /dev/null +++ b/client/public/images/icons/sword.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/client/public/stylesheets/contextmenus.css b/client/public/stylesheets/contextmenus.css index f6981488..78b9d3e7 100644 --- a/client/public/stylesheets/contextmenus.css +++ b/client/public/stylesheets/contextmenus.css @@ -207,10 +207,44 @@ display: flex; flex-direction: column; height: fit-content; + width: fit-content; position: absolute; row-gap: 5px; - width: 150px; z-index: 1000; + padding: 15px; +} + +#unit-contextmenu button { + border: 1px solid var(--background-offwhite); + border-radius: var(--border-radius-sm); + padding: 12px; + font-weight: normal; +} + +#unit-contextmenu div { + display: flex; + flex-direction: row; + align-content: center; +} + +#unit-contextmenu div:before { + display: inline-block; + filter: invert(100%); + height: 16px; + margin-right: 15px; + width: 16px; +} + +#refuel::before { + content: url( /images/icons/fuel.svg ); +} + +#attack::before { + content: url( /images/icons/sword.svg ); +} + +#follow::before { + content: url( /images/icons/follow.svg ); } /* Airbase context menu */ diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index a0cccbec..6e96c593 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -53,6 +53,10 @@ button { padding: 6px; } +button:hover { + background-color: var(--background-hover); +} + button[disabled="disabled"] { color: var(--highlight-color); cursor: not-allowed; diff --git a/client/public/stylesheets/unitcontrolpanel.css b/client/public/stylesheets/unitcontrolpanel.css index fcc78ca0..ba50dc42 100644 --- a/client/public/stylesheets/unitcontrolpanel.css +++ b/client/public/stylesheets/unitcontrolpanel.css @@ -93,8 +93,24 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { margin-bottom:8px; } - #unit-control-panel #threat, #unit-control-panel #roe { margin-top:12px; +} + +#advanced-settings-dialog { + width: 400px; +} + +#advanced-settings-dialog > .ol-dialog-content { + margin-top: 10px; + margin-bottom: 10px; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + row-gap: 10px; +} + +#advanced-settings-dialog > .ol-dialog-content > .ol-group { + justify-content: space-between; } \ No newline at end of file diff --git a/client/public/themes/olympus/olympus.css b/client/public/themes/olympus/olympus.css index 3836ea74..8d909b7f 100644 --- a/client/public/themes/olympus/olympus.css +++ b/client/public/themes/olympus/olympus.css @@ -31,6 +31,8 @@ --secondary-light-grey : #797e83; --secondary-yellow : #ffd46893; + --background-hover : #f2f2f333; + --nav-text : #ECECEC; diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index 9dc67839..9e621fde 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -37,19 +37,21 @@ interface FormationData { } interface TaskData { + currentState: string; currentTask: string; activePath: any; targetSpeed: number; targetAltitude: number; isTanker: boolean; isAWACS: boolean; - radioOn: boolean; TACANOn: boolean; - radioFrequency: number; - radioCallsign: number; TACANChannel: number; TACANXY: string; TACANCallsign: string; + radioFrequency: number; + radioCallsign: number; + radioCallsignNumber: number; + radioAMFM: string; } interface OptionsData { diff --git a/client/src/controls/unitcontextmenu.ts b/client/src/controls/unitcontextmenu.ts index 2579c3f0..51bde81b 100644 --- a/client/src/controls/unitcontextmenu.ts +++ b/client/src/controls/unitcontextmenu.ts @@ -5,12 +5,12 @@ export class UnitContextMenu extends ContextMenu { super(id); } - setOptions(options: string[], callback: CallableFunction) + setOptions(options: {[key: string]: string}, callback: CallableFunction) { - this.getContainer()?.replaceChildren(...options.map((option: string) => + this.getContainer()?.replaceChildren(...Object.keys(options).map((option: string, idx: number) => { var button = document.createElement("button"); - button.innerText = option; + button.innerHTML = options[option]; button.addEventListener("click", () => callback(option)); return (button); })); diff --git a/client/src/panels/mouseinfopanel.ts b/client/src/panels/mouseinfopanel.ts index 1e66e3aa..a21c67bd 100644 --- a/client/src/panels/mouseinfopanel.ts +++ b/client/src/panels/mouseinfopanel.ts @@ -18,7 +18,7 @@ export class MouseInfoPanel extends Panel { this.#measureMarker = new Marker([0, 0], {icon: this.#measureIcon, interactive: false}); this.#measureBox = document.createElement("div"); - this.#measureBox.classList.add("ol-measure-box"); + this.#measureBox.classList.add("ol-measure-box", "hide"); document.body.appendChild(this.#measureBox); getMap()?.on("click", (e: any) => this.#onMapClick(e)); diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index f3ac1957..e520eb45 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -111,6 +111,9 @@ export class UnitControlPanel extends Panel { update() { var units = getUnitsManager().getSelectedUnits(); + + this.getElement().querySelector("#advanced-settings-div")?.classList.toggle("hide", units.length != 1); + if (this.getElement() != null && units.length > 0) { this.#showFlightControlSliders(units); @@ -123,8 +126,6 @@ export class UnitControlPanel extends Panel { else database = null; // TODO add databases for other unit types - console.log(unit.getBaseData()); - var button = document.createElement("button"); var callsign = unit.getBaseData().unitName || ""; @@ -202,19 +203,20 @@ export class UnitControlPanel extends Panel { #updateAdvancedSettingsDialog(units: Unit[]) { - this.getElement().querySelector("#advanced-settings-div")?.classList.toggle("hide", units.length != 1); - if (units.length == 1) { const unit = units[0]; (this.#advancedSettingsDialog.querySelector("#unit-name")).innerText = unit.getBaseData().unitName; - if (getUnitsManager().getSelectedUnits().length == 1){ - + if (getUnitsManager().getSelectedUnits().length == 1) + { + var asd = this.#advancedSettingsDialog; this.#radioCallsignDropdown.setOptions(["Enfield", "Springfield", "Uzi", "Colt", "Dodge", "Ford", "Chevy", "Pontiac"]); this.#radioCallsignDropdown.selectValue(unit.getTaskData().radioCallsign); - this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input")?.setAttribute('checked', String(unit.getTaskData().isTanker)); - this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input")?.setAttribute('checked', String(unit.getTaskData().isAWACS)); + var tankerCheckbox = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input") + if (tankerCheckbox) tankerCheckbox.checked = unit.getTaskData().isTanker; + var AWACSCheckbox = this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input") + if (AWACSCheckbox) AWACSCheckbox.checked = unit.getTaskData().isAWACS; var roles = aircraftDatabase.getByName(unit.getBaseData().name)?.loadouts.map((loadout) => {return loadout.roles}) if (roles != undefined && Array.prototype.concat.apply([], roles)?.includes("Tanker")){ @@ -240,7 +242,7 @@ export class UnitControlPanel extends Panel { #applyAdvancedSettings() { const isTanker = this.#advancedSettingsDialog.querySelector("#tanker-checkbox")?.querySelector("input")?.checked? true: false; - const isAWACS = false; //this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input")?.checked? true: false; + const isAWACS = this.#advancedSettingsDialog.querySelector("#AWACS-checkbox")?.querySelector("input")?.checked? true: false; const TACANChannel = Number(this.#advancedSettingsDialog.querySelector("#TACAN-channel")?.querySelector("input")?.value); const TACANXY = this.#TACANXYDropdown.getValue(); const TACANCallsign = this.#advancedSettingsDialog.querySelector("#tacan-callsign")?.querySelector("input")?.value diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 48e89148..1bbe50cf 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -117,6 +117,12 @@ export function attackUnit(ID: number, targetID: number) { POST(data, () => { }); } +export function followUnit(ID: number, targetID: number) { + var command = { "ID": ID, "targetID": targetID }; + var data = { "followUnit": command } + POST(data, () => { }); +} + export function cloneUnit(ID: number, latlng: L.LatLng) { var command = { "ID": ID, "location": latlng }; var data = { "cloneUnit": command } diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index 35836b16..e570c0ec 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,7 +1,7 @@ import { Marker, LatLng, Polyline, Icon, DivIcon } 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 } from '../server/server'; +import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit } from '../server/server'; import { aircraftDatabase } from './aircraftdatabase'; import { groundUnitsDatabase } from './groundunitsdatabase'; @@ -46,19 +46,21 @@ export class Unit extends Marker { wingmenIDs: [], }, taskData: { + currentState: "IDLE", currentTask: "", activePath: {}, targetSpeed: 0, targetAltitude: 0, isTanker: false, isAWACS: false, - radioOn: false, TACANOn: false, - radioFrequency: 0, - radioCallsign: 0, TACANChannel: 0, TACANXY: "X", TACANCallsign: "", + radioFrequency: 0, + radioCallsign: 0, + radioCallsignNumber: 0, + radioAMFM: "AM" }, optionsData: { ROE: "", @@ -336,6 +338,16 @@ export class Unit extends Marker { } } + followUnit(targetID: number) { + /* Call DCS attackUnit function */ + if (this.ID != targetID) { + followUnit(this.ID, targetID); + } + else { + // TODO: show a message + } + } + landAt(latlng: LatLng) { landAt(this.ID, latlng); } @@ -400,22 +412,23 @@ export class Unit extends Marker { } #onContextMenu(e: any) { - var options: string[] = []; + var options: {[key: string]: string} = {}; if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().includes(this))) { - options = [ - 'Attack' - ] + options = { + 'Attack': `Attack`, + 'Follow': `Follow` + } } - else if (getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) + else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { if (this.getBaseData().category == "Aircraft") { - options.push("Refuel"); // TODO Add some way of knowing which aircraft can AAR + options["Refuel"] = `Refuel`; // TODO Add some way of knowing which aircraft can AAR } } - if (options.length > 0) + if (Object.keys(options).length > 0) { getMap().showUnitContextMenu(e); getMap().getUnitContextMenu().setOptions(options, (option: string) => { @@ -430,6 +443,8 @@ export class Unit extends Marker { getUnitsManager().selectedUnitsAttackUnit(this.ID); if (action === "Refuel") getUnitsManager().selectedUnitsRefuel(); + if (action === "Follow") + getUnitsManager().selectedUnitsFollowUnit(this.ID); } #updateMarker() { diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 4fc63a34..3e861717 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -334,6 +334,14 @@ export class UnitsManager { } } + selectedUnitsFollowUnit(ID: number) { + var selectedUnits = this.getSelectedUnits(); + for (let idx in selectedUnits) { + var commandedUnit = selectedUnits[idx]; + commandedUnit.followUnit(ID); + } + } + copyUnits() { this.#copiedUnits = this.getSelectedUnits(); @@ -370,7 +378,7 @@ export class UnitsManager { setTimeout(() => { document.dispatchEvent(new CustomEvent("unitsSelection", {detail: this.getSelectedUnits()})); this.#selectionEventDisabled = false; - }, 300); + }, 100); this.#selectionEventDisabled = true; } } diff --git a/client/views/unitcontrolpanel.ejs b/client/views/unitcontrolpanel.ejs index 55e92a2c..40629d4d 100644 --- a/client/views/unitcontrolpanel.ejs +++ b/client/views/unitcontrolpanel.ejs @@ -72,91 +72,81 @@ - - + - - - - Operate as tanker - - + + + + Operate as AAR tanker + + - - - - Operate as AWACS - - - + + + + Operate as AWACS + + + + + A/A TACAN: + - - - A/A TACAN: - - - - + X - - Morse: - - + + + + Radio frequency: - - - Radio frequency: - - - @@ -167,12 +157,12 @@ + + + + Radio callsign: - - Radio callsign: - - @@ -186,9 +176,8 @@ - - +