Merge branch 'release-candidate' into features/quick-filter-rework-drawings-layers
4
frontend/react/public/images/markers/navpoint-tgt.svg
Normal 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 |
3
frontend/react/public/images/markers/navpoint.svg
Normal 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 |
BIN
frontend/react/public/images/training/step14.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
frontend/react/public/images/training/unitmarker1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/react/public/images/training/unitmarker2.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/react/public/images/training/unitmarker3.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/react/public/images/training/unitmarker4.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/react/public/images/training/unitmarker5.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/react/public/images/training/unitmarker6.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/react/public/images/training/unitmarker7.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
@ -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
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
`}
|
||||
`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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`} />
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}%`);
|
||||
|
||||
19
notes.txt
@ -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
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v2.0.1"
|
||||
"version": "v2.0.2"
|
||||
}
|
||||
|
||||