Added explosions, protection screen and delete unit

This commit is contained in:
Davide Passoni 2024-10-12 17:13:41 +02:00
parent 44bd054a3d
commit 4947997a0c
24 changed files with 1083 additions and 953 deletions

View File

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

View File

@ -71,6 +71,10 @@ export interface SpawnRequestTable {
unit: UnitSpawnTable;
}
export interface EffectRequestTable {
type: string;
}
export interface UnitSpawnTable {
unitType: string;
location: LatLng;

View File

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

View 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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@ export type MapOptions = {
showUnitsAcquisitionRings: boolean;
fillSelectedRing: boolean;
showMinimap: boolean;
protectDCSUnits: boolean;
};
export type MapHiddenTypes = {

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

View File

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

View File

@ -84,6 +84,7 @@ export function MapContextMenu(props: {}) {
getApp()
.getUnitsManager()
.getSelectedUnits()
.filter(unit => !unit.getHuman())
.forEach((unit: Unit) => {
unit.appendContextActions(newContextActionSet);
});

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

View File

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

View File

@ -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
`}
/>

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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