mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Added explosions, protection screen and delete unit
This commit is contained in:
parent
44bd054a3d
commit
4947997a0c
@ -240,6 +240,7 @@ export const defaultMapLayers = {};
|
||||
export const NOT_INITIALIZED = "Not initialized";
|
||||
export const IDLE = "Idle";
|
||||
export const SPAWN_UNIT = "Spawn unit";
|
||||
export const SPAWN_EFFECT = "Spawn effect";
|
||||
export const CONTEXT_ACTION = "Context action";
|
||||
export const COALITIONAREA_DRAW_POLYGON = "Draw Coalition Area polygon";
|
||||
export const COALITIONAREA_DRAW_CIRCLE = "Draw Coalition Area circle";
|
||||
@ -275,6 +276,7 @@ export const MAP_OPTIONS_DEFAULTS = {
|
||||
showUnitsAcquisitionRings: true,
|
||||
fillSelectedRing: false,
|
||||
showMinimap: false,
|
||||
protectDCSUnits: true
|
||||
} as MapOptions;
|
||||
|
||||
export const MAP_HIDDEN_TYPES_DEFAULTS = {
|
||||
|
||||
@ -71,6 +71,10 @@ export interface SpawnRequestTable {
|
||||
unit: UnitSpawnTable;
|
||||
}
|
||||
|
||||
export interface EffectRequestTable {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface UnitSpawnTable {
|
||||
unitType: string;
|
||||
location: LatLng;
|
||||
|
||||
@ -20,10 +20,11 @@ import {
|
||||
COALITIONAREA_EDIT,
|
||||
COALITIONAREA_DRAW_CIRCLE,
|
||||
NOT_INITIALIZED,
|
||||
SPAWN_EFFECT,
|
||||
} from "../constants/constants";
|
||||
import { CoalitionPolygon } from "./coalitionarea/coalitionpolygon";
|
||||
import { MapHiddenTypes, MapOptions } from "../types/types";
|
||||
import { SpawnRequestTable } from "../interfaces";
|
||||
import { EffectRequestTable, SpawnRequestTable } from "../interfaces";
|
||||
import { ContextAction } from "../unit/contextaction";
|
||||
|
||||
/* Stylesheets */
|
||||
@ -35,6 +36,7 @@ import { CoalitionCircle } from "./coalitionarea/coalitioncircle";
|
||||
|
||||
import { initDraggablePath } from "./coalitionarea/draggablepath";
|
||||
import { faDrawPolygon, faHandPointer, faJetFighter, faMap } from "@fortawesome/free-solid-svg-icons";
|
||||
import { ExplosionMarker } from "./markers/explosionmarker";
|
||||
|
||||
/* Register the handler for the box selection */
|
||||
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
|
||||
@ -108,6 +110,7 @@ export class Map extends L.Map {
|
||||
|
||||
/* Unit spawning */
|
||||
#spawnRequestTable: SpawnRequestTable | null = null;
|
||||
#effectRequestTable: EffectRequestTable | null = null;
|
||||
#temporaryMarkers: TemporaryUnitMarker[] = [];
|
||||
#currentSpawnMarker: TemporaryUnitMarker | null = null;
|
||||
|
||||
@ -342,6 +345,7 @@ export class Map extends L.Map {
|
||||
state: string,
|
||||
options?: {
|
||||
spawnRequestTable?: SpawnRequestTable;
|
||||
effectRequestTable?: EffectRequestTable;
|
||||
contextAction?: ContextAction | null;
|
||||
defaultContextAction?: ContextAction | null;
|
||||
}
|
||||
@ -366,6 +370,13 @@ export class Map extends L.Map {
|
||||
console.log(this.#spawnRequestTable);
|
||||
this.#currentSpawnMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral")
|
||||
this.#currentSpawnMarker.addTo(this);
|
||||
} else if (this.#state === SPAWN_EFFECT) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
this.#effectRequestTable = options?.effectRequestTable ?? null;
|
||||
console.log(`Effect request table:`);
|
||||
console.log(this.#effectRequestTable);
|
||||
//this.#currentEffectMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral")
|
||||
//this.#currentEffectMarker.addTo(this);
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
this.deselectAllCoalitionAreas();
|
||||
this.#contextAction = options?.contextAction ?? null;
|
||||
@ -435,6 +446,24 @@ export class Map extends L.Map {
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
} else if (this.#state === SPAWN_EFFECT) {
|
||||
return [
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB"],
|
||||
target: faMap,
|
||||
text: "Spawn effect",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", 2],
|
||||
target: faMap,
|
||||
text: "Exit spawn mode",
|
||||
},
|
||||
{
|
||||
actions: [touch ? faHandPointer : "LMB", "Drag"],
|
||||
target: faMap,
|
||||
text: "Move map location",
|
||||
},
|
||||
];
|
||||
} else if (this.#state === CONTEXT_ACTION) {
|
||||
let controls = [
|
||||
{
|
||||
@ -670,6 +699,12 @@ export class Map extends L.Map {
|
||||
return marker;
|
||||
}
|
||||
|
||||
addExplosionMarker(latlng: L.LatLng, timeout: number = 30) {
|
||||
var marker = new ExplosionMarker(latlng, timeout);
|
||||
marker.addTo(this);
|
||||
return marker;
|
||||
}
|
||||
|
||||
getSelectedCoalitionArea() {
|
||||
const coalitionArea = this.#coalitionAreas.find((coalitionArea: CoalitionPolygon | CoalitionCircle) => {
|
||||
return coalitionArea.getSelected();
|
||||
@ -695,24 +730,6 @@ export class Map extends L.Map {
|
||||
return this.#previousZoom;
|
||||
}
|
||||
|
||||
getIsUnitProtected(unit: Unit) {
|
||||
//const toggles = this.#mapMarkerVisibilityControls.reduce((list, control: MapMarkerVisibilityControl) => {
|
||||
// if (control.isProtected) {
|
||||
// list = list.concat(control.toggles);
|
||||
// }
|
||||
// return list;
|
||||
//}, [] as string[]);
|
||||
//
|
||||
//if (toggles.length === 0)
|
||||
// return false;
|
||||
//
|
||||
//return toggles.some((toggle: string) => {
|
||||
// // Specific coding for robots - extend later if needed
|
||||
// return (toggle === "dcs" && !unit.getControlled() && !unit.getHuman());
|
||||
//});
|
||||
return false;
|
||||
}
|
||||
|
||||
setSlaveDCSCamera(newSlaveDCSCamera: boolean) {
|
||||
this.#slaveDCSCamera = newSlaveDCSCamera;
|
||||
let button = document.getElementById("camera-link-control");
|
||||
@ -829,6 +846,7 @@ export class Map extends L.Map {
|
||||
this.setState(COALITIONAREA_EDIT);
|
||||
} else {
|
||||
this.setState(IDLE);
|
||||
document.dispatchEvent(new CustomEvent("hideAllMenus"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -861,7 +879,11 @@ export class Map extends L.Map {
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
|
||||
} else if (this.#state === SPAWN_EFFECT) {
|
||||
if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) {
|
||||
getApp().getServerManager().spawnExplosion(50, 'normal', pressLocation);
|
||||
}
|
||||
} else if (this.#state === COALITIONAREA_DRAW_POLYGON) {
|
||||
const selectedArea = this.getSelectedCoalitionArea();
|
||||
if (selectedArea && selectedArea instanceof CoalitionPolygon) {
|
||||
selectedArea.addTemporaryLatLng(pressLocation);
|
||||
|
||||
38
frontend/react/src/map/markers/explosionmarker.ts
Normal file
38
frontend/react/src/map/markers/explosionmarker.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { CustomMarker } from "./custommarker";
|
||||
import { DivIcon, LatLng } from "leaflet";
|
||||
import { SVGInjector } from "@tanem/svg-injector";
|
||||
import { getApp } from "../../olympusapp";
|
||||
|
||||
export class ExplosionMarker extends CustomMarker {
|
||||
#timer: number = 0;
|
||||
#timeout: number = 0;
|
||||
|
||||
constructor(latlng: LatLng, timeout: number) {
|
||||
super(latlng, { interactive: false });
|
||||
|
||||
this.#timeout = timeout;
|
||||
|
||||
this.#timer = window.setTimeout(() => {
|
||||
this.removeFrom(getApp().getMap());
|
||||
}, timeout * 1000);
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
/* Set the icon */
|
||||
var icon = new DivIcon({
|
||||
className: "leaflet-explosion-icon",
|
||||
iconAnchor: [25, 25],
|
||||
iconSize: [50, 50],
|
||||
});
|
||||
this.setIcon(icon);
|
||||
|
||||
var el = document.createElement("div");
|
||||
var img = document.createElement("img");
|
||||
img.src = `/vite/images/markers/smoke.svg`;
|
||||
img.onload = () => SVGInjector(img);
|
||||
el.append(img);
|
||||
|
||||
this.getElement()?.appendChild(el);
|
||||
this.getElement()?.classList.add("ol-temporary-marker");
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@ import { DivIcon } from "leaflet";
|
||||
import { CustomMarker } from "../map/markers/custommarker";
|
||||
import { SVGInjector } from "@tanem/svg-injector";
|
||||
import { AirbaseChartData, AirbaseOptions } from "../interfaces";
|
||||
import { getApp } from "../olympusapp";
|
||||
import { IDLE } from "../constants/constants";
|
||||
|
||||
export class Airbase extends CustomMarker {
|
||||
#name: string = "";
|
||||
@ -21,6 +23,12 @@ export class Airbase extends CustomMarker {
|
||||
|
||||
this.#name = options.name;
|
||||
this.#img = document.createElement("img");
|
||||
|
||||
this.addEventListener("click", (ev) => {
|
||||
if (getApp().getMap().getState() === IDLE) {
|
||||
document.dispatchEvent(new CustomEvent("airbaseClick", { detail: ev.target }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createIcon() {
|
||||
@ -34,7 +42,7 @@ export class Airbase extends CustomMarker {
|
||||
var el = document.createElement("div");
|
||||
el.classList.add("airbase-icon");
|
||||
el.setAttribute("data-object", "airbase");
|
||||
|
||||
|
||||
this.#img.src = "/vite/images/markers/airbase.svg";
|
||||
this.#img.onload = () => SVGInjector(this.#img);
|
||||
el.appendChild(this.#img);
|
||||
|
||||
@ -2,8 +2,6 @@ import { DivIcon, LatLng, Map } from "leaflet";
|
||||
import { Airbase } from "./airbase";
|
||||
|
||||
export class Carrier extends Airbase {
|
||||
#heading: number = 0;
|
||||
|
||||
createIcon() {
|
||||
var icon = new DivIcon({
|
||||
className: "leaflet-airbase-marker",
|
||||
@ -42,7 +40,6 @@ export class Carrier extends Airbase {
|
||||
}
|
||||
|
||||
setHeading(heading: number) {
|
||||
this.#heading = heading;
|
||||
this.getImg().style.transform = `rotate(${heading - 3.14 / 2}rad)`;
|
||||
}
|
||||
|
||||
@ -53,6 +50,7 @@ export class Carrier extends Airbase {
|
||||
const maxMeters = this._map.containerPointToLatLng([0, y]).distanceTo(this._map.containerPointToLatLng([x, y]));
|
||||
const meterPerPixel = maxMeters / x;
|
||||
this.getImg().style.width = `${Math.round(333 / meterPerPixel)}px`;
|
||||
this.setZIndexOffset(-10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,7 +91,6 @@ export class MissionManager {
|
||||
position: new LatLng(airbase.latitude, airbase.longitude),
|
||||
name: airbaseCallsign,
|
||||
}).addTo(getApp().getMap());
|
||||
this.#airbases[airbaseCallsign].on("click", (e) => this.#onAirbaseClick(e));
|
||||
this.#loadAirbaseChartData(airbaseCallsign);
|
||||
}
|
||||
}
|
||||
@ -321,10 +320,6 @@ export class MissionManager {
|
||||
if (requestRefresh) getApp().getServerManager().refreshAll();
|
||||
}
|
||||
|
||||
#onAirbaseClick(ev: any) {
|
||||
document.dispatchEvent(new CustomEvent("airbaseclick", { detail: ev.target }));
|
||||
}
|
||||
|
||||
#loadAirbaseChartData(callsign: string) {
|
||||
if (!this.#theatre) {
|
||||
return;
|
||||
|
||||
@ -349,7 +349,7 @@ export class ServerManager {
|
||||
this.PUT(data, callback);
|
||||
}
|
||||
|
||||
createFormation(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) {
|
||||
showFormationMenu(ID: number, isLeader: boolean, wingmenIDs: number[], callback: CallableFunction = () => {}) {
|
||||
var command = { ID: ID, wingmenIDs: wingmenIDs, isLeader: isLeader };
|
||||
var data = { setLeader: command };
|
||||
this.PUT(data, callback);
|
||||
|
||||
@ -20,6 +20,7 @@ export type MapOptions = {
|
||||
showUnitsAcquisitionRings: boolean;
|
||||
fillSelectedRing: boolean;
|
||||
showMinimap: boolean;
|
||||
protectDCSUnits: boolean;
|
||||
};
|
||||
|
||||
export type MapHiddenTypes = {
|
||||
|
||||
28
frontend/react/src/ui/components/oleffectlistentry.tsx
Normal file
28
frontend/react/src/ui/components/oleffectlistentry.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
|
||||
|
||||
export function OlEffectListEntry(props: { icon: IconProp; label: string, onClick: () => void }) {
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
className={`
|
||||
group relative mr-2 flex cursor-pointer select-none items-center
|
||||
justify-between rounded-sm px-2 py-2 text-sm
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>
|
||||
<div className="flex-1 px-2 text-left font-normal">{props.label}</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className={`
|
||||
px-1 transition-transform
|
||||
dark:text-olympus-50
|
||||
group-hover:translate-x-1
|
||||
`}
|
||||
></FontAwesomeIcon>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,17 +2,16 @@ import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { faArrowRightLong, faCaretRight, faCircleArrowRight, faLongArrowAltRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
|
||||
|
||||
export function OlUnitEntryList(props: { icon: IconProp; blueprint: UnitBlueprint; onClick: () => void }) {
|
||||
export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; onClick: () => void }) {
|
||||
return (
|
||||
<div
|
||||
onClick={props.onClick}
|
||||
className={`
|
||||
group relative mr-2 flex cursor-pointer select-none items-center
|
||||
justify-between rounded-sm px-2 py-2 text-sm
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500 dark:hover:bg-white
|
||||
dark:text-gray-300 dark:hover:bg-olympus-500
|
||||
`}
|
||||
>
|
||||
<FontAwesomeIcon icon={props.icon} className="text-sm"></FontAwesomeIcon>
|
||||
|
||||
@ -84,6 +84,7 @@ export function MapContextMenu(props: {}) {
|
||||
getApp()
|
||||
.getUnitsManager()
|
||||
.getSelectedUnits()
|
||||
.filter(unit => !unit.getHuman())
|
||||
.forEach((unit: Unit) => {
|
||||
unit.appendContextActions(newContextActionSet);
|
||||
});
|
||||
|
||||
90
frontend/react/src/ui/modals/protectionprompt.tsx
Normal file
90
frontend/react/src/ui/modals/protectionprompt.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { FaLock } from "react-icons/fa6";
|
||||
|
||||
export function ProtectionPrompt(props: {onContinue: (units: Unit[]) => void, onBack: () => void, units: Unit[] }) {
|
||||
return (
|
||||
<Modal
|
||||
className={`
|
||||
inline-flex h-fit w-[600px] overflow-y-auto scroll-smooth bg-white p-10
|
||||
dark:bg-olympus-800
|
||||
max-md:h-full max-md:max-h-full max-md:w-full max-md:rounded-none
|
||||
max-md:border-none
|
||||
`}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col gap-12">
|
||||
<div className={`flex flex-col items-start gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Your selection contains protected units, are you sure you want to continue?
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
Pressing "Continue" will cause all DCS controlled units in the current selection to abort their mission and start following Olympus commands only.
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
If you are trying to delete a human player unit, they will be killed and de-slotted. Be careful!
|
||||
</span>
|
||||
<span
|
||||
className={`
|
||||
text-gray-800 text-md
|
||||
dark:text-gray-500
|
||||
`}
|
||||
>
|
||||
To disable this warning, press on the <span className={`
|
||||
inline-block translate-y-3 rounded-full border-[1px]
|
||||
border-gray-900 bg-red-500 p-2 text-olympus-900
|
||||
`}><FaLock/></span> button
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {props.onContinue(props.units);}}
|
||||
className={`
|
||||
mb-2 me-2 ml-auto flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm font-medium text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onBack}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2 rounded-sm
|
||||
border-[1px] bg-blue-700 px-5 py-2.5 text-sm font-medium
|
||||
text-white
|
||||
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400
|
||||
dark:hover:bg-gray-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -7,7 +7,7 @@ import { getUnitsByLabel } from "../../other/utils";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { OlUnitEntryList } from "../components/olunitlistentry";
|
||||
import { OlUnitListEntry } from "../components/olunitlistentry";
|
||||
import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons";
|
||||
import { UnitSpawnMenu } from "./unitspawnmenu";
|
||||
|
||||
@ -103,7 +103,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredAircraft).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -114,7 +114,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredHelicopters).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
|
||||
@ -51,7 +51,7 @@ export function Menu(props: {
|
||||
onClick={props.onBack ?? (() => {})}
|
||||
icon={faArrowLeft}
|
||||
className={`
|
||||
mr-1 h-8 cursor-pointer rounded-md p-2
|
||||
mr-1 h-4 cursor-pointer rounded-md p-2
|
||||
dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-white
|
||||
`}
|
||||
/>
|
||||
|
||||
45
frontend/react/src/ui/panels/effectspawnmenu.tsx
Normal file
45
frontend/react/src/ui/panels/effectspawnmenu.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { IDLE, SPAWN_EFFECT } from "../../constants/constants";
|
||||
|
||||
export function EffectSpawnMenu(props: { effect: string }) {
|
||||
const [explosionType, setExplosionType] = useState("High explosive");
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
useEffect(() => {
|
||||
if (props.effect !== null) {
|
||||
getApp()
|
||||
?.getMap()
|
||||
?.setState(SPAWN_EFFECT, {
|
||||
effectRequestTable: {
|
||||
type: props.effect,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (getApp().getMap().getState() === SPAWN_EFFECT) getApp().getMap().setState(IDLE);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
<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) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={optionExplosionType}
|
||||
onClick={() => {
|
||||
setExplosionType(optionExplosionType);
|
||||
}}
|
||||
>
|
||||
{optionExplosionType}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -115,7 +115,7 @@ export function Header() {
|
||||
flex h-fit flex-row items-center justify-start gap-1
|
||||
`}
|
||||
>
|
||||
<OlLockStateButton checked={false} onClick={() => {}} tooltip="Lock/unlock protected units (from scripted mission)" />
|
||||
<OlLockStateButton checked={!appState.mapOptions.protectDCSUnits} onClick={() => {getApp().getMap().setOption("protectDCSUnits", !appState.mapOptions.protectDCSUnits)}} tooltip="Lock/unlock protected units (from scripted mission)" />
|
||||
<OlRoundStateButton
|
||||
checked={audioEnabled}
|
||||
onClick={() => {
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faPlaneDeparture, faRadio, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faGamepad, faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, faMagnifyingGlass, faRadio, faVolumeHigh } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EventsConsumer } from "../../eventscontext";
|
||||
import { StateConsumer } from "../../statecontext";
|
||||
import { IDLE } from "../../constants/constants";
|
||||
import { faSpeakerDeck } from "@fortawesome/free-brands-svg-icons";
|
||||
|
||||
export function SideBar() {
|
||||
return (
|
||||
@ -53,12 +52,6 @@ export function SideBar() {
|
||||
icon={faPencil}
|
||||
tooltip="Hide/show drawing menu"
|
||||
></OlStateButton>
|
||||
<OlStateButton
|
||||
onClick={events.toggleAirbaseMenuVisible}
|
||||
checked={appState.airbaseMenuVisible}
|
||||
icon={faPlaneDeparture}
|
||||
tooltip="Hide/show airbase menu"
|
||||
></OlStateButton>
|
||||
<OlStateButton
|
||||
onClick={events.toggleRadioMenuVisible}
|
||||
checked={appState.radioMenuVisible}
|
||||
|
||||
@ -3,7 +3,7 @@ import { Menu } from "./components/menu";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { OlUnitEntryList } from "../components/olunitlistentry";
|
||||
import { OlUnitListEntry } from "../components/olunitlistentry";
|
||||
import { UnitSpawnMenu } from "./unitspawnmenu";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import {
|
||||
@ -13,11 +13,15 @@ import {
|
||||
olButtonsVisibilityHelicopter,
|
||||
olButtonsVisibilityNavyunit,
|
||||
} from "../components/olicons";
|
||||
import { IDLE, SPAWN_UNIT } from "../../constants/constants";
|
||||
import { IDLE, SPAWN_EFFECT, SPAWN_UNIT } from "../../constants/constants";
|
||||
import { getUnitsByLabel } from "../../other/utils";
|
||||
import { faExplosion, faSmog } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlEffectListEntry } from "../components/oleffectlistentry";
|
||||
import { EffectSpawnMenu } from "./effectspawnmenu";
|
||||
|
||||
export function SpawnMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
|
||||
const [effect, setEffect] = useState(null as null | string);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
|
||||
const [filteredAircraft, filteredHelicopters, filteredAirDefense, filteredGroundUnits, filteredNavyUnits] = getUnitsByLabel(filterString);
|
||||
@ -25,7 +29,10 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
useEffect(() => {
|
||||
if (!props.open && getApp()) {
|
||||
if (getApp().getMap().getState() === SPAWN_UNIT) getApp().getMap().setState(IDLE);
|
||||
else if (getApp().getMap().getState() === SPAWN_EFFECT) getApp().getMap().setState(IDLE);
|
||||
|
||||
if (blueprint !== null) setBlueprint(null);
|
||||
if (effect !== null) setEffect(null);
|
||||
}
|
||||
});
|
||||
|
||||
@ -33,15 +40,16 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
<Menu
|
||||
{...props}
|
||||
title="Spawn menu"
|
||||
showBackButton={blueprint !== null}
|
||||
showBackButton={blueprint !== null || effect !== null}
|
||||
canBeHidden={true}
|
||||
onBack={() => {
|
||||
getApp().getMap().setState(IDLE);
|
||||
setBlueprint(null);
|
||||
setEffect(null);
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{blueprint === null && (
|
||||
{blueprint === null && effect === null && (
|
||||
<div className="p-5">
|
||||
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
|
||||
<OlAccordion title={`Aircraft`}>
|
||||
@ -51,7 +59,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredAircraft).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -62,7 +70,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredHelicopters).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -73,7 +81,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredAirDefense).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityGroundunitSam} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityGroundunitSam} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -84,7 +92,7 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredGroundUnits).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityGroundunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityGroundunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
@ -95,15 +103,39 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
|
||||
`}
|
||||
>
|
||||
{Object.entries(filteredNavyUnits).map((entry) => {
|
||||
return <OlUnitEntryList key={entry[0]} icon={olButtonsVisibilityNavyunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityNavyunit} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
|
||||
})}
|
||||
</div>
|
||||
</OlAccordion>
|
||||
<OlAccordion title="Effects (smokes, explosions etc)"></OlAccordion>
|
||||
<OlAccordion title="Effects (smokes, explosions etc)">
|
||||
<div
|
||||
className={`
|
||||
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
|
||||
`}
|
||||
>
|
||||
<OlEffectListEntry
|
||||
key={"explosion"}
|
||||
icon={faExplosion}
|
||||
label={"Explosion"}
|
||||
onClick={() => {
|
||||
setEffect("explosion");
|
||||
}}
|
||||
/>
|
||||
<OlEffectListEntry
|
||||
key={"smoke"}
|
||||
icon={faSmog}
|
||||
label={"Smoke"}
|
||||
onClick={() => {
|
||||
setEffect("smoke");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</OlAccordion>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!(blueprint === null) && <UnitSpawnMenu blueprint={blueprint} spawnAtLocation={true} />}
|
||||
{!(effect === null) && <EffectSpawnMenu effect={effect} />}
|
||||
</>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@ -61,6 +61,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
ROE: undefined as undefined | string,
|
||||
reactionToThreat: undefined as undefined | string,
|
||||
emissionsCountermeasures: undefined as undefined | string,
|
||||
scenicAAA: undefined as undefined | boolean,
|
||||
missOnPurpose: undefined as undefined | boolean,
|
||||
shotsScatter: undefined as undefined | number,
|
||||
shotsIntensity: undefined as undefined | number,
|
||||
operateAs: undefined as undefined | Coalition,
|
||||
@ -112,42 +114,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
if (!props.open && filterString !== "") setFilterString("");
|
||||
});
|
||||
|
||||
/* */
|
||||
const minAltitude = 0;
|
||||
const maxAltitude = getApp()
|
||||
?.getUnitsManager()
|
||||
?.getSelectedUnitsCategories()
|
||||
.every((category) => {
|
||||
return category === "Helicopter";
|
||||
})
|
||||
? 20000
|
||||
: 60000;
|
||||
const altitudeStep = getApp()
|
||||
?.getUnitsManager()
|
||||
?.getSelectedUnitsCategories()
|
||||
.every((category) => {
|
||||
return category === "Helicopter";
|
||||
})
|
||||
? 100
|
||||
: 500;
|
||||
const minSpeed = 0;
|
||||
const maxSpeed = getApp()
|
||||
?.getUnitsManager()
|
||||
?.getSelectedUnitsCategories()
|
||||
.every((category) => {
|
||||
return category === "Helicopter";
|
||||
})
|
||||
? 200
|
||||
: 800;
|
||||
const speedStep = getApp()
|
||||
?.getUnitsManager()
|
||||
?.getSelectedUnitsCategories()
|
||||
.every((category) => {
|
||||
return category === "Helicopter";
|
||||
})
|
||||
? 5
|
||||
: 10;
|
||||
|
||||
useEffect(() => {
|
||||
/* When a unit is selected, update the data */
|
||||
document.addEventListener("unitsSelection", (ev: CustomEventInit) => {
|
||||
@ -190,6 +156,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
emissionsCountermeasures: (unit: Unit) => {
|
||||
return unit.getEmissionsCountermeasures();
|
||||
},
|
||||
scenicAAA: (unit: Unit) => {
|
||||
return unit.getState() === "scenic-aaa";
|
||||
},
|
||||
missOnPurpose: (unit: Unit) => {
|
||||
return unit.getState() === "miss-on-purpose";
|
||||
},
|
||||
shotsScatter: (unit: Unit) => {
|
||||
return unit.getShotsScatter();
|
||||
},
|
||||
@ -213,8 +185,12 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
},
|
||||
isAudioSink: (unit: Unit) => {
|
||||
return (
|
||||
getApp()?.getAudioManager().getSinks().filter((sink) => {
|
||||
return sink instanceof UnitSink}).length > 0 &&
|
||||
getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
.filter((sink) => {
|
||||
return sink instanceof UnitSink;
|
||||
}).length > 0 &&
|
||||
getApp()
|
||||
?.getAudioManager()
|
||||
.getSinks()
|
||||
@ -257,6 +233,37 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
[key: string]: UnitBlueprint;
|
||||
};
|
||||
|
||||
const everyUnitIsGround = selectedCategories.every((category) => {
|
||||
return category === "GroundUnit";
|
||||
});
|
||||
const everyUnitIsNavy = selectedCategories.every((category) => {
|
||||
return category === "NavyUnit";
|
||||
});
|
||||
const everyUnitIsHelicopter = selectedCategories.every((category) => {
|
||||
return category === "Helicopter";
|
||||
});
|
||||
|
||||
const minAltitude = 0;
|
||||
const minSpeed = 0;
|
||||
|
||||
let maxAltitude = 60000;
|
||||
let maxSpeed = 800;
|
||||
|
||||
let altitudeStep = 500;
|
||||
let speedStep = 10;
|
||||
|
||||
if (everyUnitIsHelicopter) {
|
||||
maxAltitude = 20000;
|
||||
maxSpeed = 200;
|
||||
speedStep = 5;
|
||||
altitudeStep = 100;
|
||||
}
|
||||
|
||||
if (everyUnitIsGround || everyUnitIsNavy) {
|
||||
maxSpeed = 60;
|
||||
speedStep = 1;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={props.open}
|
||||
@ -618,20 +625,22 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
{selectedUnitsData.desiredSpeed !== undefined ? selectedUnitsData.desiredSpeed + " KTS" : "Different values"}
|
||||
</span>
|
||||
</div>
|
||||
<OlLabelToggle
|
||||
toggled={selectedUnitsData.desiredSpeedType === undefined ? undefined : selectedUnitsData.desiredSpeedType === "GS"}
|
||||
leftLabel={"GS"}
|
||||
rightLabel={"CAS"}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS",
|
||||
{!(everyUnitIsGround || everyUnitIsNavy) && (
|
||||
<OlLabelToggle
|
||||
toggled={selectedUnitsData.desiredSpeedType === undefined ? undefined : selectedUnitsData.desiredSpeedType === "GS"}
|
||||
leftLabel={"GS"}
|
||||
rightLabel={"CAS"}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS",
|
||||
});
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<OlRangeSlider
|
||||
onChange={(ev) => {
|
||||
@ -853,94 +862,152 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
return ["GroundUnit", "NavyUnit"].includes(category);
|
||||
}) && (
|
||||
<>
|
||||
{/* ============== Shots scatter START ============== */}
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots scatter
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsScatter(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
});
|
||||
});
|
||||
}}
|
||||
active={selectedUnitsData.shotsScatter === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots scatter END ============== */}
|
||||
{/* ============== Shots intensity START ============== */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots intensity
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsIntensity(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
});
|
||||
});
|
||||
}}
|
||||
active={selectedUnitsData.shotsIntensity === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots intensity END ============== */}
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Operate as
|
||||
</span>
|
||||
<OlCoalitionToggle
|
||||
coalition={selectedUnitsData.operateAs as Coalition}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-4 rounded-md bg-olympus-200/30 p-4
|
||||
`}
|
||||
>
|
||||
{/* ============== Scenic AAA toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Scenic AAA mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.scenicAAA}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Scenic AAA toggle END ============== */}
|
||||
{/* ============== Miss on purpose toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Miss on purpose mode
|
||||
</span>
|
||||
<OlToggle
|
||||
toggled={selectedUnitsData.missOnPurpose}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Miss on purpose toggle END ============== */}
|
||||
<div className="flex gap-4">
|
||||
{/* ============== Shots scatter START ============== */}
|
||||
<div className={`flex flex-col gap-2`}>
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots scatter
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsScatter1, olButtonsScatter2, olButtonsScatter3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsScatter(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
});
|
||||
});
|
||||
}}
|
||||
active={selectedUnitsData.shotsScatter === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots scatter END ============== */}
|
||||
{/* ============== Shots intensity START ============== */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Shots intensity
|
||||
</span>
|
||||
<OlButtonGroup>
|
||||
{[olButtonsIntensity1, olButtonsIntensity2, olButtonsIntensity3].map((icon, idx) => {
|
||||
return (
|
||||
<OlButtonGroupItem
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsIntensity(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
});
|
||||
});
|
||||
}}
|
||||
active={selectedUnitsData.shotsIntensity === idx + 1}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OlButtonGroup>
|
||||
</div>
|
||||
{/* ============== Shots intensity END ============== */}
|
||||
</div>
|
||||
{/* ============== Operate as toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
className={`
|
||||
font-normal
|
||||
dark:text-white
|
||||
`}
|
||||
>
|
||||
Operate as
|
||||
</span>
|
||||
<OlCoalitionToggle
|
||||
coalition={selectedUnitsData.operateAs as Coalition}
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* ============== Operate as toggle END ============== */}
|
||||
</div>
|
||||
{/* ============== Operate as toggle END ============== */}
|
||||
{/* ============== Follow roads toggle START ============== */}
|
||||
<div className="flex content-center justify-between">
|
||||
<span
|
||||
@ -1129,9 +1196,10 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdown
|
||||
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -1250,9 +1318,11 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
|
||||
56
frontend/react/src/ui/panels/unitexplosionmenu.tsx
Normal file
56
frontend/react/src/ui/panels/unitexplosionmenu.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React, { useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { getApp } from "../../olympusapp";
|
||||
|
||||
export function UnitExplosionMenu(props: { open: boolean; onClose: () => void; units: Unit[] | null; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [explosionType, setExplosionType] = useState("High explosive");
|
||||
|
||||
return (
|
||||
<Menu title="Unit explosion menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<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) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={optionExplosionType}
|
||||
onClick={() => {
|
||||
setExplosionType(optionExplosionType);
|
||||
}}
|
||||
>
|
||||
{optionExplosionType}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
{props.units !== null && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (explosionType === "High explosive") {
|
||||
getApp()?.getUnitsManager().delete(true, "normal", props.units);
|
||||
} else if (explosionType === "Napalm") {
|
||||
getApp()?.getUnitsManager().delete(true, "napalm", props.units);
|
||||
} else if (explosionType === "White phosphorous") {
|
||||
getApp()?.getUnitsManager().delete(true, "phosphorous", props.units);
|
||||
}
|
||||
props.onClose();
|
||||
}}
|
||||
className={`
|
||||
mb-2 rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium
|
||||
text-white
|
||||
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
||||
focus:outline-none focus:ring-4 focus:ring-blue-300
|
||||
hover:bg-blue-800
|
||||
`}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -26,6 +26,8 @@ import { RadioMenu } from "./panels/radiomenu";
|
||||
import { AudioMenu } from "./panels/audiomenu";
|
||||
import { FormationMenu } from "./panels/formationmenu";
|
||||
import { Unit } from "../unit/unit";
|
||||
import { ProtectionPrompt } from "./modals/protectionprompt";
|
||||
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
|
||||
|
||||
export type OlympusUIState = {
|
||||
mainMenuVisible: boolean;
|
||||
@ -51,6 +53,7 @@ export function UI() {
|
||||
const [optionsMenuVisible, setOptionsMenuVisible] = useState(false);
|
||||
const [airbaseMenuVisible, setAirbaseMenuVisible] = useState(false);
|
||||
const [formationMenuVisible, setFormationMenuVisible] = useState(false);
|
||||
const [unitExplosionMenuVisible, setUnitExplosionMenuVisible] = useState(false);
|
||||
const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS);
|
||||
const [mapOptions, setMapOptions] = useState(MAP_OPTIONS_DEFAULTS);
|
||||
const [checkingPassword, setCheckingPassword] = useState(false);
|
||||
@ -62,6 +65,10 @@ export function UI() {
|
||||
const [airbase, setAirbase] = useState(null as null | Airbase);
|
||||
const [formationLeader, setFormationLeader] = useState(null as null | Unit);
|
||||
const [formationWingmen, setFormationWingmen] = useState(null as null | Unit[]);
|
||||
const [protectionPromptVisible, setProtectionPromptVisible] = useState(false);
|
||||
const [protectionCallback, setProtectionCallback] = useState(null as any);
|
||||
const [protectionUnits, setProtectionUnits] = useState([] as Unit[]);
|
||||
const [unitExplosionUnits, setUnitExplosionUnits] = useState([] as Unit[]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("hiddenTypesChanged", (ev) => {
|
||||
@ -73,11 +80,15 @@ export function UI() {
|
||||
});
|
||||
|
||||
document.addEventListener("mapStateChanged", (ev) => {
|
||||
if ((ev as CustomEvent).detail === IDLE) hideAllMenus();
|
||||
else if ((ev as CustomEvent).detail === CONTEXT_ACTION && window.innerWidth > 1000) setUnitControlMenuVisible(true);
|
||||
//if ((ev as CustomEvent).detail === IDLE) hideAllMenus();
|
||||
/*else*/ if ((ev as CustomEvent).detail === CONTEXT_ACTION && window.innerWidth > 1000) setUnitControlMenuVisible(true);
|
||||
setMapState(String((ev as CustomEvent).detail));
|
||||
});
|
||||
|
||||
document.addEventListener("hideAllMenus", (ev) => {
|
||||
hideAllMenus();
|
||||
});
|
||||
|
||||
document.addEventListener("mapSourceChanged", (ev) => {
|
||||
var source = (ev as CustomEvent).detail;
|
||||
setActiveMapSource(source);
|
||||
@ -90,18 +101,29 @@ export function UI() {
|
||||
setActiveMapSource(sources[0]);
|
||||
});
|
||||
|
||||
document.addEventListener("airbaseclick", (ev) => {
|
||||
document.addEventListener("airbaseClick", (ev) => {
|
||||
hideAllMenus();
|
||||
getApp().getMap().setState(IDLE);
|
||||
setAirbase((ev as CustomEvent).detail);
|
||||
setAirbaseMenuVisible(true);
|
||||
});
|
||||
|
||||
document.addEventListener("createFormation", (ev) => {
|
||||
document.addEventListener("showFormationMenu", (ev) => {
|
||||
setFormationMenuVisible(true);
|
||||
setFormationLeader((ev as CustomEvent).detail.leader);
|
||||
setFormationWingmen((ev as CustomEvent).detail.wingmen);
|
||||
});
|
||||
|
||||
document.addEventListener("showProtectionPrompt", (ev: CustomEventInit) => {
|
||||
setProtectionPromptVisible(true);
|
||||
setProtectionCallback(() => {return ev.detail.callback});
|
||||
setProtectionUnits(ev.detail.units);
|
||||
});
|
||||
|
||||
document.addEventListener("showUnitExplosionMenu", (ev) => {
|
||||
setUnitExplosionMenuVisible(true);
|
||||
setUnitExplosionUnits((ev as CustomEvent).detail.units);
|
||||
})
|
||||
}, []);
|
||||
|
||||
function hideAllMenus() {
|
||||
@ -115,6 +137,7 @@ export function UI() {
|
||||
setRadioMenuVisible(false);
|
||||
setAudioMenuVisible(false);
|
||||
setFormationMenuVisible(false);
|
||||
setUnitExplosionMenuVisible(false);
|
||||
}
|
||||
|
||||
function checkPassword(password: string) {
|
||||
@ -246,16 +269,36 @@ export function UI() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{protectionPromptVisible && (
|
||||
<>
|
||||
<div
|
||||
className={`
|
||||
fixed left-0 top-0 z-30 h-full w-full bg-[#111111]/95
|
||||
`}
|
||||
></div>
|
||||
<ProtectionPrompt
|
||||
onContinue={(units) => {
|
||||
protectionCallback(units);
|
||||
setProtectionPromptVisible(false);
|
||||
}}
|
||||
onBack={() => {
|
||||
setProtectionPromptVisible(false);
|
||||
}}
|
||||
units={protectionUnits}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div id="map-container" className="z-0 h-full w-screen" />
|
||||
<MainMenu open={mainMenuVisible} onClose={() => setMainMenuVisible(false)} />
|
||||
<SpawnMenu open={spawnMenuVisible} onClose={() => setSpawnMenuVisible(false)} />
|
||||
<OptionsMenu open={optionsMenuVisible} onClose={() => setOptionsMenuVisible(false)} options={mapOptions} />
|
||||
<UnitControlMenu open={unitControlMenuVisible} onClose={() => setUnitControlMenuVisible(false)} />
|
||||
<DrawingMenu open={drawingMenuVisible} onClose={() => setDrawingMenuVisible(false)} />
|
||||
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase}/>
|
||||
<AirbaseMenu open={airbaseMenuVisible} onClose={() => setAirbaseMenuVisible(false)} airbase={airbase} />
|
||||
<RadioMenu open={radioMenuVisible} onClose={() => setRadioMenuVisible(false)} />
|
||||
<AudioMenu open={audioMenuVisible} onClose={() => setAudioMenuVisible(false)} />
|
||||
<FormationMenu open={formationMenuVisible} leader={formationLeader} wingmen={formationWingmen} onClose={() => setFormationMenuVisible(false)} />
|
||||
<UnitExplosionMenu open={unitExplosionMenuVisible} units={unitExplosionUnits} onClose={() => setUnitExplosionMenuVisible(false)} />
|
||||
|
||||
<MiniMapPanel />
|
||||
<ControlsPanel />
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
rad2deg,
|
||||
bearing,
|
||||
deg2rad,
|
||||
ftToM,
|
||||
getGroundElevation,
|
||||
coalitionToEnum,
|
||||
nmToM,
|
||||
@ -51,30 +50,22 @@ import { Group } from "./group";
|
||||
import { ContextActionSet } from "./contextactionset";
|
||||
import * as turf from "@turf/turf";
|
||||
import {
|
||||
olButtonsContextMissOnPurpose,
|
||||
olButtonsContextScenicAaa,
|
||||
olButtonsContextSimulateFireFight,
|
||||
olButtonsContextDiamond,
|
||||
olButtonsContextEchelonLh,
|
||||
olButtonsContextEchelonRh,
|
||||
olButtonsContextFollow,
|
||||
olButtonsContextFront,
|
||||
olButtonsContextLandAtPoint,
|
||||
olButtonsContextLineAbreast,
|
||||
olButtonsContextTrail,
|
||||
olButtonsContextAttack,
|
||||
olButtonsContextRefuel,
|
||||
} from "../ui/components/olicons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faExclamation,
|
||||
faExplosion,
|
||||
faLocationCrosshairs,
|
||||
faLocationDot,
|
||||
faMapLocation,
|
||||
faPeopleGroup,
|
||||
faPlaneArrival,
|
||||
faQuestionCircle,
|
||||
faRoute,
|
||||
faVolumeHigh,
|
||||
faTrash,
|
||||
faXmarksLines,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Carrier } from "../mission/carrier";
|
||||
@ -841,7 +832,7 @@ export abstract class Unit extends CustomMarker {
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"path",
|
||||
"Append destination",
|
||||
"Create route",
|
||||
"Click on the map to add a destination to the path",
|
||||
faRoute,
|
||||
"position",
|
||||
@ -850,6 +841,36 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
);
|
||||
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"delete",
|
||||
"Delete unit",
|
||||
"Deletes the unit",
|
||||
faTrash,
|
||||
null,
|
||||
(units: Unit[], _1, _2) => {
|
||||
getApp().getUnitsManager().delete(false);
|
||||
},
|
||||
{
|
||||
executeImmediately: true,
|
||||
}
|
||||
);
|
||||
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"explode",
|
||||
"Explode unit",
|
||||
"Explodes the unit",
|
||||
faExplosion,
|
||||
null,
|
||||
(units: Unit[], _1, _2) => {
|
||||
document.dispatchEvent(new CustomEvent("showUnitExplosionMenu", { detail: { units: units } }));
|
||||
},
|
||||
{
|
||||
executeImmediately: true,
|
||||
}
|
||||
);
|
||||
|
||||
contextActionSet.addDefaultContextAction(this, "default", "Set destination", "", faRoute, null, (units: Unit[], targetUnit, targetPosition) => {
|
||||
if (targetPosition) {
|
||||
getApp().getUnitsManager().clearDestinations(units);
|
||||
@ -1211,7 +1232,24 @@ export abstract class Unit extends CustomMarker {
|
||||
}
|
||||
|
||||
delete(explosion: boolean, explosionType: string, immediate: boolean) {
|
||||
getApp().getServerManager().deleteUnit(this.ID, explosion, explosionType, immediate);
|
||||
getApp()
|
||||
.getServerManager()
|
||||
.deleteUnit(this.ID, explosion, explosionType, immediate, (commandHash) => {
|
||||
/* When the command is executed, add an explosion marker where the unit was */
|
||||
if (explosion) {
|
||||
// TODO some commands don't currently return a commandHash, fix that!
|
||||
let timer = window.setTimeout(() => {
|
||||
//getApp()
|
||||
// .getServerManager()
|
||||
// .isCommandExecuted((res: any) => {
|
||||
// if (res.commandExecuted) {
|
||||
getApp().getMap().addExplosionMarker(this.getPosition());
|
||||
window.clearInterval(timer);
|
||||
// }
|
||||
// }, commandHash);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refuel() {
|
||||
@ -1294,38 +1332,6 @@ export abstract class Unit extends CustomMarker {
|
||||
this.#redrawMarker();
|
||||
}
|
||||
|
||||
applyFollowOptions(formation: string, units: Unit[]) {
|
||||
if (formation === "custom") {
|
||||
document.getElementById("custom-formation-dialog")?.classList.remove("hide");
|
||||
document.addEventListener("applyCustomFormation", () => {
|
||||
var dialog = document.getElementById("custom-formation-dialog");
|
||||
if (dialog) {
|
||||
dialog.classList.add("hide");
|
||||
var clock = 1;
|
||||
while (clock < 8) {
|
||||
if ((<HTMLInputElement>dialog.querySelector(`#formation-${clock}`)).checked) break;
|
||||
clock++;
|
||||
}
|
||||
var angleDeg = 360 - (clock - 1) * 45;
|
||||
var angleRad = deg2rad(angleDeg);
|
||||
var distance = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#distance`)?.querySelector("input")).value));
|
||||
var upDown = ftToM(parseInt((<HTMLInputElement>dialog.querySelector(`#up-down`)?.querySelector("input")).value));
|
||||
|
||||
// X: front-rear, positive front
|
||||
// Y: top-bottom, positive top
|
||||
// Z: left-right, positive right
|
||||
var x = distance * Math.cos(angleRad);
|
||||
var y = upDown;
|
||||
var z = distance * Math.sin(angleRad);
|
||||
|
||||
getApp().getUnitsManager().followUnit(this.ID, { x: x, y: y, z: z }, undefined, units);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getApp().getUnitsManager().followUnit(this.ID, undefined, formation, units);
|
||||
}
|
||||
}
|
||||
|
||||
/***********************************************/
|
||||
#onMouseUp(e: any) {
|
||||
this.#isMouseDown = false;
|
||||
@ -1847,7 +1853,7 @@ export abstract class AirUnit extends Unit {
|
||||
(units: Unit[], targetUnit: Unit | null, _) => {
|
||||
if (targetUnit) {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent("createFormation", {
|
||||
new CustomEvent("showFormationMenu", {
|
||||
detail: {
|
||||
leader: targetUnit,
|
||||
wingmen: units.filter((unit) => unit !== targetUnit),
|
||||
@ -1881,6 +1887,18 @@ export abstract class AirUnit extends Unit {
|
||||
if (targetPosition) getApp().getUnitsManager().carpetBomb(targetPosition, units);
|
||||
}
|
||||
);
|
||||
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"land",
|
||||
"Land",
|
||||
"Click on a point to land at the nearest airbase",
|
||||
faPlaneArrival,
|
||||
"position",
|
||||
(units: Unit[], _, targetPosition: LatLng | null) => {
|
||||
if (targetPosition) getApp().getUnitsManager().landAt(targetPosition, units);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1990,33 +2008,6 @@ export class GroundUnit extends Unit {
|
||||
{ executeImmediately: true }
|
||||
);
|
||||
|
||||
if (this.canAAA()) {
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"scenic-aaa",
|
||||
"Scenic AAA",
|
||||
"Shoot AAA in the air without aiming at any target, when an enemy unit gets close enough. WARNING: works correctly only on neutral units, blue or red units will aim",
|
||||
olButtonsContextScenicAaa,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getUnitsManager().scenicAAA(units);
|
||||
},
|
||||
{ executeImmediately: true }
|
||||
);
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
"miss-aaa",
|
||||
"Dynamic accuracy AAA",
|
||||
"Shoot AAA towards the closest enemy unit, but don't aim precisely. WARNING: works correctly only on neutral units, blue or red units will aim",
|
||||
olButtonsContextMissOnPurpose,
|
||||
null,
|
||||
(units: Unit[]) => {
|
||||
getApp().getUnitsManager().missOnPurpose(units);
|
||||
},
|
||||
{ executeImmediately: true }
|
||||
);
|
||||
}
|
||||
|
||||
/* Context actions that require a target unit */
|
||||
contextActionSet.addContextAction(
|
||||
this,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user