diff --git a/client/demo.js b/client/demo.js index c30f7f7b..2c113e9f 100644 --- a/client/demo.js +++ b/client/demo.js @@ -2,7 +2,7 @@ const DEMO_UNIT_DATA = { ["1"]:{ baseData: { - AI: true, + AI: false, name: "KC-135", unitName: "Olympus 1-1", groupName: "Group 1", @@ -18,7 +18,7 @@ const DEMO_UNIT_DATA = { }, missionData: { fuel: 50, - flags: {human: true}, + flags: {Human: false}, ammo: [ { count: 4, @@ -47,6 +47,7 @@ const DEMO_UNIT_DATA = { }, taskData: { currentTask: "Holding", + currentState: "Idle", activePath: undefined, targetSpeed: 400, targetAltitude: 3000, @@ -67,7 +68,7 @@ const DEMO_UNIT_DATA = { }, ["2"]:{ baseData: { - AI: false, + AI: true, name: "KC-135", unitName: "Olympus 1-2", groupName: "Group 1", diff --git a/client/public/images/airbase.png b/client/public/images/airbase.png deleted file mode 100644 index 874174d6..00000000 Binary files a/client/public/images/airbase.png and /dev/null differ diff --git a/client/public/stylesheets/units.css b/client/public/stylesheets/units.css index 0685381f..ddd1bd77 100644 --- a/client/public/stylesheets/units.css +++ b/client/public/stylesheets/units.css @@ -392,8 +392,8 @@ padding: var( --unit-aircraft-ammo-radius ); } - [data-object|="unit"] .unit-summary { + pointer-events: none; column-gap: 6px; color:white; display:flex; @@ -438,14 +438,14 @@ -[data-object|="unit"][data-pilot|="ai"]:hover .unit-ammo, -[data-object|="unit"][data-pilot|="ai"]:hover .unit-fuel { +[data-object|="unit"]:hover .unit-ammo, +[data-object|="unit"]:hover .unit-fuel { display:flex; } [data-object|="unit"][data-is-in-hotgroup] .unit-hotgroup, -[data-object|="unit"][data-pilot|="ai"][data-is-selected] .unit-ammo, -[data-object|="unit"][data-pilot|="ai"][data-is-selected] .unit-fuel, +[data-object|="unit"][data-is-selected] .unit-ammo, +[data-object|="unit"][data-is-selected] .unit-fuel, [data-object|="unit"][data-is-selected] .unit-selected-spotlight { display:flex; } @@ -501,7 +501,7 @@ } } -[data-object|="unit"][data-pilot|="ai"][data-has-low-fuel] .unit-fuel { +[data-object|="unit"][data-has-low-fuel] .unit-fuel { animation: pulse 1.5s linear infinite; } diff --git a/client/public/themes/olympus/images/icon_airbase_blue.svg b/client/public/themes/olympus/images/icon_airbase_blue.svg index 0800974c..d1fcf84c 100644 --- a/client/public/themes/olympus/images/icon_airbase_blue.svg +++ b/client/public/themes/olympus/images/icon_airbase_blue.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icon_airbase_neutral.svg b/client/public/themes/olympus/images/icon_airbase_neutral.svg index 69713bd5..43222171 100644 --- a/client/public/themes/olympus/images/icon_airbase_neutral.svg +++ b/client/public/themes/olympus/images/icon_airbase_neutral.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/icon_airbase_red.svg b/client/public/themes/olympus/images/icon_airbase_red.svg index 45d55abd..d95872f1 100644 --- a/client/public/themes/olympus/images/icon_airbase_red.svg +++ b/client/public/themes/olympus/images/icon_airbase_red.svg @@ -1,10 +1,83 @@ - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + diff --git a/client/public/themes/olympus/images/task_tanker.svg b/client/public/themes/olympus/images/task_tanker.svg new file mode 100644 index 00000000..32ee5980 --- /dev/null +++ b/client/public/themes/olympus/images/task_tanker.svg @@ -0,0 +1,1256 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index b8ad84b2..fcb02761 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -71,6 +71,7 @@ export class Unit extends Marker { #selectable: boolean; #selected: boolean = false; + #hovered: boolean = false; #hidden: boolean = false; #preventClick: boolean = false; @@ -81,7 +82,6 @@ export class Unit extends Marker { #miniMapMarker: CircleMarker | null = null; #timer: number = 0; - #forceUpdate: boolean = false; static getConstructor(type: string) { if (type === "GroundUnit") return GroundUnit; @@ -102,6 +102,8 @@ export class Unit extends Marker { this.on('click', (e) => this.#onClick(e)); this.on('dblclick', (e) => this.#onDoubleClick(e)); this.on('contextmenu', (e) => this.#onContextMenu(e)); + this.on('mouseover', () => { this.#hovered = true;}) + this.on('mouseout', () => { this.#hovered = false;}) this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 }); this.#pathPolyline.addTo(getMap()); @@ -123,7 +125,8 @@ export class Unit extends Marker { var icon = new DivIcon({ html: this.getMarkerHTML(), className: 'leaflet-unit-marker', - iconAnchor: [0, 0] + iconAnchor: [25, 0], + iconSize: [50, 50], }); this.setIcon(icon); } @@ -143,13 +146,11 @@ export class Unit extends Marker { } setData(data: UpdateData) { - var updateMarker = false; - - if ((data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude)) - || (data.flightData.heading != undefined && this.getFlightData().heading != data.flightData.heading) - || (data.baseData.alive != undefined && this.getBaseData().alive != data.baseData.alive) - || this.#forceUpdate || !getMap().hasLayer(this)) - updateMarker = true; + /* Check if data has changed comparing new values to old values */ + const positionChanged = (data.flightData.latitude != undefined && data.flightData.longitude != undefined && (this.getFlightData().latitude != data.flightData.latitude || this.getFlightData().longitude != data.flightData.longitude)); + const headingChanged = (data.flightData.heading != undefined && this.getFlightData().heading != data.flightData.heading); + const aliveChanged = (data.baseData.alive != undefined && this.getBaseData().alive != data.baseData.alive); + var updateMarker = (positionChanged || headingChanged || aliveChanged || !getMap().hasLayer(this)) if (data.baseData != undefined) { @@ -331,21 +332,17 @@ export class Unit extends Marker { } attackUnit(targetID: number) { + /* Units can't attack themselves */ if (this.ID != targetID) { attackUnit(this.ID, targetID); } - else { - // TODO: show a message - } } followUnit(targetID: number, offset: {"x": number, "y": number, "z": number}) { + /* Units can't follow themselves */ if (this.ID != targetID) { followUnit(this.ID, targetID, offset); } - else { - // TODO: show a message - } } landAt(latlng: LatLng) { @@ -399,7 +396,7 @@ export class Unit extends Marker { if (!e.originalEvent.ctrlKey) { getUnitsManager().deselectAllUnits(); } - this.setSelected(true); + this.setSelected(!this.getSelected()); } } this.#preventClick = false; @@ -416,12 +413,11 @@ export class Unit extends Marker { options["Center"] = `
Center map
`; - if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().includes(this))) + if (getUnitsManager().getSelectedUnits().length > 0 && !(getUnitsManager().getSelectedUnits().length == 1 && (getUnitsManager().getSelectedUnits().includes(this)))) { - options = { - 'Attack': `
Attack
`, - 'Follow': `
Follow
`, - } + options['Attack'] = `
Attack
`; + if (getUnitsManager().getSelectedUnitsType() === "Aircraft") + options['Follow'] = `
Follow
`; } else if ((getUnitsManager().getSelectedUnits().length > 0 && (getUnitsManager().getSelectedUnits().includes(this))) || getUnitsManager().getSelectedUnits().length == 0) { @@ -487,30 +483,12 @@ export class Unit extends Marker { // Z: left-right, positive right var offset = {"x": 0, "y": 0, "z": 0}; - if (action == "Trail") - { - offset.x = -50; offset.y = -30; offset.z = 0; - } - else if (action == "Echelon (LH)") - { - offset.x = -50; offset.y = -10; offset.z = -50; - } - else if (action == "Echelon (RH)") - { - offset.x = -50; offset.y = -10; offset.z = 50; - } - else if (action == "Line abreast (RH)") - { - offset.x = 0; offset.y = 0; offset.z = 50; - } - else if (action == "Line abreast (LH)") - { - offset.x = 0; offset.y = 0; offset.z = -50; - } - else if (action == "Front") - { - offset.x = 100; offset.y = 0; offset.z = 0; - } + if (action == "Trail") { offset.x = -50; offset.y = -30; offset.z = 0; } + else if (action == "Echelon (LH)") { offset.x = -50; offset.y = -10; offset.z = -50; } + else if (action == "Echelon (RH)") { offset.x = -50; offset.y = -10; offset.z = 50; } + else if (action == "Line abreast (RH)") { offset.x = 0; offset.y = 0; offset.z = 50; } + else if (action == "Line abreast (LH)") { offset.x = 0; offset.y = 0; offset.z = -50; } + else if (action == "Front") { offset.x = 100; offset.y = 0; offset.z = 0; } getUnitsManager().selectedUnitsFollowUnit(this.ID, offset); } } @@ -518,6 +496,7 @@ export class Unit extends Marker { #updateMarker() { this.updateVisibility(); + /* Draw the minimap marker */ if (this.getBaseData().alive ) { if (this.#miniMapMarker == null) @@ -544,47 +523,47 @@ export class Unit extends Marker { } } + /* Draw the marker */ if (!this.getHidden()) { this.setLatLng(new LatLng(this.getFlightData().latitude, this.getFlightData().longitude)); + var element = this.getElement(); if (element != null) { + /* Draw the velocity vector */ element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.getFlightData().speed / 5}px;`); - element.querySelector(".unit")?.setAttribute("data-pilot", this.getMissionData().flags.human? "human": "ai"); + /* Set fuel data */ element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.getMissionData().fuel}%`); element.querySelector(".unit")?.toggleAttribute("data-has-low-fuel", this.getMissionData().fuel < 20); + /* Set dead/alive flag */ element.querySelector(".unit")?.toggleAttribute("data-is-dead", !this.getBaseData().alive); - if (this.getMissionData().flags.Human) // Unit is human + /* Set current unit state */ + if (this.getMissionData().flags.Human) // Unit is human element.querySelector(".unit")?.setAttribute("data-state", "human"); - else if (!this.getBaseData().AI) // Unit is under DCS control (no Olympus) + else if (!this.getBaseData().AI) // Unit is under DCS control (not Olympus) element.querySelector(".unit")?.setAttribute("data-state", "dcs"); - else // Unit is under Olympus control + else // Unit is under Olympus control element.querySelector(".unit")?.setAttribute("data-state", this.getTaskData().currentState.toLowerCase()); - var unitAltitudeDiv = element.querySelector(".unit-altitude"); - if (unitAltitudeDiv != null) - unitAltitudeDiv.innerHTML = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 1000)); - - var unitSpeedDiv = element.querySelector(".unit-speed"); - if (unitSpeedDiv != null) - unitSpeedDiv.innerHTML = String(Math.floor(this.getFlightData().speed * 1.94384 ) ); + /* Set altitude and speed */ + if (element.querySelector(".unit-altitude")) + ( element.querySelector(".unit-altitude")).innerText = "FL" + String(Math.floor(this.getFlightData().altitude / 0.3048 / 1000)); + if (element.querySelector(".unit-speed")) + ( element.querySelector(".unit-speed")).innerHTML = String(Math.floor(this.getFlightData().speed * 1.94384 ) ); + /* Rotate elements according to heading */ element.querySelectorAll( "[data-rotate-to-heading]" ).forEach( el => { const headingDeg = rad2deg( this.getFlightData().heading ); let currentStyle = el.getAttribute( "style" ) || ""; el.setAttribute( "style", currentStyle + `transform:rotate(${headingDeg}deg);` ); }); - - - } + /* Set vertical offset for altitude stacking */ var pos = getMap().latLngToLayerPoint(this.getLatLng()).round(); - this.setZIndexOffset(1000 + Math.floor(this.getFlightData().altitude) - pos.y); + this.setZIndexOffset(1000 + Math.floor(this.getFlightData().altitude) - pos.y + (this.#hovered || this.#selected? 5000: 0)); } - - this.#forceUpdate = false; } #drawPath() { @@ -643,7 +622,7 @@ export class Unit extends Marker { color = "#00FF00"; else color = "#FFFFFF"; - var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 1, smoothFactor: 1 }); + var targetPolyline = new Polyline([startLatLng, endLatLng], { color: color, weight: 3, opacity: 0.4, smoothFactor: 1 }); targetPolyline.addTo(getMap()); this.#targetsPolylines.push(targetPolyline) } diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index 7c6086d5..c4b67c54 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -191,8 +191,17 @@ export class UnitsManager { selectedUnitsAddDestination(latlng: L.LatLng) { var selectedUnits = this.getSelectedUnits(); for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - commandedUnit.addDestination(latlng); + const unit = selectedUnits[idx]; + if (unit.getTaskData().currentState === "Follow") + { + const leader = this.getUnitByID(unit.getFormationData().leaderID) + if (leader && leader.getSelected()) + leader.addDestination(latlng); + else + unit.addDestination(latlng); + } + else + unit.addDestination(latlng); } this.#showActionMessage(selectedUnits, " new destination added"); } @@ -200,8 +209,17 @@ export class UnitsManager { selectedUnitsClearDestinations() { var selectedUnits = this.getSelectedUnits(); for (let idx in selectedUnits) { - var commandedUnit = selectedUnits[idx]; - commandedUnit.clearDestinations(); + const unit = selectedUnits[idx]; + if (unit.getTaskData().currentState === "Follow") + { + const leader = this.getUnitByID(unit.getFormationData().leaderID) + if (leader && leader.getSelected()) + leader.clearDestinations(); + else + unit.clearDestinations(); + } + else + unit.clearDestinations(); } }