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 ( - +
void; airbase `} >
void; airbase >
ICAO name - {props.airbase?.getChartData().ICAO !== "" ? props.airbase?.getChartData().ICAO : "N/A"} + {airbase?.getChartData().ICAO !== "" ? airbase?.getChartData().ICAO : "N/A"}
TACAN - {props.airbase?.getChartData().TACAN !== "" ? props.airbase?.getChartData().TACAN : "None"} + {airbase?.getChartData().TACAN !== "" ? airbase?.getChartData().TACAN : "None"}
Elevation - {props.airbase?.getChartData().elevation !== "" ? props.airbase?.getChartData().elevation : "N/A"}ft + {airbase?.getChartData().elevation !== "" ? airbase?.getChartData().elevation : "N/A"}ft
{ - // TODO + // TODO I can't remember what tho } - + setRunwaysAccordionOpen(!runwaysAccordionOpen)} + open={runwaysAccordionOpen} + >
- {props.airbase?.getChartData().runways.map((runway, idx) => { + {airbase?.getChartData().runways.map((runway, idx) => { return ( <> {Object.keys(runway.headings[0]).map((runwayName) => { return ( -
+
{" "} RWY {runwayName} @@ -82,7 +132,6 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
-
{blueprint && ( void; airbase )} Spawn units at airbase
+ {blueprint === null && ( +
+ setFilterString(value)} text={filterString} /> + { + setOpenAccordion(openAccordion === CategoryAccordion.AIRCRAFT ? CategoryAccordion.NONE : CategoryAccordion.AIRCRAFT); + setSelectedRole(null); + }} + > +
+ {roles.aircraft.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {filteredBlueprints + .filter((blueprint) => blueprint.category === "aircraft") + .map((entry) => { + return setBlueprint(entry)} />; + })} +
+
+ { + setOpenAccordion(openAccordion === CategoryAccordion.HELICOPTER ? CategoryAccordion.NONE : CategoryAccordion.HELICOPTER); + setSelectedRole(null); + }} + > +
+ {roles.helicopter.sort().map((role) => { + return ( +
{ + selectedRole === role ? setSelectedRole(null) : setSelectedRole(role); + }} + > + {role} +
+ ); + })} +
+
+ {filteredBlueprints + .filter((blueprint) => blueprint.category === "helicopter") + .map((entry) => { + return setBlueprint(entry)} />; + })} +
+
+
+ )} <> - {blueprint === null && ( -
- setFilterString(value)} text={filterString} /> - -
- {Object.entries(filteredAircraft).map((entry) => { - return setBlueprint(entry[1])} />; - })} -
-
- -
- {Object.entries(filteredHelicopters).map((entry) => { - return setBlueprint(entry[1])} />; - })} -
-
-
- )} - {!(blueprint === null) && ( - + )}
diff --git a/frontend/react/src/ui/panels/effectspawnmenu.tsx b/frontend/react/src/ui/panels/effectspawnmenu.tsx index 6d15f0a8..fc6984ad 100644 --- a/frontend/react/src/ui/panels/effectspawnmenu.tsx +++ b/frontend/react/src/ui/panels/effectspawnmenu.tsx @@ -1,45 +1,74 @@ import React, { useEffect, useState } from "react"; import { OlDropdown, OlDropdownItem } from "../components/oldropdown"; import { getApp } from "../../olympusapp"; +import { OlympusState, SpawnSubState } from "../../constants/constants"; +import { OlStateButton } from "../components/olstatebutton"; +import { faSmog } from "@fortawesome/free-solid-svg-icons"; export function EffectSpawnMenu(props: { effect: string }) { const [explosionType, setExplosionType] = useState("High explosive"); + const [smokeColor, setSmokeColor] = useState("white"); /* When the menu is opened show the unit preview on the map as a cursor */ useEffect(() => { - // TODO - //if (props.effect !== null) { - // getApp() - // ?.getMap() - // ?.setState(SPAWN_EFFECT, { - // effectRequestTable: { - // type: props.effect, - // } - // }); - //} else { - // if (getApp().getState() === SPAWN_EFFECT) getApp().setState(OlympusState.IDLE); - //} - + if (props.effect !== null) { + if (props.effect === "explosion") + getApp()?.getMap()?.setEffectRequestTable({ + type: props.effect, + explosionType, + }); + else if (props.effect === "smoke") + getApp()?.getMap()?.setEffectRequestTable({ + type: props.effect, + smokeColor, + }); + getApp().setState(OlympusState.SPAWN, SpawnSubState.SPAWN_EFFECT); + } else { + if (getApp().getState() === OlympusState.SPAWN && getApp().getSubState() === SpawnSubState.SPAWN_EFFECT) getApp().setState(OlympusState.IDLE); + } }); return ( -
- Explosion type - - - {["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => { - return ( - { - setExplosionType(optionExplosionType); - }} - > - {optionExplosionType} - - ); - })} - -
+
+ {props.effect === "explosion" && ( + <> + Explosion type + + {["High explosive", "Napalm", "White phosphorous"].map((optionExplosionType) => { + return ( + { + setExplosionType(optionExplosionType); + }} + > + {optionExplosionType} + + ); + })} + + + )} + {props.effect === "smoke" && ( + <> + Smoke color +
+ {["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => { + return ( + { + setSmokeColor(optionSmokeColor); + }} + tooltip="" + borderColor={optionSmokeColor} + /> + ); + })} +
+ + )} +
); } diff --git a/frontend/react/src/ui/panels/gamemastermenu.tsx b/frontend/react/src/ui/panels/gamemastermenu.tsx new file mode 100644 index 00000000..ba186156 --- /dev/null +++ b/frontend/react/src/ui/panels/gamemastermenu.tsx @@ -0,0 +1,296 @@ +import React, { useEffect, useState } from "react"; +import { Menu } from "./components/menu"; +import { OlCheckbox } from "../components/olcheckbox"; +import { OlRangeSlider } from "../components/olrangeslider"; +import { OlNumberInput } from "../components/olnumberinput"; +import { MapOptions } from "../../types/types"; +import { getApp } from "../../olympusapp"; +import { CommandModeOptions, ServerStatus } from "../../interfaces"; +import { CommandModeOptionsChangedEvent, ServerStatusUpdatedEvent } from "../../events"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, ERAS, GAME_MASTER, RED_COMMANDER } from "../../constants/constants"; + +export function GameMasterMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) { + const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); + const [currentSetupTime, setCurrentSetupTime] = useState(300); + const [serverStatus, setServerStatus] = useState({} as ServerStatus); + + useEffect(() => { + ServerStatusUpdatedEvent.on((status) => setServerStatus(status)); + CommandModeOptionsChangedEvent.on((commandModeOptions) => { + setCommandModeOptions(commandModeOptions); + setCurrentSetupTime(commandModeOptions.setupTime); + }); + }, []); + + return ( + +
+ You are operating as: + {commandModeOptions.commandMode === GAME_MASTER && ( +
+ GAME MASTER +
+ )} + {commandModeOptions.commandMode === BLUE_COMMANDER &&
BLUE COMMANDER
} + {commandModeOptions.commandMode === RED_COMMANDER &&
RED COMMANDER
} + {serverStatus.elapsedTime > currentSetupTime && ( +
+ Setup time has ended +
+ )} + {serverStatus.elapsedTime <= currentSetupTime && ( +
+ SETUP ends in {(currentSetupTime - serverStatus.elapsedTime)?.toFixed()} seconds +
+ )} + Options: +
+
{ + if (commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.restrictSpawns = !commandModeOptions.restrictSpawns; + setCommandModeOptions(newCommandModeOptions); + }} + > + {}} disabled={commandModeOptions.commandMode !== GAME_MASTER} /> + + Restrict unit spanws + +
+
{ + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.restrictToCoalition = !commandModeOptions.restrictToCoalition; + setCommandModeOptions(newCommandModeOptions); + }} + > + {}} + disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER} + /> + + Restrict spawns to coalition + +
+ {ERAS.sort((a, b) => (a.chronologicalOrder > b.chronologicalOrder ? 1 : -1)).map((era) => { + return ( +
{ + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + if (commandModeOptions.eras.includes(era.name)) newCommandModeOptions.eras.splice(newCommandModeOptions.eras.indexOf(era.name)); + else newCommandModeOptions.eras.push(era.name); + setCommandModeOptions(newCommandModeOptions); + }} + > + {}} + disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER} + /> + + Allow {era.name} units + +
+ ); + })} + +
+ + Blue spawn points + + { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.blue = parseInt(e.target.value); + setCommandModeOptions(newCommandModeOptions); + }} + onIncrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.blue = Math.min(newCommandModeOptions.spawnPoints.blue + 10, 1000000); + setCommandModeOptions(newCommandModeOptions); + }} + onDecrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.blue = Math.max(newCommandModeOptions.spawnPoints.blue - 10, 0); + setCommandModeOptions(newCommandModeOptions); + }} + > +
+
+ + Red spawn points + + { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.red = parseInt(e.target.value); + setCommandModeOptions(newCommandModeOptions); + }} + onIncrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.red = Math.min(newCommandModeOptions.spawnPoints.red + 15, 6000); + setCommandModeOptions(newCommandModeOptions); + }} + onDecrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.spawnPoints.red = Math.max(newCommandModeOptions.spawnPoints.red - 15, 0); + setCommandModeOptions(newCommandModeOptions); + }} + > +
+
+ + Setup time (seconds) + + { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.setupTime = parseInt(e.target.value); + setCommandModeOptions(newCommandModeOptions); + }} + onIncrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.setupTime = Math.min(newCommandModeOptions.setupTime + 10, 6000); + setCommandModeOptions(newCommandModeOptions); + }} + onDecrease={() => { + if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return; + const newCommandModeOptions = { ...commandModeOptions }; + newCommandModeOptions.setupTime = Math.max(newCommandModeOptions.setupTime - 10, 0); + setCommandModeOptions(newCommandModeOptions); + }} + > +
+
+ Elapsed time (seconds){" "} + + {serverStatus.elapsedTime?.toFixed()} + +
+ {commandModeOptions.commandMode === GAME_MASTER && ( + + )} +
+
+
+ ); +} diff --git a/frontend/react/src/ui/panels/header.tsx b/frontend/react/src/ui/panels/header.tsx index f3813824..6b1242f1 100644 --- a/frontend/react/src/ui/panels/header.tsx +++ b/frontend/react/src/ui/panels/header.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { OlRoundStateButton, OlStateButton, OlLockStateButton } from "../components/olstatebutton"; import { faSkull, faCamera, faFlag, faLink, faUnlink, faBars, faVolumeHigh } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { OlDropdownItem, OlDropdown } from "../components/oldropdown"; import { OlLabelToggle } from "../components/ollabeltoggle"; import { getApp, IP } from "../../olympusapp"; @@ -17,9 +16,9 @@ import { olButtonsVisibilityOlympus, } from "../components/olicons"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; -import { ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events"; -import { MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; -import { OlympusConfig } from "../../interfaces"; +import { CommandModeOptionsChangedEvent, ConfigLoadedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, MapSourceChangedEvent } from "../../events"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, MAP_HIDDEN_TYPES_DEFAULTS, MAP_OPTIONS_DEFAULTS } from "../../constants/constants"; +import { CommandModeOptions, OlympusConfig } from "../../interfaces"; export function Header() { const [mapHiddenTypes, setMapHiddenTypes] = useState(MAP_HIDDEN_TYPES_DEFAULTS); @@ -29,6 +28,7 @@ export function Header() { const [scrolledLeft, setScrolledLeft] = useState(true); const [scrolledRight, setScrolledRight] = useState(false); const [audioEnabled, setAudioEnabled] = useState(false); + const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); useEffect(() => { HiddenTypesChangedEvent.on((hiddenTypes) => setMapHiddenTypes({ ...hiddenTypes })); @@ -38,6 +38,9 @@ export function Header() { var sources = Object.keys(config.mapMirrors).concat(Object.keys(config.mapLayers)); setMapSources(sources); }); + CommandModeOptionsChangedEvent.on((commandModeOptions) => { + setCommandModeOptions(commandModeOptions); + }); }, []); /* Initialize the "scroll" position of the element */ @@ -111,6 +114,9 @@ export function Header() {
+ {commandModeOptions.commandMode === BLUE_COMMANDER &&
BLUE Commander ({commandModeOptions.spawnPoints.blue} points)
}
diff --git a/frontend/react/src/ui/panels/infobar.tsx b/frontend/react/src/ui/panels/infobar.tsx new file mode 100644 index 00000000..e41ca112 --- /dev/null +++ b/frontend/react/src/ui/panels/infobar.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useRef, useState } from "react"; +import { ContextActionSet } from "../../unit/contextactionset"; +import { OlStateButton } from "../components/olstatebutton"; +import { getApp } from "../../olympusapp"; +import { ContextAction } from "../../unit/contextaction"; +import { CONTEXT_ACTION_COLORS } from "../../constants/constants"; +import { FaInfoCircle } from "react-icons/fa"; +import { FaChevronLeft, FaChevronRight } from "react-icons/fa6"; +import { OlympusState } from "../../constants/constants"; +import { AppStateChangedEvent, ContextActionChangedEvent, ContextActionSetChangedEvent } from "../../events"; + +export function InfoBar(props: {}) { + const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED); + const [contextActionSet, setContextActionsSet] = useState(null as ContextActionSet | null); + const [contextAction, setContextAction] = useState(null as ContextAction | null); + const [scrolledLeft, setScrolledLeft] = useState(true); + const [scrolledRight, setScrolledRight] = useState(false); + + /* Initialize the "scroll" position of the element */ + var scrollRef = useRef(null); + useEffect(() => { + if (scrollRef.current) onScroll(scrollRef.current); + }); + + useEffect(() => { + AppStateChangedEvent.on((state, subState) => setAppState(state)); + ContextActionSetChangedEvent.on((contextActionSet) => setContextActionsSet(contextActionSet)); + ContextActionChangedEvent.on((contextAction) => setContextAction(contextAction)); + }, []); + + function onScroll(el) { + const sl = el.scrollLeft; + const sr = el.scrollWidth - el.scrollLeft - el.clientWidth; + + sl < 1 && !scrolledLeft && setScrolledLeft(true); + sl > 1 && scrolledLeft && setScrolledLeft(false); + + sr < 1 && !scrolledRight && setScrolledRight(true); + sr > 1 && scrolledRight && setScrolledRight(false); + } + + let reorderedActions: ContextAction[] = []; + CONTEXT_ACTION_COLORS.forEach((color) => { + if (contextActionSet) { + Object.values(contextActionSet.getContextActions()).forEach((contextAction: ContextAction) => { + if (color === null && contextAction.getOptions().buttonColor === undefined) reorderedActions.push(contextAction); + else if (color === contextAction.getOptions().buttonColor) reorderedActions.push(contextAction); + }); + } + }); + + return ( + <> + {appState === OlympusState.UNIT_CONTROL && contextActionSet && Object.keys(contextActionSet.getContextActions()).length > 0 && ( + <> +
+ {!scrolledLeft && ( + + )} +
onScroll(ev.target)} ref={scrollRef}> + {reorderedActions.map((contextActionIt: ContextAction) => { + return ( + { + if (contextActionIt.getOptions().executeImmediately) { + contextActionIt.executeCallback(null, null); + } else { + contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null); + } + }} + /> + ); + })} +
+ {!scrolledRight && ( + + )} +
+ {contextAction && ( +
+
+ )} + + )} + + ); +} diff --git a/frontend/react/src/ui/panels/sidebar.tsx b/frontend/react/src/ui/panels/sidebar.tsx index f75db7aa..376bb2c7 100644 --- a/frontend/react/src/ui/panels/sidebar.tsx +++ b/frontend/react/src/ui/panels/sidebar.tsx @@ -2,18 +2,17 @@ import React, { useEffect, useState } from "react"; import { OlStateButton } from "../components/olstatebutton"; import { faGamepad, - faRuler, faPencil, faEllipsisV, faCog, faQuestionCircle, faPlusSquare, - faMagnifyingGlass, faVolumeHigh, faJ, + faCrown, } from "@fortawesome/free-solid-svg-icons"; import { getApp } from "../../olympusapp"; -import { NO_SUBSTATE, OlympusState, OlympusSubState } from "../../constants/constants"; +import { OlympusState } from "../../constants/constants"; import { AppStateChangedEvent } from "../../events"; export function SideBar() { @@ -80,6 +79,14 @@ export function SideBar() { icon={faJ} tooltip="Hide/show JTAC menu" > + { + getApp().setState(appState !== OlympusState.GAME_MASTER ? OlympusState.GAME_MASTER : OlympusState.IDLE); + }} + checked={appState === OlympusState.GAME_MASTER} + icon={faCrown} + tooltip="Hide/show Game Master menu" + >
diff --git a/frontend/react/src/ui/panels/spawnmenu.tsx b/frontend/react/src/ui/panels/spawnmenu.tsx index d9ad346d..33a0122d 100644 --- a/frontend/react/src/ui/panels/spawnmenu.tsx +++ b/frontend/react/src/ui/panels/spawnmenu.tsx @@ -16,8 +16,8 @@ import { import { faExplosion, faSmog } from "@fortawesome/free-solid-svg-icons"; import { OlEffectListEntry } from "../components/oleffectlistentry"; import { EffectSpawnMenu } from "./effectspawnmenu"; -import { NO_SUBSTATE, OlympusState } from "../../constants/constants"; -import { AppStateChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; +import { BLUE_COMMANDER, COMMAND_MODE_OPTIONS_DEFAULTS, GAME_MASTER, NO_SUBSTATE, OlympusState } from "../../constants/constants"; +import { AppStateChangedEvent, CommandModeOptionsChangedEvent, UnitDatabaseLoadedEvent } from "../../events"; enum CategoryAccordion { NONE, @@ -40,6 +40,8 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? const [blueprints, setBlueprints] = useState([] as UnitBlueprint[]); const [roles, setRoles] = useState({ aircraft: [] as string[], helicopter: [] as string[] }); const [types, setTypes] = useState({ groundunit: [] as string[], navyunit: [] as string[] }); + const [commandModeOptions, setCommandModeOptions] = useState(COMMAND_MODE_OPTIONS_DEFAULTS); + const [showCost, setShowCost] = useState(false); useEffect(() => { if (selectedRole) setBlueprints(getApp()?.getUnitsManager().getDatabase().getByRole(selectedRole)); @@ -71,6 +73,19 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? .getTypes((unit) => unit.category === "navyunit"), }); }); + + AppStateChangedEvent.on((state, subState) => { + if (subState === NO_SUBSTATE) { + setBlueprint(null); + setEffect(null); + } + }); + + CommandModeOptionsChangedEvent.on((commandModeOptions) => { + setCommandModeOptions(commandModeOptions); + setShowCost(!(commandModeOptions.commandMode == GAME_MASTER || !commandModeOptions.restrictSpawns)); + setOpenAccordion(CategoryAccordion.NONE); + }); }, []); /* Filter the blueprints according to the label */ @@ -90,15 +105,6 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? } }); - useEffect(() => { - AppStateChangedEvent.on((state, subState) => { - if (subState === NO_SUBSTATE) { - setBlueprint(null); - setEffect(null); - } - }); - }, []); - return ( void; children? > {filteredBlueprints .filter((blueprint) => blueprint.category === "aircraft") - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })}
@@ -196,8 +211,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? > {filteredBlueprints .filter((blueprint) => blueprint.category === "helicopter") - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })} @@ -218,8 +242,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? > {filteredBlueprints .filter((blueprint) => blueprint.category === "groundunit" && blueprint.type === "SAM Site") - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })} @@ -240,8 +273,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? > {filteredBlueprints .filter((blueprint) => blueprint.canAAA) - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })} @@ -286,8 +328,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? > {filteredBlueprints .filter((blueprint) => blueprint.category === "groundunit" && blueprint.type !== "SAM Site") - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })} @@ -329,8 +380,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? > {filteredBlueprints .filter((blueprint) => blueprint.category === "navyunit") - .map((entry) => { - return setBlueprint(entry)} />; + .map((blueprint) => { + return ( + setBlueprint(blueprint)} + showCost={showCost} + cost={blueprint.cost ?? 10} + /> + ); })} @@ -370,7 +430,13 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children? )} - {!(blueprint === null) && } + {!(blueprint === null) && ( + + )} {!(effect === null) && }
diff --git a/frontend/react/src/ui/panels/unitcontrolbar.tsx b/frontend/react/src/ui/panels/unitcontrolbar.tsx index 619f5b6a..54d1257b 100644 --- a/frontend/react/src/ui/panels/unitcontrolbar.tsx +++ b/frontend/react/src/ui/panels/unitcontrolbar.tsx @@ -77,14 +77,7 @@ export function UnitControlBar(props: {}) { checked={contextActionIt === contextAction} icon={contextActionIt.getIcon()} tooltip={contextActionIt.getLabel()} - className={ - contextActionIt.getOptions().buttonColor - ? ` - border-2 - border-${contextActionIt.getOptions().buttonColor}-500 - ` - : "" - } + borderColor={contextActionIt.getOptions().buttonColor} onClick={() => { if (contextActionIt.getOptions().executeImmediately) { contextActionIt.executeCallback(null, null); diff --git a/frontend/react/src/ui/panels/unitcontrolmenu.tsx b/frontend/react/src/ui/panels/unitcontrolmenu.tsx index 87baad94..fb1ce8e2 100644 --- a/frontend/react/src/ui/panels/unitcontrolmenu.tsx +++ b/frontend/react/src/ui/panels/unitcontrolmenu.tsx @@ -6,7 +6,7 @@ import { OlRangeSlider } from "../components/olrangeslider"; import { getApp } from "../../olympusapp"; import { OlButtonGroup, OlButtonGroupItem } from "../components/olbuttongroup"; import { OlCheckbox } from "../components/olcheckbox"; -import { ROEs, altitudeIncrements, emissionsCountermeasures, maxAltitudeValues, minAltitudeValues, reactionsToThreat, speedIncrements } from "../../constants/constants"; +import { ROEs, altitudeIncrements, emissionsCountermeasures, maxAltitudeValues, maxSpeedValues, minAltitudeValues, reactionsToThreat, speedIncrements } from "../../constants/constants"; import { OlToggle } from "../components/oltoggle"; import { OlCoalitionToggle } from "../components/olcoalitiontoggle"; import { @@ -207,22 +207,22 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) { const minSpeed = 0; let maxAltitude = maxAltitudeValues.aircraft; - let maxSpeed = minAltitudeValues.aircraft; + let maxSpeed = maxSpeedValues.aircraft; let speedStep = speedIncrements.aircraft; let altitudeStep = altitudeIncrements.aircraft; if (everyUnitIsHelicopter) { maxAltitude = maxAltitudeValues.helicopter; - maxSpeed = minAltitudeValues.helicopter; + maxSpeed = maxSpeedValues.helicopter; speedStep = speedIncrements.helicopter; altitudeStep = altitudeIncrements.helicopter; } else if (everyUnitIsGround) { - maxSpeed = minAltitudeValues.groundunit; + maxSpeed = maxSpeedValues.groundunit; speedStep = speedIncrements.groundunit; } else if (everyUnitIsNavy) { - maxSpeed = minAltitudeValues.navyunit; + maxSpeed = maxSpeedValues.navyunit; speedStep = speedIncrements.navyunit; } diff --git a/frontend/react/src/ui/panels/unitspawnmenu.tsx b/frontend/react/src/ui/panels/unitspawnmenu.tsx index 4e2cbfc6..82a1bd53 100644 --- a/frontend/react/src/ui/panels/unitspawnmenu.tsx +++ b/frontend/react/src/ui/panels/unitspawnmenu.tsx @@ -143,38 +143,40 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation {["aircraft", "helicopter"].includes(props.blueprint.category) && ( <> -
-
-
- - Altitude - - {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} + {!props.airbase && ( +
+
+
+ + Altitude + + {`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`} +
+ setSpawnAltitudeType(!spawnAltitudeType)} />
- setSpawnAltitudeType(!spawnAltitudeType)} /> + setSpawnAltitude(Number(ev.target.value))} + value={spawnAltitude} + min={minAltitude} + max={maxAltitude} + step={altitudeStep} + />
- setSpawnAltitude(Number(ev.target.value))} - value={spawnAltitude} - min={minAltitude} - max={maxAltitude} - step={altitudeStep} - /> -
+ )}
getApp().setState(OlympusState.IDLE)} /> - getApp().setState(OlympusState.IDLE)} airbase={airbase} /* TODO remove */ /> + getApp().setState(OlympusState.IDLE)}/> getApp().setState(OlympusState.IDLE)} /> + getApp().setState(OlympusState.IDLE)} /> {/* TODO} getApp().setState(OlympusState.IDLE)} /> {*/} getApp().setState(OlympusState.IDLE)} /> diff --git a/frontend/react/src/unit/databases/unitdatabase.ts b/frontend/react/src/unit/databases/unitdatabase.ts index d3fe3c0a..97f2a8aa 100644 --- a/frontend/react/src/unit/databases/unitdatabase.ts +++ b/frontend/react/src/unit/databases/unitdatabase.ts @@ -211,14 +211,8 @@ export class UnitDatabase { return null; } - getSpawnPointsByLabel(label: string) { - var blueprint = this.getByLabel(label); - if (blueprint) return this.getSpawnPointsByName(blueprint.name); - else return Infinity; - } - getSpawnPointsByName(name: string) { - return Infinity; + return this.getByLabel(name)?.cost ?? 10; } getUnkownUnit(name: string): UnitBlueprint { diff --git a/frontend/react/src/unit/unit.ts b/frontend/react/src/unit/unit.ts index 2b0301b3..0c198a17 100644 --- a/frontend/react/src/unit/unit.ts +++ b/frontend/react/src/unit/unit.ts @@ -69,7 +69,7 @@ import { faXmarksLines, } from "@fortawesome/free-solid-svg-icons"; import { Carrier } from "../mission/carrier"; -import { ContactsUpdatedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitDeadEvent, UnitDeselectedEvent, UnitSelectedEvent } from "../events"; +import { ContactsUpdatedEvent, HiddenTypesChangedEvent, MapOptionsChangedEvent, UnitDeadEvent, UnitDeselectedEvent, UnitSelectedEvent, UnitUpdatedEvent } from "../events"; var pathIcon = new Icon({ iconUrl: "/vite/images/markers/marker-icon.png", @@ -618,6 +618,8 @@ export abstract class Unit extends CustomMarker { } } } + + UnitUpdatedEvent.dispatch(this); } /** Get unit data collated into an object diff --git a/frontend/react/src/unit/unitsmanager.ts b/frontend/react/src/unit/unitsmanager.ts index 4c559c05..5378b31e 100644 --- a/frontend/react/src/unit/unitsmanager.ts +++ b/frontend/react/src/unit/unitsmanager.ts @@ -1422,8 +1422,7 @@ export class UnitsManager { return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return 0; - // TODO return points + this.#unitIndexedDB.selectBlueprints({from:"Units", where: {name: unit.unitType}}); + return points + this.getDatabase().getSpawnPointsByName(unit.unitType) }, 0); spawnFunction = () => getApp().getServerManager().spawnAircrafts(units, coalition, airbase, country, immediate, spawnPoints, callback); } else if (category === "helicopter") { @@ -1432,8 +1431,7 @@ export class UnitsManager { return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return 0; - //TODO return points + helicopterDatabase.getSpawnPointsByName(unit.unitType); + return points + this.getDatabase().getSpawnPointsByName(unit.unitType) }, 0); spawnFunction = () => getApp().getServerManager().spawnHelicopters(units, coalition, airbase, country, immediate, spawnPoints, callback); } else if (category === "groundunit") { @@ -1442,8 +1440,7 @@ export class UnitsManager { return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return 0; - //TODOreturn points + groundUnitDatabase.getSpawnPointsByName(unit.unitType); + return points + this.getDatabase().getSpawnPointsByName(unit.unitType) }, 0); spawnFunction = () => getApp().getServerManager().spawnGroundUnits(units, coalition, country, immediate, spawnPoints, callback); } else if (category === "navyunit") { @@ -1452,8 +1449,7 @@ export class UnitsManager { return false; } spawnPoints = units.reduce((points: number, unit: UnitSpawnTable) => { - return 0; - //TODOreturn points + navyUnitDatabase.getSpawnPointsByName(unit.unitType); + return points + this.getDatabase().getSpawnPointsByName(unit.unitType) }, 0); spawnFunction = () => getApp().getServerManager().spawnNavyUnits(units, coalition, country, immediate, spawnPoints, callback); }