Merge branch 'release-candidate' into features/quick-filter-rework-drawings-layers

This commit is contained in:
Pax1601 2025-04-05 20:31:20 +02:00 committed by GitHub
commit 22e37ceec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 570 additions and 163 deletions

View File

@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="48" y="10" width="4" height="80" fill="white"/>
<rect x="10" y="48" width="80" height="4" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@ -0,0 +1,3 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<polygon points="100,10 10,190 190,190" fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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 {

View File

@ -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<string, { parent: DCSDrawingsContainer, container: DCSDrawingsContainer }> = {};
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<string, Record<string, any>> }): 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<string, string[]> = {
[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;
}

View File

@ -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()

View File

@ -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

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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";

View File

@ -85,7 +85,7 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
<div className="flex flex-row gap-1 px-2">
{props.blueprint.abilities?.split(" ").map((ability) => {
return (
<>
<div key={ability}>
{ability.replaceAll(" ", "") !== "" && (
<div
key={ability}
@ -97,7 +97,7 @@ export function OlUnitSummary(props: { blueprint: UnitBlueprint; coalition: Coal
{ability}
</div>
)}
</>
</div>
);
})}

View File

@ -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
`
: ""
}

View File

@ -89,8 +89,8 @@ export function LoginModal(props: { open: boolean }) {
<Modal open={props.open} size="md" disableClose={true}>
<div
className={`
flex w-full flex-row gap-6
max-lg:flex-col
flex w-full flex-col gap-6
lg:flex-row
`}
>
<div
@ -177,7 +177,7 @@ export function LoginModal(props: { open: boolean }) {
</div>
</div>
<div className={`flex flex-col items-start gap-2`}>
<div className={`flex flex-col items-start gap-1`}>
{loginByRole ? (
<>
<label

View File

@ -9,6 +9,7 @@ const MAX_STEPS = 15;
export function TrainingModal(props: { open: boolean }) {
const [step, setStep] = useState(0);
const [markerComponent, setMarkerComponent] = useState(null as number | null);
return (
<Modal open={props.open} size="lg">
@ -43,10 +44,12 @@ export function TrainingModal(props: { open: boolean }) {
Welcome to the Olympus quick start guide! This tour will guide you through the basics of DCS Olympus. You can navigate through the steps using
the "Next" and "Previous" buttons at the bottom of the screen, or select a topic from the list below.
</p>
<div className={`
flex w-fit flex-col flex-wrap gap-2
md:h-32
`}>
<div
className={`
flex w-fit flex-col flex-wrap gap-2
md:h-32
`}
>
<div className="flex gap-2">
<FaLink className="my-auto" />
<div className={`cursor-pointer text-blue-400`} onClick={() => setStep(1)}>
@ -92,15 +95,16 @@ export function TrainingModal(props: { open: boolean }) {
<div className="flex gap-2">
<FaLink className="my-auto" />
<div className={`cursor-pointer text-blue-400`} onClick={() => setStep(15)}>
Game master mode
Commander mode
</div>
</div>
<div className="flex gap-2">
{/* TODO <div className="flex gap-2">
<FaLink className="my-auto" />
<div className={`cursor-pointer text-blue-400`} onClick={() => {}}>
Advanced topics
</div>
</div>
} */}
</div>
<div>
Every panel has a dedicated integrated wiki. Click on the{" "}
@ -127,9 +131,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step1.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step1.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Main navbar</h2>
<p>
@ -151,9 +156,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step2.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step2.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (1 of 3)</h2>
<p>
@ -179,9 +185,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step3.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step3.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (2 of 3)</h2>
<p>
@ -190,9 +197,10 @@ export function TrainingModal(props: { open: boolean }) {
</p>
<p>You can edit the unit properties like in the previous method. Remember you can open the unit summary section to get more info on the unit.</p>
<div className="flex gap-4">
<img src="images/training/step3.1.gif" className={`
h-32 w-32 rounded-xl
`} />
<img
src="images/training/step3.1.gif"
className={`h-32 w-32 rounded-xl`}
/>
You can change the spawn heading of the unit by dragging the arrow on the map. This will also change the spawn heading in the unit properties.
</div>
</div>
@ -209,9 +217,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step4.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step4.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Spawning units (3 of 3)</h2>
<p>
@ -233,9 +242,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step5.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step5.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (1 of 4)</h2>
<p>
@ -248,9 +258,10 @@ export function TrainingModal(props: { open: boolean }) {
destinations will be shared between them.
</p>
<div className="flex gap-4">
<img src="images/training/step4.1.gif" className={`
h-40 w-40 rounded-xl
`} />
<img
src="images/training/step4.1.gif"
className={`h-40 w-40 rounded-xl`}
/>
Holding down the right mouse button enters "group movement" mode. The units will hold their relative positions and move as a formation. Move the
mouse to choose the formation heading. Ctrl can be pressed to create a path.
</div>
@ -268,9 +279,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step6.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step6.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (2 of 4)</h2>
<p>
@ -302,9 +314,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step7.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step7.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (3 of 4)</h2>
<p>
@ -330,9 +343,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step8.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step8.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Controlling units (4 of 4)</h2>
<p>
@ -364,9 +378,12 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/unitmarker.png" className={`
max-h-34 max-w-34 my-auto rounded-xl
`} />
<img
src={`images/training/unitmarker${markerComponent ? markerComponent : ""}.png`}
className={`
max-h-34 max-w-34 mx-auto my-auto rounded-xl
`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>The unit marker (1 of 2)</h2>
<p>
@ -374,7 +391,12 @@ export function TrainingModal(props: { open: boolean }) {
players only). It has the following parts:
</p>
<div className="flex flex-wrap gap-4">
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(1)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(1)}
>
<p className="flex gap-4">
<div
className={`
@ -387,7 +409,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">Unit short label or type symbol</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(2)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(2)}
>
<p className="flex gap-4">
<div
className={`
@ -400,7 +427,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">Flight level</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(3)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(3)}
>
<p className="flex gap-4">
<div
className={`
@ -413,7 +445,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">Ground speed (knots)</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(4)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(4)}
>
<p className="flex gap-4">
<div
className={`
@ -426,7 +463,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">Bullseye position</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(5)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(5)}
>
<p className="flex gap-4">
<div
className={`
@ -439,7 +481,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">Fuel state (% of internal)</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(6)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(6)}
>
<p className="flex gap-4">
<div
className={`
@ -452,7 +499,12 @@ export function TrainingModal(props: { open: boolean }) {
<p className="my-auto">A/A weapons (Fox 1/2/3 & guns)</p>
</p>
</div>
<div className="flex flex-col">
<div
className="flex cursor-pointer flex-col"
onMouseEnter={() => setMarkerComponent(7)}
onMouseLeave={() => setMarkerComponent(null)}
onClick={() => setMarkerComponent(7)}
>
<p className="flex gap-4">
<div
className={`
@ -466,6 +518,7 @@ export function TrainingModal(props: { open: boolean }) {
</p>
</div>
</div>
<p className="font-bold">Hover your cursor or click on the components to see them highlighted in the image.</p>
<p>
Most of these information is only available for air units. Ground units will show the type symbol, the name, and the coalition, and the fuel
level is replace by the unit's health (%).
@ -632,9 +685,10 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step12.gif" className={`
h-96 w-96 rounded-xl
`} />
<img
src="images/training/step12.gif"
className={`h-96 w-96 rounded-xl`}
/>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Interacting with the map (2 of 2)</h2>
<p>
@ -646,9 +700,10 @@ export function TrainingModal(props: { open: boolean }) {
flex w-full flex-col content-center justify-center gap-4
`}
>
<img src="images/training/step12.1.png" className={`
mx-auto w-40 rounded-xl
`} />
<img
src="images/training/step12.1.png"
className={`mx-auto w-40 rounded-xl`}
/>
On the bottom right corner of the map, you can find the coordinates panel, providing the coordinates of the mouse cursor, as well as its
bullseye position and the ground elevation. Click on the coordinates to rotate format.
</div>
@ -666,13 +721,23 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step13.png" className={`h-96 rounded-xl`} />
<img src="images/training/step13.png" className={`
mx-auto h-96 rounded-xl
`} />
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Mission drawings</h2>
<p>Mission drawings are useful to provide information from the mission creator. They can define borders, Areas of Operation, navigational points and more. </p>
<p>Mission drawings are automatically imported from the mission and can be shown using the drawing menu. You can enable or disable different sections, change the opacity, and look for specific drawings or navpoint using the shearch bar.</p>
<p>
Mission drawings are useful to provide information from the mission creator. They can define borders, Areas of Operation, navigational points
and more.{" "}
</p>
<p>
Mission drawings are automatically imported from the mission and can be shown using the drawing menu. You can enable or disable different
sections, change the opacity, and look for specific drawings or navpoint using the shearch bar.
</p>
<p>You can also define your own drawings, which can be useful as reference, or as a way to automatically create IADS.</p>
<p>Use the <FaQuestionCircle className="inline"/> button on the upper right of the drawings panel for more info. </p>
<p>
Use the <FaQuestionCircle className="inline" /> button on the upper right of the drawings panel for more info.{" "}
</p>
</div>
</div>
)}
@ -687,10 +752,30 @@ export function TrainingModal(props: { open: boolean }) {
sm:gap-16
`}
>
<img src="images/training/step14.png" className={`
mx-auto h-96 rounded-xl
`} />
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>The audio backend</h2>
<p></p>
<p></p>
<p>
The audio backend is, for all intents and purposes, a client for the great open source mod{" "}
<a className={`text-blue-500`} href="http://dcssimpleradio.com/" target="_blank">
DCS-SRS
</a>{" "}
baked directly into DCS Olympus.
</p>
<p>
It allows you to talk on frequency with players operating in the mission, as well as other Olympus operators. Moreover, it allows to create fake
"loudspeakers", i.e. sound sources in game that people will only be able to hear if close enough.
</p>
<p>
The audio backend allows you to perform useful operations on audio sources: you can mix your microphone with one or multiple fire sources (e.g.
to play a background noise like a firefight sound), or you can play files directly, e.g. for playing METAR files, or to blast music for your
friends.{" "}
</p>
<p>
Use the <FaQuestionCircle className="inline" /> button on the upper right of the audio backend panel for more info.{" "}
</p>
</div>
</div>
)}
@ -706,9 +791,29 @@ export function TrainingModal(props: { open: boolean }) {
`}
>
<div className="flex flex-col gap-4 text-gray-400">
<h2 className={`text-xl font-semibold text-white`}>Game master mode</h2>
<p></p>
<p></p>
<h2 className={`text-xl font-semibold text-white`}>Commander mode</h2>
<p>
The Commander mode is a special way of operation of Olympus that forces some limitations to the operator in order to create a more challenging
environment. This can be used to create "games" where players in DCS play against the operators, forcing them to carefully place and control
their units.
</p>
<p>
These limitations are of two types:
<li>
Spawning limitations: if enabled, spawning limitations force operators by proving an initial points "budget". Every unit costs a certain
amount of points, and once all points are used, no more units can be spawned. Moreover, the game is divided into two phases, the "setup" phase
and the "combat" phase. During the setup phase, operators can freely spawn units in the air and on the ground. Once the combat phase starts,
ground units can no longer be spawned, and aircraft/helicopters can only be spawned at airbases.{" "}
</li>
<li>
Detection limitations: always enabled, this mode forces operators to use the same detection methods as players. This means that units will not
be able to see each other unless they are in visual range, or if they are using a radar or other sensor. This is useful to create a more
realistic environment, where units have to rely on their sensors and must be placed with caution.{" "}
</li>{" "}
</p>
<p>
Use the <FaQuestionCircle className="inline" /> button on the upper right of the game master option panel for more info.{" "}
</p>
</div>
</div>
)}
@ -746,11 +851,9 @@ export function TrainingModal(props: { open: boolean }) {
key={i + 1}
className={`
h-4 w-4 rounded-full
${
i + 1 === step
? "bg-blue-700 shadow-white"
: `bg-gray-300/10`
}
${i + 1 === step ? "bg-blue-700 shadow-white" : `
bg-gray-300/10
`}
`}
/>
))}

View File

@ -75,7 +75,31 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; childre
filteredBlueprints.sort((a, b) => a.label.localeCompare(b.label));
return (
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false}>
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false}
wiki={() => {
return (
<div
className={`
h-full flex-col overflow-auto p-4 text-gray-400 no-scrollbar flex
gap-2
`}
>
<h2 className="mb-4 font-bold">Airbase menu</h2>
<div>
In the airbase menu, you can see the details of the selected airbase, including its ICAO name, elevation, runways, and TACAN (if present). You can also spawn units at the airbase.
</div>
<div>
The options available to you will be the same as in a normal spawn menu, but you will be able to only spawn aircraft or helicopters. Airplanes are spawned at available parking spots or on the runway, depending on the selected airbase. Not all aircraft can be spawned, so make sure the airbase is big enough.
</div>
<div>
You will only be able to spawn units that are of the same coalition as the airbase. If you are playing in commander mode and the airbase does not belong to your coalition, you will not be able to spawn units.
</div>
<div>
The coalition of an airbase depends on what ground units control it. A way to change the coalition of an airbase is to spawn a ground unit of the desired coalition at the airbase. The airbase will then change its coalition to the one of the unit.
</div>
</div>
);
}}>
<div
className={`
flex flex-col gap-2 font-normal text-gray-800

View File

@ -281,7 +281,7 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
/* Count how many radios have a non null frequency */
const activeRadios = clientData.radios.reduce((acc, radio) => {
if (radio.frequency > 10) acc++;
if (radio.frequency > 1000) acc++;
return acc;
}, 0);
@ -299,10 +299,23 @@ export function AudioMenu(props: { open: boolean; onClose: () => void; children?
<div className="my-auto truncate text-gray-400">{clientData.name}</div>
<OlDropdown label={"Tuned radios: " + activeRadios}>
{clientData.radios.map((radio, idx) => {
return radio.frequency > 10 ?
return radio.frequency > 1000 ?
(
<OlDropdownItem key={idx}>
<div className="flex gap-2 text-white">
<div className="flex gap-2 text-white" onClick={() => {
// Find if any radio is already tuned to this frequency
const alreadyTuned = sinks.find((sink) => {
if (sink instanceof RadioSink) {
return sink.getFrequency() === radio.frequency && sink.getModulation() === radio.modulation;
}
return false;
});
if (!alreadyTuned) {
let newRadio = getApp().getAudioManager().addRadio();
newRadio.setFrequency(radio.frequency);
newRadio.setModulation(radio.modulation);
}
}}>
{`${zeroAppend(radio.frequency / 1e6, 3, true, 3)} ${radio.modulation ? "FM" : "AM"}`}
</div>
</OlDropdownItem>

View File

@ -101,6 +101,7 @@ export function Menu(props: {
text-lg
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
hover:bg-gray-200
${props.wikiDisabled ? "ml-auto" : ""}
`}
/>
<FontAwesomeIcon

View File

@ -7,6 +7,7 @@ import { faArrowLeft, faSmog } from "@fortawesome/free-solid-svg-icons";
import { LatLng } from "leaflet";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { AppStateChangedEvent } from "../../events";
import { FaQuestionCircle } from "react-icons/fa";
export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; effect: string | null; latlng?: LatLng | null; onBack?: () => void }) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
@ -71,7 +72,7 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
<span className="my-auto text-white">Explosion type</span>
</div>
<OlDropdown label={explosionType} className="w-full">
{["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => {
{["High explosive", "Napalm", "White phosphorous", "Fire"].map((optionExplosionType) => {
return (
<OlDropdownItem
key={optionExplosionType}
@ -84,6 +85,21 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
);
})}
</OlDropdown>
{!props.compact && (
<div className="flex content-center gap-4 p-4">
<div className="mt-8 text-gray-400">
<FaQuestionCircle />
</div>
<div className="text-sm text-gray-400">
Click on the map to generate an explosion effect. The type of explosion will be the one selected above. The possible explosion effects
are:
<li>High explosive: a normal explosion, like the one from a conventional bomb;</li>
<li>Napalm: an explosion with a longer lasting fire effect;</li>
<li>White phosphorous: an explosion with multiple white flares ejecting from the blast;</li>
<li>Fire: a long lasting static fire.</li>
</div>
</div>
)}
</>
)}
{props.effect === "smoke" && (
@ -117,6 +133,17 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
);
})}
</div>
<div className="flex content-center gap-4 p-4">
<div className="my-auto text-gray-400">
<FaQuestionCircle />
</div>
{!props.compact && (
<div className="text-sm text-gray-400">
Click on the map to generate a colored smoke effect. The color of the smoke will be the one selected above.
</div>
)}
</div>
</>
)}
{props.compact && (
@ -133,6 +160,7 @@ export function EffectSpawnMenu(props: { visible: boolean; compact: boolean; eff
if (explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", props.latlng);
else if (explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", props.latlng);
else if (explosionType === "White phosphorous") getApp().getServerManager().spawnExplosion(50, "phosphorous", props.latlng);
else if (explosionType === "Fire") getApp().getServerManager().spawnExplosion(50, "fire", props.latlng);
getApp().getMap().addExplosionMarker(props.latlng);
} else if (props.effect === "smoke") {
/* Find the name of the color */

View File

@ -272,12 +272,19 @@ export function Header() {
{commandModeOptions.commandMode === GAME_MASTER && (
<div
className={`
flex h-full cursor-pointer rounded-md border-2 border-transparent
bg-olympus-600 px-4 text-gray-200
hover:bg-olympus-400
flex h-full rounded-md border-2 border-transparent bg-olympus-600
px-4 text-gray-200
${
enabledCommandModes.length > 1
? `
cursor-pointer
hover:bg-olympus-400
`
: ""
}
`}
onClick={() => {
if (enabledCommandModes.length > 0) {
if (enabledCommandModes.length > 1) {
let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER);
let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER);
if (blueCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(BLUE_COMMANDER);
@ -287,7 +294,7 @@ export function Header() {
}}
>
<span className="my-auto text-nowrap font-bold">Game Master</span>
{enabledCommandModes.length > 0 && (
{enabledCommandModes.length > 1 && (
<>
{loadingNewCommandMode ? <FaSpinner className={`
my-auto ml-2 animate-spin text-white
@ -299,12 +306,19 @@ export function Header() {
{commandModeOptions.commandMode === BLUE_COMMANDER && (
<div
className={`
flex h-full cursor-pointer rounded-md border-2 border-transparent
bg-blue-600 px-4 text-gray-200
hover:bg-blue-400
${
enabledCommandModes.length > 1
? `
cursor-pointer
hover:bg-blue-500
`
: ""
}
flex h-full rounded-md border-2 border-transparent bg-blue-600
px-4 text-gray-200
`}
onClick={() => {
if (enabledCommandModes.length > 0) {
if (enabledCommandModes.length > 1) {
let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER);
let redCommandModeIndex = enabledCommandModes.indexOf(RED_COMMANDER);
if (redCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(RED_COMMANDER);
@ -314,7 +328,7 @@ export function Header() {
}}
>
<span className="my-auto text-nowrap font-bold">BLUE Commander</span>
{enabledCommandModes.length > 0 && (
{enabledCommandModes.length > 1 && (
<>
{loadingNewCommandMode ? (
<FaSpinner className={`
@ -330,12 +344,20 @@ export function Header() {
{commandModeOptions.commandMode === RED_COMMANDER && (
<div
className={`
flex h-full cursor-pointer rounded-md border-2 border-transparent
bg-red-600 px-4 text-gray-200
hover:bg-red-500
flex h-full
${
enabledCommandModes.length > 1
? `
cursor-pointer
hover:bg-red-500
`
: ""
}
rounded-md border-2 border-transparent bg-red-600 px-4
text-gray-200
`}
onClick={() => {
if (enabledCommandModes.length > 0) {
if (enabledCommandModes.length > 1) {
let gameMasterCommandModeIndex = enabledCommandModes.indexOf(GAME_MASTER);
let blueCommandModeIndex = enabledCommandModes.indexOf(BLUE_COMMANDER);
if (gameMasterCommandModeIndex >= 0) getApp().getServerManager().setActiveCommandMode(GAME_MASTER);
@ -345,12 +367,10 @@ export function Header() {
}}
>
<span className="my-auto text-nowrap font-bold">RED Commander</span>
{enabledCommandModes.length > 0 && (
{enabledCommandModes.length > 1 && (
<>
{loadingNewCommandMode ? (
<FaSpinner className={`
my-auto ml-2 animate-spin text-gray-200
`} />
<FaSpinner className={`my-auto ml-2 animate-spin text-gray-200`} />
) : (
<FaRedo className={`my-auto ml-2 text-gray-200`} />
)}

View File

@ -7,7 +7,7 @@ import { ImportExportSubstate, OlympusState } from "../../constants/constants";
export function MainMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
return (
<Menu title="Main Menu" open={props.open} showBackButton={false} onClose={props.onClose}>
<Menu title="Main Menu" open={props.open} showBackButton={false} onClose={props.onClose} wikiDisabled={true}>
<div
className={`
flex flex-col gap-1 p-5 font-normal text-gray-900

View File

@ -4,8 +4,8 @@ import { OlCheckbox } from "../components/olcheckbox";
import { OlRangeSlider } from "../components/olrangeslider";
import { OlNumberInput } from "../components/olnumberinput";
import { getApp } from "../../olympusapp";
import { MAP_OPTIONS_DEFAULTS, OlympusState, OptionsSubstate } from "../../constants/constants";
import { BindShortcutRequestEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, MAP_OPTIONS_DEFAULTS, OlympusState, OptionsSubstate } from "../../constants/constants";
import { BindShortcutRequestEvent, CommandModeOptionsChangedEvent, MapOptionsChangedEvent, ShortcutsChangedEvent } from "../../events";
import { OlAccordion } from "../components/olaccordion";
import { Shortcut } from "../../shortcut/shortcut";
import { OlSearchBar } from "../components/olsearchbar";
@ -29,6 +29,7 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
const [filterString, setFilterString] = useState("");
const [admin, setAdmin] = useState(false);
const [password, setPassword] = useState("");
const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS);
const checkPassword = (password: string) => {
var hash = sha256.create();
@ -56,10 +57,14 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
useEffect(() => {
MapOptionsChangedEvent.on((mapOptions) => setMapOptions({ ...mapOptions }));
ShortcutsChangedEvent.on((shortcuts) => setShortcuts({ ...shortcuts }));
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
setCommandModeOptions(commandModeOptions);
});
}, []);
return (
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose}>
<Menu title="User preferences" open={props.open} showBackButton={false} onClose={props.onClose} wikiDisabled={true}>
<div
className={`
flex h-full flex-col justify-end gap-2 p-5 font-normal text-gray-800
@ -222,36 +227,40 @@ export function OptionsMenu(props: { open: boolean; onClose: () => void; childre
<OlCheckbox checked={mapOptions.showRacetracks} onChange={() => {}}></OlCheckbox>
<span className="my-auto">Show racetracks</span>
</div>
<div
className={`
group flex cursor-pointer flex-row content-center justify-start
gap-4 rounded-md p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
}}
>
<div className="flex flex-col gap-2">
<div className="flex content-center gap-4">
<OlCoalitionToggle
onClick={() => {
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
}}
coalition={mapOptions.AWACSCoalition}
/>
<span className="my-auto">Coalition of unit bullseye info</span>
<>
{commandModeOptions.commandMode === GAME_MASTER && (
<div
className={`
group flex cursor-pointer flex-row content-center
justify-start gap-4 rounded-md p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
}}
>
<div className="flex flex-col gap-2">
<div className="flex content-center gap-4">
<OlCoalitionToggle
onClick={() => {
mapOptions.AWACSCoalition === "blue" && getApp().getMap().setOption("AWACSCoalition", "neutral");
mapOptions.AWACSCoalition === "neutral" && getApp().getMap().setOption("AWACSCoalition", "red");
mapOptions.AWACSCoalition === "red" && getApp().getMap().setOption("AWACSCoalition", "blue");
}}
coalition={mapOptions.AWACSCoalition}
/>
<span className="my-auto">Coalition of unit bullseye info</span>
</div>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto w-8`} />{" "}
<div className={`my-auto ml-2`}>Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.</div>
</div>
</div>
</div>
<div className="flex gap-1 text-sm text-gray-400">
<FaQuestionCircle className={`my-auto w-8`} />{" "}
<div className={`my-auto ml-2`}>Change the coalition of the bullseye to use to provide bullseye information in the unit tooltip.</div>
</div>
</div>
</div>
)}
</>
</OlAccordion>
<OlAccordion

View File

@ -14,12 +14,12 @@ export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; c
}, [])
return (
<Menu title="Unit explosion menu" open={props.open} showBackButton={false} onClose={props.onClose}>
<Menu title="Unit explosion menu" open={props.open} showBackButton={false} onClose={props.onClose} wikiDisabled={true}>
<div className="flex h-full flex-col gap-4 p-4">
<span className="text-white">Explosion type</span>
<OlDropdown label={explosionType} className="w-full">
{["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => {
{["High explosive", "Napalm", "White phosphorous", "Fire"].map((optionExplosionType) => {
return (
<OlDropdownItem
key={optionExplosionType}

View File

@ -346,6 +346,7 @@ export function UnitSpawnMenu(props: {
setSpawnLoadoutName("");
}}
className={`w-full`}
key={role}
>
{role}
</OlDropdownItem>
@ -380,6 +381,7 @@ export function UnitSpawnMenu(props: {
setSpawnLoadoutName(loadout.name);
}}
className={`w-full`}
key={loadout.name}
>
<span
className={`
@ -439,6 +441,7 @@ export function UnitSpawnMenu(props: {
setSpawnLiveryID(id);
}}
className={`w-full`}
key={id}
>
<span
className={`
@ -497,6 +500,7 @@ export function UnitSpawnMenu(props: {
setSpawnSkill(skill);
}}
className={`w-full`}
key={skill}
>
<span
className={`
@ -584,9 +588,9 @@ export function UnitSpawnMenu(props: {
open={openAccordion === OpenAccordion.LOADOUT}
title="Loadout"
>
{spawnLoadout.items.map((item) => {
{spawnLoadout.items.map((item, idx) => {
return (
<div className="flex content-center gap-2">
<div className="flex content-center gap-2" key={idx}>
<div
className={`
my-auto w-6 min-w-6 rounded-full py-0.5 text-center

View File

@ -60,6 +60,7 @@ import { ContextActionSet } from "./contextactionset";
import * as turf from "@turf/turf";
import { Carrier } from "../mission/carrier";
import {
CommandModeOptionsChangedEvent,
ContactsUpdatedEvent,
CoordinatesFreezeEvent,
HiddenTypesChangedEvent,
@ -510,6 +511,10 @@ export abstract class Unit extends CustomMarker {
if (this.getSelected()) this.drawLines();
});
CommandModeOptionsChangedEvent.on((commandModeOptions) => {
this.#redrawMarker();
});
}
/********************** Abstract methods *************************/
@ -881,7 +886,7 @@ export abstract class Unit extends CustomMarker {
targetingRange: this.#targetingRange,
aimMethodRange: this.#aimMethodRange,
acquisitionRange: this.#acquisitionRange,
airborne: this.#airborne
airborne: this.#airborne,
};
}
@ -1782,7 +1787,8 @@ export abstract class Unit extends CustomMarker {
element.querySelector(".unit-vvi")?.setAttribute("style", `height: ${15 + this.#speed / 5}px;`);
/* Set the unit name or callsign */
if (element.querySelector(".unit-callsign")) (element.querySelector(".unit-callsign") as HTMLElement).innerText = getApp().getMap().getOptions().showUnitCallsigns? this.#callsign: this.#unitName;
if (element.querySelector(".unit-callsign"))
(element.querySelector(".unit-callsign") as HTMLElement).innerText = getApp().getMap().getOptions().showUnitCallsigns ? this.#callsign : this.#unitName;
/* Set fuel data */
element.querySelector(".unit-fuel-level")?.setAttribute("style", `width: ${this.#fuel}%`);

View File

@ -1,3 +1,22 @@
v2.0.2 ====================
feat: Added more docs to ingame wiki
feat: Added threat rings to spawn tool
feat: Added unit ranges on spawn menu
fix: Removed incorrect help string in compact effect spawn menu
fix: Added missing fire effect
feat: Added error message if admin password wrong
fix: Radio menu names being squashed when too long
fix: Added callback to close all tooltips on click
fix: "Follow roads" toggle available for navy units
fix: Not airborne units no longer targeted, racetracks not drawn for airborne units
feat: Added airborne variable
feat: Session data is now saving selected map source
fix: MGRS are now separed properly
fix: State buttons hovering styles will be applied instantly
feature: Added ability to exclusively show unit types
feat: Added header button to quick toggle navpoints drawing
refactor: Reduced the zoom of the go to drawing feature
v2.0.1 ====================
Changes:
feat: Added starred spawns to spawn menu and ability to remove starred spawns

View File

@ -1,3 +1,3 @@
{
"version": "v2.0.1"
"version": "v2.0.2"
}