diff --git a/frontend/react/public/images/markers/navpoint-tgt.svg b/frontend/react/public/images/markers/navpoint-tgt.svg new file mode 100644 index 00000000..7f7cd846 --- /dev/null +++ b/frontend/react/public/images/markers/navpoint-tgt.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/react/public/images/markers/navpoint.svg b/frontend/react/public/images/markers/navpoint.svg new file mode 100644 index 00000000..b9075e11 --- /dev/null +++ b/frontend/react/public/images/markers/navpoint.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/react/public/images/training/step14.png b/frontend/react/public/images/training/step14.png new file mode 100644 index 00000000..e2e5b15b Binary files /dev/null and b/frontend/react/public/images/training/step14.png differ diff --git a/frontend/react/public/images/training/unitmarker1.png b/frontend/react/public/images/training/unitmarker1.png new file mode 100644 index 00000000..e55116c5 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker1.png differ diff --git a/frontend/react/public/images/training/unitmarker2.png b/frontend/react/public/images/training/unitmarker2.png new file mode 100644 index 00000000..932648d2 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker2.png differ diff --git a/frontend/react/public/images/training/unitmarker3.png b/frontend/react/public/images/training/unitmarker3.png new file mode 100644 index 00000000..2aecb726 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker3.png differ diff --git a/frontend/react/public/images/training/unitmarker4.png b/frontend/react/public/images/training/unitmarker4.png new file mode 100644 index 00000000..99f91a1d Binary files /dev/null and b/frontend/react/public/images/training/unitmarker4.png differ diff --git a/frontend/react/public/images/training/unitmarker5.png b/frontend/react/public/images/training/unitmarker5.png new file mode 100644 index 00000000..89390ec0 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker5.png differ diff --git a/frontend/react/public/images/training/unitmarker6.png b/frontend/react/public/images/training/unitmarker6.png new file mode 100644 index 00000000..3f3d3740 Binary files /dev/null and b/frontend/react/public/images/training/unitmarker6.png differ diff --git a/frontend/react/public/images/training/unitmarker7.png b/frontend/react/public/images/training/unitmarker7.png new file mode 100644 index 00000000..5979783d Binary files /dev/null and b/frontend/react/public/images/training/unitmarker7.png differ diff --git a/frontend/react/src/audio/audiomanager.ts b/frontend/react/src/audio/audiomanager.ts index d5b092ed..f11b4834 100644 --- a/frontend/react/src/audio/audiomanager.ts +++ b/frontend/react/src/audio/audiomanager.ts @@ -2,7 +2,7 @@ import { AudioManagerState, AudioMessageType, BLUE_COMMANDER, GAME_MASTER, Olymp import { MicrophoneSource } from "./microphonesource"; import { RadioSink } from "./radiosink"; import { getApp } from "../olympusapp"; -import { coalitionToEnum, makeID } from "../other/utils"; +import { coalitionToEnum, deepCopyTable, makeID } from "../other/utils"; import { FileSource } from "./filesource"; import { AudioSource } from "./audiosource"; import { Buffer } from "buffer"; @@ -465,7 +465,9 @@ export class AudioManager { if (this.#devices.includes(input)) { this.#input = input; AudioManagerInputChangedEvent.dispatch(input); + let sessionData = deepCopyTable(getApp().getSessionDataManager().getSessionData()); this.stop(); + getApp().getSessionDataManager().setSessionData(sessionData); this.start(); this.#options.input = input.deviceId; AudioOptionsChangedEvent.dispatch(this.#options); @@ -478,8 +480,11 @@ export class AudioManager { if (this.#devices.includes(output)) { this.#input = output; AudioManagerOutputChangedEvent.dispatch(output); + let sessionData = deepCopyTable(getApp().getSessionDataManager().getSessionData()); this.stop(); + getApp().getSessionDataManager().setSessionData(sessionData); this.start(); + this.#options.output = output.deviceId; AudioOptionsChangedEvent.dispatch(this.#options); } else { diff --git a/frontend/react/src/map/drawings/drawingsmanager.ts b/frontend/react/src/map/drawings/drawingsmanager.ts index 108bda5e..48f9345e 100644 --- a/frontend/react/src/map/drawings/drawingsmanager.ts +++ b/frontend/react/src/map/drawings/drawingsmanager.ts @@ -1,9 +1,10 @@ import { decimalToRGBA } from "../../other/utils"; import { getApp } from "../../olympusapp"; -import { DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events"; +import { CommandModeOptionsChangedEvent, DrawingsInitEvent, DrawingsUpdatedEvent, MapOptionsChangedEvent, SessionDataLoadedEvent } from "../../events"; import { MapOptions } from "../../types/types"; import { Circle, DivIcon, Layer, LayerGroup, layerGroup, Marker, Polygon, Polyline } from "leaflet"; import { NavpointMarker } from "../markers/navpointmarker"; +import { BLUE_COMMANDER, GAME_MASTER, NONE, RED_COMMANDER } from "../../constants/constants"; export abstract class DCSDrawing { #name: string; @@ -459,7 +460,7 @@ export class DCSNavpoint extends DCSDrawing { constructor(drawingData, parent) { super(drawingData, parent); - this.#point = new NavpointMarker([drawingData.lat, drawingData.lng], drawingData.callsignStr, drawingData.comment); + this.#point = new NavpointMarker([drawingData.lat, drawingData.lng], drawingData.callsignStr, drawingData.comment, drawingData.tag); this.setVisibility(true); } @@ -527,7 +528,9 @@ export class DCSDrawingsContainer { initFromData(drawingsData) { let hasContainers = false; Object.keys(drawingsData).forEach((layerName: string) => { - if (drawingsData[layerName]["name"] === undefined && drawingsData[layerName]["callsignStr"] === undefined) { + const layerIsAContainer = drawingsData[layerName]["name"] === undefined && drawingsData[layerName]["callsignStr"] === undefined; + const layerIsNotEmpty = Object.keys(drawingsData[layerName]).length > 0; + if (layerIsAContainer && layerIsNotEmpty) { const newContainer = new DCSDrawingsContainer(layerName, this); this.addSubContainer(newContainer); newContainer.initFromData(drawingsData[layerName]); @@ -579,15 +582,12 @@ export class DCSDrawingsContainer { else this.addDrawing(newDrawing); }); - if (othersContainer.getDrawings().length === 0) this.removeSubContainer(othersContainer); // Remove empty container + if (othersContainer.getDrawings().length === 0) { + this.removeSubContainer(othersContainer); // Remove empty container + // FIXME: it's not working for main containers. + } } - // initNavpoints(drawingsData) { - // const newContainer = new DCSDrawingsContainer('Navpoints', this); - // this.addSubContainer(newContainer); - // newContainer.initFromData(drawingsData); - // } - getLayerGroup() { return this.#layerGroup; } @@ -705,6 +705,7 @@ export class DrawingsManager { #sessionDataDrawings = {}; #sessionDataNavpoints = {}; #initialized: boolean = false; + #hiddenContainers: Record = {}; constructor() { const drawingsLayerGroup = new LayerGroup(); @@ -727,6 +728,13 @@ export class DrawingsManager { this.#drawingsContainer.setVisibility(getApp().getMap().getOptions().showMissionDrawings); this.#navpointsContainer.setVisibility(getApp().getMap().getOptions().showMissionNavpoints); }); + + CommandModeOptionsChangedEvent.on((commandOptions) => { + if (commandOptions.commandMode !== GAME_MASTER) { + this.restrictCoalitionLayers(commandOptions.commandMode) + } + this.restoreHiddenLayers(commandOptions.commandMode); + }) } initDrawings(data: { drawings: Record> }): boolean { @@ -747,6 +755,49 @@ export class DrawingsManager { } } + private restrictContainer(containerName: string, targetContainer: any, hiddenKey: string) { + const container = targetContainer.getSubContainers().find(c => c.getName().toLowerCase() === containerName.toLowerCase()); + if (container) { + this.#hiddenContainers[hiddenKey] = { + parent: container['#parent'], + container: container + }; + container.setVisibility(false); + targetContainer.removeSubContainer(container); + } + } + + restrictCoalitionLayers(playerRole: string) { + if (playerRole === RED_COMMANDER) { + this.restrictContainer('Blue', this.#drawingsContainer, 'blue_drawings'); + this.restrictContainer('blue', this.#navpointsContainer, 'blue_navpoints'); + } else { + this.restrictContainer('Red', this.#drawingsContainer, 'red_drawings'); + this.restrictContainer('red', this.#navpointsContainer, 'red_navpoints'); + } + } + + private restoreContainer(key: string, targetContainer: any) { + if (this.#hiddenContainers[key]) { + const container = this.#hiddenContainers[key].container; + targetContainer.addSubContainer(container); + container.setVisibility(true); + } + } + + restoreHiddenLayers(playerRole: string) { + const roleContainers: Record = { + [RED_COMMANDER]: ['red_drawings', 'red_navpoints'], + [BLUE_COMMANDER]: ['blue_drawings', 'blue_navpoints'], + [GAME_MASTER]: ['red_drawings', 'red_navpoints', 'blue_drawings', 'blue_navpoints'] + }; + + roleContainers[playerRole]?.forEach((key) => { + const targetContainer = key.includes('drawings') ? this.#drawingsContainer : this.#navpointsContainer; + this.restoreContainer(key, targetContainer); + }); + } + getDrawingsContainer() { return this.#drawingsContainer; } diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts index 3a3870de..4880fa7c 100644 --- a/frontend/react/src/map/map.ts +++ b/frontend/react/src/map/map.ts @@ -1070,7 +1070,7 @@ export class Map extends L.Map { if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", e.latlng); else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", e.latlng); else if (this.#effectRequestTable.explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", e.latlng); - + else if (this.#effectRequestTable.explosionType === "Fire") getApp().getServerManager().spawnExplosion(50, "fire", e.latlng); this.addExplosionMarker(e.latlng); } else if (this.#effectRequestTable.type === "smoke") { getApp() diff --git a/frontend/react/src/map/markers/navpointmarker.ts b/frontend/react/src/map/markers/navpointmarker.ts index f7092794..2ffba311 100644 --- a/frontend/react/src/map/markers/navpointmarker.ts +++ b/frontend/react/src/map/markers/navpointmarker.ts @@ -1,14 +1,35 @@ import { DivIcon, LatLngExpression, MarkerOptions } from "leaflet"; import { CustomMarker } from "./custommarker"; +import { SVGInjector } from "@tanem/svg-injector"; export class NavpointMarker extends CustomMarker { #callsignStr: string; #comment: string; + #tag: string; - constructor(latlng: LatLngExpression, callsignStr: string, comment?: string) { + constructor(latlng: LatLngExpression, callsignStr: string, comment: string, tag: string) { super(latlng, { interactive: false, draggable: false }); this.#callsignStr = callsignStr; comment ? this.#comment = comment : null; + tag ? this.#tag = tag : null; + } + + private getImage() { + switch (this.#tag) { + case 'TGT': + return 'images/markers/navpoint-tgt.svg' + default: + return 'images/markers/navpoint.svg' + } + } + + private getSize() { + switch (this.#tag) { + case 'TGT': + return '20px' + default: + return '8px' + } } createIcon() { @@ -16,7 +37,7 @@ export class NavpointMarker extends CustomMarker { let icon = new DivIcon({ className: "leaflet-navpoint-icon", iconAnchor: [0, 0], - iconSize: [50, 50], + iconSize: [2, 2], }); this.setIcon(icon); @@ -26,6 +47,14 @@ export class NavpointMarker extends CustomMarker { // Main icon let pointIcon = document.createElement("div"); pointIcon.classList.add("navpoint-icon"); + var img = document.createElement("img"); + img.src = this.getImage(); + img.onload = () => { + SVGInjector(img); + }; + img.style.width = this.getSize(); + img.style.height = this.getSize(); + pointIcon.appendChild(img); el.append(pointIcon); // Label diff --git a/frontend/react/src/map/markers/stylesheets/navpoint.css b/frontend/react/src/map/markers/stylesheets/navpoint.css index a3e30eb9..52cd8082 100644 --- a/frontend/react/src/map/markers/stylesheets/navpoint.css +++ b/frontend/react/src/map/markers/stylesheets/navpoint.css @@ -5,11 +5,7 @@ gap: 10px; } .ol-navpoint-marker>.navpoint>.navpoint-icon { - height: 8px; - width: 8px; - background: white; - flex: none; - transform: rotate3d(0, 0, 1, 45deg); + filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 1)); } .ol-navpoint-marker>.navpoint>.navpoint-main-label { @@ -17,6 +13,8 @@ flex-direction: column; font-size: 10px; color: white; + text-shadow: -1px -1px 2px rgb(113, 113, 113), 1px -1px 2px rgb(113, 113, 113), -1px 1px 2px rgb(113, 113, 113), 1px 1px 2px rgb(113, 113, 113); + } .ol-navpoint-marker .navpoint-comment-box { @@ -24,4 +22,6 @@ font-style: italic; color: white; max-width: 50px; + text-shadow: -1px -1px 2px rgb(113, 113, 113), 1px -1px 2px rgb(113, 113, 113), -1px 1px 2px rgb(113, 113, 113), 1px 1px 2px rgb(113, 113, 113); + } \ No newline at end of file diff --git a/frontend/react/src/map/markers/temporaryunitmarker.ts b/frontend/react/src/map/markers/temporaryunitmarker.ts index 7d093645..3b310a81 100644 --- a/frontend/react/src/map/markers/temporaryunitmarker.ts +++ b/frontend/react/src/map/markers/temporaryunitmarker.ts @@ -1,10 +1,12 @@ import { CustomMarker } from "./custommarker"; -import { DivIcon, LatLng } from "leaflet"; +import { DivIcon, LatLng, LatLngExpression } from "leaflet"; import { SVGInjector } from "@tanem/svg-injector"; import { getApp } from "../../olympusapp"; -import { UnitBlueprint } from "../../interfaces"; -import { deg2rad, normalizeAngle, rad2deg } from "../../other/utils"; +import { adjustBrightness, normalizeAngle, rad2deg } from "../../other/utils"; import { SpawnHeadingChangedEvent } from "../../events"; +import { RangeCircle } from "../rangecircle"; +import { Map } from "../map"; +import { colors } from "../../constants/constants"; export class TemporaryUnitMarker extends CustomMarker { #name: string; @@ -12,6 +14,8 @@ export class TemporaryUnitMarker extends CustomMarker { #commandHash: string | undefined = undefined; #timer: number = 0; #headingHandle: boolean; + #acquisitionCircle: RangeCircle | undefined = undefined; + #engagementCircle: RangeCircle | undefined = undefined; constructor(latlng: LatLng, name: string, coalition: string, headingHandle: boolean, commandHash?: string) { super(latlng, { interactive: false }); @@ -39,6 +43,14 @@ export class TemporaryUnitMarker extends CustomMarker { }, 1000); } + setLatLng(latlng: LatLngExpression): this { + super.setLatLng(latlng); + if (this.#acquisitionCircle) this.#acquisitionCircle.setLatLng(latlng); + if (this.#engagementCircle) this.#engagementCircle.setLatLng(latlng); + + return this; + } + createIcon() { const blueprint = getApp().getUnitsManager().getDatabase().getByName(this.#name); @@ -51,6 +63,58 @@ export class TemporaryUnitMarker extends CustomMarker { }); this.setIcon(icon); + if (blueprint.acquisitionRange) { + this.#acquisitionCircle = new RangeCircle(this.getLatLng(), { + radius: blueprint.acquisitionRange, + weight: 2, + opacity: 1, + fillOpacity: 0, + dashArray: "8 12", + interactive: false, + bubblingMouseEvents: false, + }); + + switch (this.#coalition) { + case "red": + this.#acquisitionCircle.options.color = adjustBrightness(colors.RED_COALITION, -20); + break; + case "blue": + this.#acquisitionCircle.options.color = adjustBrightness(colors.BLUE_COALITION, -20); + break; + default: + this.#acquisitionCircle.options.color = adjustBrightness(colors.NEUTRAL_COALITION, -20); + break; + } + + getApp().getMap().addLayer(this.#acquisitionCircle); + } + + if (blueprint.engagementRange) { + this.#engagementCircle = new RangeCircle(this.getLatLng(), { + radius: blueprint.engagementRange, + weight: 4, + opacity: 1, + fillOpacity: 0, + dashArray: "4 8", + interactive: false, + bubblingMouseEvents: false, + }); + + switch (this.#coalition) { + case "red": + this.#engagementCircle.options.color = colors.RED_COALITION; + break; + case "blue": + this.#engagementCircle.options.color = colors.BLUE_COALITION + break; + default: + this.#engagementCircle.options.color = colors.NEUTRAL_COALITION; + break; + } + + getApp().getMap().addLayer(this.#engagementCircle); + } + var el = document.createElement("div"); el.classList.add("unit"); el.setAttribute("data-object", `unit-${blueprint.category}`); @@ -89,8 +153,7 @@ export class TemporaryUnitMarker extends CustomMarker { const rotateHandle = (heading) => { el.style.transform = `rotate(${heading}deg)`; unitIcon.style.transform = `rotate(-${heading}deg)`; - if (shortLabel) - shortLabel.style.transform = `rotate(-${heading}deg)`; + if (shortLabel) shortLabel.style.transform = `rotate(-${heading}deg)`; }; SpawnHeadingChangedEvent.on((heading) => rotateHandle(heading)); @@ -124,4 +187,13 @@ export class TemporaryUnitMarker extends CustomMarker { this.getElement()?.classList.add("ol-temporary-marker"); } } + + onRemove(map: Map): this { + super.onRemove(map); + + if (this.#acquisitionCircle) map.removeLayer(this.#acquisitionCircle); + if (this.#engagementCircle) map.removeLayer(this.#engagementCircle); + + return this; + } } diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts index 68812ceb..190696f7 100644 --- a/frontend/react/src/mission/missionmanager.ts +++ b/frontend/react/src/mission/missionmanager.ts @@ -291,6 +291,17 @@ export class MissionManager { this.setSpentSpawnPoints(0); this.refreshSpawnPoints(); + if (commandModeOptions.commandMode === BLUE_COMMANDER && getApp().getMap().getOptions().AWACSCoalition !== "blue") { + getApp() + .getMap() + .setOption("AWACSCoalition", "blue" as Coalition); + } + else if (commandModeOptions.commandMode === RED_COMMANDER && getApp().getMap().getOptions().AWACSCoalition !== "red") { + getApp() + .getMap() + .setOption("AWACSCoalition", "red" as Coalition); + } + if (commandModeOptionsChanged) { CommandModeOptionsChangedEvent.dispatch(this.#commandModeOptions); } diff --git a/frontend/react/src/sessiondata.ts b/frontend/react/src/sessiondata.ts index d0d26479..763df95e 100644 --- a/frontend/react/src/sessiondata.ts +++ b/frontend/react/src/sessiondata.ts @@ -187,6 +187,12 @@ export class SessionDataManager { return this.#sessionData; } + setSessionData(sessionData: SessionData) { + this.#sessionData = sessionData; + + this.#saveSessionData(); + } + #saveSessionData() { if (getApp().getState() === OlympusState.SERVER) return; diff --git a/frontend/react/src/shortcut/shortcut.ts b/frontend/react/src/shortcut/shortcut.ts index 3949286e..73259b4c 100644 --- a/frontend/react/src/shortcut/shortcut.ts +++ b/frontend/react/src/shortcut/shortcut.ts @@ -1,5 +1,4 @@ -import { OlympusState } from "../constants/constants"; -import { AppStateChangedEvent, ModalEvent, ShortcutChangedEvent, ShortcutsChangedEvent } from "../events"; +import { ModalEvent } from "../events"; import { ShortcutOptions } from "../interfaces"; import { keyEventWasInInput } from "../other/utils"; diff --git a/frontend/react/src/ui/components/olunitsummary.tsx b/frontend/react/src/ui/components/olunitsummary.tsx index e597c984..04ea6f6e 100644 --- a/frontend/react/src/ui/components/olunitsummary.tsx +++ b/frontend/react/src/ui/components/olunitsummary.tsx @@ -85,7 +85,7 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
{props.blueprint.abilities?.split(" ").map((ability) => { return ( - <> +
{ability.replaceAll(" ", "") !== "" && (
)} - +
); })} diff --git a/frontend/react/src/ui/modals/components/modal.tsx b/frontend/react/src/ui/modals/components/modal.tsx index c1aecb23..07ba7f8f 100644 --- a/frontend/react/src/ui/modals/components/modal.tsx +++ b/frontend/react/src/ui/modals/components/modal.tsx @@ -33,8 +33,8 @@ export function Modal(props: { ${ props.size === "lg" ? ` - h-[600px] w-[1100px] - max-md:h-full max-md:w-full + h-[700px] w-[1100px] + max-xl:h-full max-xl:w-full ` : "" } @@ -42,7 +42,7 @@ export function Modal(props: { props.size === "md" ? ` h-[600px] w-[950px] - max-md:h-full max-md:w-full + max-lg:h-full max-lg:w-full ` : "" } diff --git a/frontend/react/src/ui/modals/loginmodal.tsx b/frontend/react/src/ui/modals/loginmodal.tsx index 402d8607..5488d398 100644 --- a/frontend/react/src/ui/modals/loginmodal.tsx +++ b/frontend/react/src/ui/modals/loginmodal.tsx @@ -89,8 +89,8 @@ export function LoginModal(props: { open: boolean }) {
-
+
{loginByRole ? ( <>