diff --git a/frontend/react/public/images/markers/explosion.svg b/frontend/react/public/images/markers/explosion.svg
new file mode 100644
index 00000000..2f9b89e0
--- /dev/null
+++ b/frontend/react/public/images/markers/explosion.svg
@@ -0,0 +1,59 @@
+
+
+
+
diff --git a/frontend/react/src/constants/constants.ts b/frontend/react/src/constants/constants.ts
index a01141bb..268ff63f 100644
--- a/frontend/react/src/constants/constants.ts
+++ b/frontend/react/src/constants/constants.ts
@@ -1,5 +1,6 @@
import { LatLng, LatLngBounds } from "leaflet";
import { MapOptions } from "../types/types";
+import { CommandModeOptions } from "../interfaces";
export const UNITS_URI = "units";
export const WEAPONS_URI = "weapons";
@@ -136,7 +137,7 @@ export const groupUnitCount: { [key: string]: number } = {
helicopter: 4,
navyunit: 20,
groundunit: 20,
-}
+};
export const minimapBoundaries = {
Nevada: [
@@ -252,6 +253,7 @@ export enum OlympusState {
OPTIONS = "Options",
AUDIO = "Audio",
AIRBASE = "Airbase",
+ GAME_MASTER = "Game master",
}
export const NO_SUBSTATE = "No substate";
@@ -262,7 +264,7 @@ export enum UnitControlSubState {
PROTECTION = "Protection",
MAP_CONTEXT_MENU = "Map context menu",
UNIT_CONTEXT_MENU = "Unit context menu",
- UNIT_EXPLOSION_MENU = "Unit explosion menu"
+ UNIT_EXPLOSION_MENU = "Unit explosion menu",
}
export enum DrawSubState {
@@ -324,6 +326,15 @@ export const MAP_HIDDEN_TYPES_DEFAULTS = {
neutral: false,
};
+export const COMMAND_MODE_OPTIONS_DEFAULTS: CommandModeOptions = {
+ commandMode: GAME_MASTER,
+ eras: [] as string[],
+ restrictSpawns: false,
+ restrictToCoalition: false,
+ setupTime: 0,
+ spawnPoints: { blue: 0, red: 0 },
+};
+
export enum DataIndexes {
startOfData = 0,
category,
diff --git a/frontend/react/src/events.ts b/frontend/react/src/events.ts
index 30c2e260..f2b06fe4 100644
--- a/frontend/react/src/events.ts
+++ b/frontend/react/src/events.ts
@@ -147,13 +147,13 @@ export class CoalitionAreaSelectedEvent {
}
export class AirbaseSelectedEvent {
- static on(callback: (airbase: Airbase) => void) {
+ static on(callback: (airbase: Airbase | null) => void) {
document.addEventListener(this.name, (ev: CustomEventInit) => {
callback(ev.detail.airbase);
});
}
- static dispatch(airbase: Airbase) {
+ static dispatch(airbase: Airbase | null) {
document.dispatchEvent(new CustomEvent(this.name, { detail: { airbase } }));
console.log(`Event ${this.name} dispatched`);
}
@@ -168,7 +168,7 @@ export class ContactsUpdatedEvent {
static dispatch() {
document.dispatchEvent(new CustomEvent(this.name));
- console.log(`Event ${this.name} dispatched`);
+ // Logging disabled since periodic
}
}
@@ -198,7 +198,12 @@ export class ContextActionChangedEvent {
}
}
-export class UnitUpdatedEvent extends BaseUnitEvent {};
+export class UnitUpdatedEvent extends BaseUnitEvent {
+ static dispatch(unit: Unit) {
+ document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));
+ // Logging disabled since periodic
+ }
+};
export class UnitSelectedEvent extends BaseUnitEvent {};
export class UnitDeselectedEvent extends BaseUnitEvent {};
export class UnitDeadEvent extends BaseUnitEvent {};
diff --git a/frontend/react/src/interfaces.ts b/frontend/react/src/interfaces.ts
index b73cb66e..14fd813d 100644
--- a/frontend/react/src/interfaces.ts
+++ b/frontend/react/src/interfaces.ts
@@ -92,6 +92,8 @@ export interface SpawnRequestTable {
export interface EffectRequestTable {
type: string;
+ explosionType?: string;
+ smokeColor?: string;
}
export interface UnitSpawnTable {
diff --git a/frontend/react/src/map/map.ts b/frontend/react/src/map/map.ts
index 5ee02085..78bd7ffc 100644
--- a/frontend/react/src/map/map.ts
+++ b/frontend/react/src/map/map.ts
@@ -38,6 +38,7 @@ import { ExplosionMarker } from "./markers/explosionmarker";
import { TextMarker } from "./markers/textmarker";
import { TargetMarker } from "./markers/targetmarker";
import {
+ AirbaseSelectedEvent,
AppStateChangedEvent,
CoalitionAreaSelectedEvent,
ConfigLoadedEvent,
@@ -49,6 +50,7 @@ import {
UnitUpdatedEvent,
} from "../events";
import { ContextActionSet } from "../unit/contextactionset";
+import { SmokeMarker } from "./markers/smokemarker";
/* Register the handler for the box selection */
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
@@ -85,7 +87,6 @@ export class Map extends L.Map {
#isShiftKeyDown: boolean = false;
/* Center on unit target */
- // TODO add back
#centeredUnit: Unit | null = null;
/* Minimap */
@@ -124,6 +125,7 @@ export class Map extends L.Map {
#effectRequestTable: EffectRequestTable | null = null;
#temporaryMarkers: TemporaryUnitMarker[] = [];
#currentSpawnMarker: TemporaryUnitMarker | null = null;
+ #currentEffectMarker: ExplosionMarker | SmokeMarker | null = null;
/* JTAC tools */
#ECHOPoint: TextMarker | null = null;
@@ -201,9 +203,8 @@ export class Map extends L.Map {
});
UnitUpdatedEvent.on((unit) => {
- if (this.#centeredUnit != null && unit == this.#centeredUnit)
- this.#panToUnit(this.#centeredUnit);
- })
+ if (this.#centeredUnit != null && unit == this.#centeredUnit) this.#panToUnit(this.#centeredUnit);
+ });
MapOptionsChangedEvent.on((options) => {
this.getContainer().toggleAttribute("data-hide-labels", !options.showUnitLabels);
@@ -723,12 +724,6 @@ 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;
- }
-
setOption(key, value) {
this.#options[key] = value;
MapOptionsChangedEvent.dispatch(this.#options);
@@ -814,8 +809,11 @@ export class Map extends L.Map {
this.getSelectedCoalitionArea()?.setEditing(false);
this.#currentSpawnMarker?.removeFrom(this);
this.#currentSpawnMarker = null;
+ this.#currentEffectMarker?.removeFrom(this);
+ this.#currentEffectMarker = null;
if (state !== OlympusState.UNIT_CONTROL) getApp().getUnitsManager().deselectAllUnits();
if (state !== OlympusState.DRAW || (state === OlympusState.DRAW && subState !== DrawSubState.EDIT)) this.deselectAllCoalitionAreas();
+ AirbaseSelectedEvent.dispatch(null);
/* Operations to perform when entering a state */
if (state === OlympusState.IDLE) {
@@ -833,9 +831,11 @@ export class Map extends L.Map {
} else if (subState === SpawnSubState.SPAWN_EFFECT) {
console.log(`Effect request table:`);
console.log(this.#effectRequestTable);
- // TODO add temporary effect marker
- //this.#currentEffectMarker = new TemporaryUnitMarker(new L.LatLng(0, 0), this.#spawnRequestTable?.unit.unitType ?? "", this.#spawnRequestTable?.coalition ?? "neutral")
- //this.#currentEffectMarker.addTo(this);
+ if (this.#effectRequestTable?.type === 'explosion')
+ this.#currentEffectMarker = new ExplosionMarker(new L.LatLng(0, 0))
+ else if (this.#effectRequestTable?.type === 'smoke')
+ this.#currentEffectMarker = new SmokeMarker(new L.LatLng(0, 0), this.#effectRequestTable.smokeColor ?? "white")
+ this.#currentEffectMarker?.addTo(this);
}
} else if (state === OlympusState.UNIT_CONTROL) {
console.log(`Context action:`);
@@ -941,7 +941,21 @@ export class Map extends L.Map {
}
} else if (getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) {
if (e.originalEvent.button != 2 && this.#effectRequestTable !== null) {
- getApp().getServerManager().spawnExplosion(50, "normal", pressLocation);
+ if (this.#effectRequestTable.type === "explosion") {
+ if (this.#effectRequestTable.explosionType === "High explosive") getApp().getServerManager().spawnExplosion(50, "normal", pressLocation);
+ else if (this.#effectRequestTable.explosionType === "Napalm") getApp().getServerManager().spawnExplosion(50, "napalm", pressLocation);
+ else if (this.#effectRequestTable.explosionType === "White phosphorous")
+ getApp().getServerManager().spawnExplosion(50, "phosphorous", pressLocation);
+
+ const explosionMarker = new ExplosionMarker(pressLocation, 5);
+ explosionMarker.addTo(this);
+ } else if (this.#effectRequestTable.type === "smoke") {
+ getApp()
+ .getServerManager()
+ .spawnSmoke(this.#effectRequestTable.smokeColor ?? "white", pressLocation);
+ const smokeMarker = new SmokeMarker(pressLocation, this.#effectRequestTable.smokeColor ?? "white");
+ smokeMarker.addTo(this);
+ }
}
}
} else if (getApp().getState() === OlympusState.DRAW) {
@@ -1056,9 +1070,10 @@ export class Map extends L.Map {
this.#lastMousePosition.y = e.originalEvent.y;
this.#lastMouseCoordinates = e.latlng;
- if (this.#currentSpawnMarker) {
+ if (this.#currentSpawnMarker)
this.#currentSpawnMarker.setLatLng(e.latlng);
- }
+ if (this.#currentEffectMarker)
+ this.#currentEffectMarker.setLatLng(e.latlng);
}
#onMapMove(e: any) {
diff --git a/frontend/react/src/map/markers/explosionmarker.ts b/frontend/react/src/map/markers/explosionmarker.ts
index 44beedaf..af32a6f0 100644
--- a/frontend/react/src/map/markers/explosionmarker.ts
+++ b/frontend/react/src/map/markers/explosionmarker.ts
@@ -7,32 +7,33 @@ export class ExplosionMarker extends CustomMarker {
#timer: number = 0;
#timeout: number = 0;
- constructor(latlng: LatLng, timeout: number) {
+ constructor(latlng: LatLng, timeout?: number) {
super(latlng, { interactive: false });
- this.#timeout = timeout;
+ if (timeout) {
+ this.#timeout = timeout;
- this.#timer = window.setTimeout(() => {
- this.removeFrom(getApp().getMap());
- }, timeout * 1000);
+ 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);
-
+ this.setIcon(
+ new DivIcon({
+ iconSize: [52, 52],
+ iconAnchor: [26, 52],
+ className: "leaflet-explosion-marker",
+ })
+ );
var el = document.createElement("div");
+ el.classList.add("ol-explosion-icon");
var img = document.createElement("img");
- img.src = `/vite/images/markers/smoke.svg`;
+ img.src = "/vite/images/markers/explosion.svg";
img.onload = () => SVGInjector(img);
- el.append(img);
-
+ el.appendChild(img);
this.getElement()?.appendChild(el);
- this.getElement()?.classList.add("ol-temporary-marker");
}
}
diff --git a/frontend/react/src/map/markers/stylesheets/airbase.css b/frontend/react/src/map/markers/stylesheets/airbase.css
index 87eb740c..8096e797 100644
--- a/frontend/react/src/map/markers/stylesheets/airbase.css
+++ b/frontend/react/src/map/markers/stylesheets/airbase.css
@@ -24,3 +24,7 @@
.airbase-icon[data-coalition="neutral"] svg * {
stroke: var(--unit-background-neutral);
}
+
+.airbase-icon[data-selected="true"] {
+ filter: drop-shadow(0px 2px 0px white) drop-shadow(0px -2px 0px white) drop-shadow(2px 0px 0px white) drop-shadow(-2px 0px 0px white);
+}
diff --git a/frontend/react/src/map/stylesheets/map.css b/frontend/react/src/map/stylesheets/map.css
index ed0831b1..358845cd 100644
--- a/frontend/react/src/map/stylesheets/map.css
+++ b/frontend/react/src/map/stylesheets/map.css
@@ -144,4 +144,36 @@
font-weight: bold;
border: 2px solid black;
font-size: 14px;
+}
+
+.ol-smoke-icon {
+ opacity: 75%;
+}
+
+[data-color="white"].ol-smoke-icon {
+ fill: white;
+}
+
+[data-color="blue"].ol-smoke-icon {
+ fill: blue;
+}
+
+[data-color="red"].ol-smoke-icon {
+ fill: red;
+}
+
+[data-color="green"].ol-smoke-icon {
+ fill: green;
+}
+
+[data-color="orange"].ol-smoke-icon {
+ fill: orange;
+}
+
+.ol-explosion-icon * {
+ opacity: 75%;
+}
+
+.ol-explosion-icon {
+ fill: red;
}
\ No newline at end of file
diff --git a/frontend/react/src/mission/airbase.ts b/frontend/react/src/mission/airbase.ts
index b8edfb1a..266fdcc6 100644
--- a/frontend/react/src/mission/airbase.ts
+++ b/frontend/react/src/mission/airbase.ts
@@ -19,6 +19,7 @@ export class Airbase extends CustomMarker {
#properties: string[] = [];
#parkings: string[] = [];
#img: HTMLImageElement;
+ #selected: boolean = false;
constructor(options: AirbaseOptions) {
super(options.position, { riseOnHover: true });
@@ -26,8 +27,14 @@ export class Airbase extends CustomMarker {
this.#name = options.name;
this.#img = document.createElement("img");
+ AirbaseSelectedEvent.on((airbase) => {
+ this.#selected = airbase == this;
+ if (this.getElement()?.querySelector(".airbase-icon"))
+ (this.getElement()?.querySelector(".airbase-icon") as HTMLElement).dataset.selected = `${this.#selected}`;
+ })
+
this.addEventListener("click", (ev) => {
- if (getApp().getState() === OlympusState.IDLE) {
+ if (getApp().getState() === OlympusState.IDLE || getApp().getState() === OlympusState.AIRBASE) {
getApp().setState(OlympusState.AIRBASE)
AirbaseSelectedEvent.dispatch(this)
}
diff --git a/frontend/react/src/mission/missionmanager.ts b/frontend/react/src/mission/missionmanager.ts
index 44def441..8d9754bc 100644
--- a/frontend/react/src/mission/missionmanager.ts
+++ b/frontend/react/src/mission/missionmanager.ts
@@ -223,7 +223,8 @@ export class MissionManager {
commandModeOptions.spawnPoints.red !== this.getCommandModeOptions().spawnPoints.red ||
commandModeOptions.spawnPoints.blue !== this.getCommandModeOptions().spawnPoints.blue ||
commandModeOptions.restrictSpawns !== this.getCommandModeOptions().restrictSpawns ||
- commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition;
+ commandModeOptions.restrictToCoalition !== this.getCommandModeOptions().restrictToCoalition ||
+ commandModeOptions.setupTime !== this.getCommandModeOptions().setupTime;
this.#commandModeOptions = commandModeOptions;
this.setSpentSpawnPoints(0);
diff --git a/frontend/react/src/server/servermanager.ts b/frontend/react/src/server/servermanager.ts
index 2cd64ad3..8fca1ae1 100644
--- a/frontend/react/src/server/servermanager.ts
+++ b/frontend/react/src/server/servermanager.ts
@@ -13,7 +13,7 @@ import {
emissionsCountermeasures,
reactionsToThreat,
} from "../constants/constants";
-import { AirbasesData, BullseyesData, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
+import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, TACAN } from "../interfaces";
import { ServerStatusUpdatedEvent } from "../events";
export class ServerManager {
@@ -489,22 +489,10 @@ export class ServerManager {
}
setCommandModeOptions(
- restrictSpawns: boolean,
- restrictToCoalition: boolean,
- spawnPoints: { blue: number; red: number },
- eras: string[],
- setupTime: number,
+ commandModeOptions: CommandModeOptions,
callback: CallableFunction = () => {}
) {
- var command = {
- restrictSpawns: restrictSpawns,
- restrictToCoalition: restrictToCoalition,
- spawnPoints: spawnPoints,
- eras: eras,
- setupTime: setupTime,
- };
-
- var data = { setCommandModeOptions: command };
+ var data = { setCommandModeOptions: commandModeOptions };
this.PUT(data, callback);
}
diff --git a/frontend/react/src/ui/components/olstatebutton.tsx b/frontend/react/src/ui/components/olstatebutton.tsx
index 0ec2485a..ce2f8fc2 100644
--- a/frontend/react/src/ui/components/olstatebutton.tsx
+++ b/frontend/react/src/ui/components/olstatebutton.tsx
@@ -6,6 +6,7 @@ import { OlTooltip } from "./oltooltip";
export function OlStateButton(props: {
className?: string;
+ borderColor?: string | null;
checked: boolean;
icon: IconProp;
tooltip: string;
@@ -35,6 +36,9 @@ export function OlStateButton(props: {
data-checked={props.checked}
type="button"
className={className}
+ style={{
+ border: props.borderColor ? "2px solid " + props.borderColor : "0px solid transparent"
+ }}
onMouseEnter={() => {
setHover(true);
}}
diff --git a/frontend/react/src/ui/components/olunitlistentry.tsx b/frontend/react/src/ui/components/olunitlistentry.tsx
index 254df5d8..ce3c63d3 100644
--- a/frontend/react/src/ui/components/olunitlistentry.tsx
+++ b/frontend/react/src/ui/components/olunitlistentry.tsx
@@ -4,8 +4,10 @@ import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { UnitBlueprint } from "../../interfaces";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
-export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; onClick: () => void }) {
- const pillString = !["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities;
+export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprint; showCost: boolean; cost: number; onClick: () => void }) {
+ let pillString = "" as string | undefined
+ if (props.showCost) pillString = `${props.cost} points`
+ else pillString = !["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities
return (
- {!["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities}
+ {pillString}
)}
void; airbase: Airbase | null; children?: JSX.Element | JSX.Element[] }) {
+enum CategoryAccordion {
+ NONE,
+ AIRCRAFT,
+ HELICOPTER,
+}
+
+export function AirbaseMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
+ const [airbase, setAirbase] = useState(null as null | Airbase);
const [blueprint, setBlueprint] = useState(null as null | UnitBlueprint);
const [filterString, setFilterString] = useState("");
+ const [selectedRole, setSelectedRole] = useState(null as null | string);
+ const [runwaysAccordionOpen, setRunwaysAccordionOpen] = useState(false);
+ const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]);
+ const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] });
+ const [openAccordion, setOpenAccordion] = useState(CategoryAccordion.NONE);
- const [filteredAircraft, filteredHelicopters] = [{}, {}] // TODOgetUnitsByLabel(filterString);
+ useEffect(() => {
+ AirbaseSelectedEvent.on((airbase) => {
+ setAirbase(airbase);
+ });
+
+ UnitDatabaseLoadedEvent.on(() => {
+ setRoles({
+ aircraft: getApp()
+ ?.getUnitsManager()
+ .getDatabase()
+ .getRoles((unit) => unit.category === "aircraft"),
+ helicopter: getApp()
+ ?.getUnitsManager()
+ .getDatabase()
+ .getRoles((unit) => unit.category === "helicopter"),
+ });
+ });
+ }, []);
+
+ useEffect(() => {
+ if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole));
+ else setBlueprints(getApp()?.getUnitsManager().getDatabase().getBlueprints());
+ }, [selectedRole, openAccordion]);
+
+ /* Filter the blueprints according to the label */
+ const filteredBlueprints: UnitBlueprint[] = [];
+ if (blueprints) {
+ blueprints.forEach((blueprint) => {
+ if (blueprint.enabled && (filterString === "" || blueprint.label.toLowerCase().includes(filterString.toLowerCase()))) filteredBlueprints.push(blueprint);
+ });
+ }
return (
-