Readded command mode options

This commit is contained in:
Davide Passoni 2024-11-07 17:39:34 +01:00
parent 454c7ad2de
commit df939f1ac3
27 changed files with 990 additions and 231 deletions

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="52"
height="52"
viewBox="0 0 13.758333 13.758333"
version="1.1"
id="svg5"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="explosion.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
width="52mm"
units="px"
inkscape:zoom="8.3856042"
inkscape:cx="14.250613"
inkscape:cy="36.550735"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1" />
<defs
id="defs2">
<linearGradient
id="linearGradient4717"
inkscape:swatch="solid">
<stop
style="stop-color:#0cffff;stop-opacity:1;"
offset="0"
id="stop4715" />
</linearGradient>
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="stroke-width:6.985;stroke-linejoin:round;paint-order:stroke fill markers;fill-opacity:1"
d="M 6.5628344,12.967909 1.2936356,11.16944 4.7012613,11.421857 1.4829482,6.9099074 5.0483343,9.6864912 3.5022818,0.8519064 6.8468032,9.623387 8.6452724,3.0290005 8.0142306,9.9389079 11.737377,6.8783553 8.5506162,10.980127 12.904805,10.885471 Z"
id="path1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

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

View File

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

View File

@ -92,6 +92,8 @@ export interface SpawnRequestTable {
export interface EffectRequestTable {
type: string;
explosionType?: string;
smokeColor?: string;
}
export interface UnitSpawnTable {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<div
onClick={props.onClick}
@ -24,7 +26,7 @@ export function OlUnitListEntry(props: { icon: IconProp; blueprint: UnitBlueprin
dark:text-olympus-50
`}
>
{!["aircraft", "helicopter"].includes(props.blueprint.category) ? props.blueprint.type : props.blueprint.abilities}
{pillString}
</div>
)}
<FontAwesomeIcon

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Menu } from "./components/menu";
import { Coalition } from "../../types/types";
import { Airbase } from "../../mission/airbase";
@ -9,15 +9,59 @@ import { OlAccordion } from "../components/olaccordion";
import { OlUnitListEntry } from "../components/olunitlistentry";
import { olButtonsVisibilityAircraft, olButtonsVisibilityHelicopter } from "../components/olicons";
import { UnitSpawnMenu } from "./unitspawnmenu";
import { AirbaseSelectedEvent, UnitDatabaseLoadedEvent } from "../../events";
import { getApp } from "../../olympusapp";
export function AirbaseMenu(props: { open: boolean; onClose: () => 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 (
<Menu title={props.airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<Menu title={airbase?.getName() ?? "No airbase selected"} open={props.open} onClose={props.onClose} showBackButton={false} canBeHidden={true}>
<div
className={`
flex flex-col gap-2 font-normal text-gray-800
@ -25,7 +69,7 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
`}
>
<div
data-coalition={props.airbase?.getCoalition()}
data-coalition={airbase?.getCoalition()}
className={`
flex flex-col content-center justify-between gap-2 border-l-4
bg-olympus-200/30 py-3 pl-4 pr-5
@ -36,29 +80,35 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
>
<div className="flex w-full justify-between">
<span className="text-gray-400">ICAO name</span>
<span>{props.airbase?.getChartData().ICAO !== "" ? props.airbase?.getChartData().ICAO : "N/A"}</span>
<span>{airbase?.getChartData().ICAO !== "" ? airbase?.getChartData().ICAO : "N/A"}</span>
</div>
<div className="flex w-full justify-between">
<span className="text-gray-400">TACAN</span>
<span>{props.airbase?.getChartData().TACAN !== "" ? props.airbase?.getChartData().TACAN : "None"}</span>
<span>{airbase?.getChartData().TACAN !== "" ? airbase?.getChartData().TACAN : "None"}</span>
</div>
<div className="flex w-full justify-between">
<span className="text-gray-400">Elevation</span>
<span>{props.airbase?.getChartData().elevation !== "" ? props.airbase?.getChartData().elevation : "N/A"}ft</span>
<span>{airbase?.getChartData().elevation !== "" ? airbase?.getChartData().elevation : "N/A"}ft</span>
</div>
{
// TODO
// TODO I can't remember what tho
}
<OlAccordion title={`Runways`} className="!p-0 !text-gray-400">
<OlAccordion
title={`Runways`}
className="!p-0 !text-gray-400"
onClick={() => setRunwaysAccordionOpen(!runwaysAccordionOpen)}
open={runwaysAccordionOpen}
>
<div className="flex flex-col gap-2">
{props.airbase?.getChartData().runways.map((runway, idx) => {
{airbase?.getChartData().runways.map((runway, idx) => {
return (
<>
{Object.keys(runway.headings[0]).map((runwayName) => {
return (
<div key={`${idx}-${runwayName}`} className={`
flex w-full justify-between
`}>
<div
key={`${idx}-${runwayName}`}
className={`flex w-full justify-between`}
>
<span>
{" "}
<span className="text-gray-400">RWY</span> {runwayName}
@ -82,7 +132,6 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
</div>
</OlAccordion>
</div>
<div className="mt-5 flex gap-2 px-5 text-white bold">
{blueprint && (
<FaArrowLeft
@ -95,42 +144,98 @@ export function AirbaseMenu(props: { open: boolean; onClose: () => void; airbase
)}
<span className="my-auto">Spawn units at airbase</span>
</div>
{blueprint === null && (
<div className="p-5">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<OlAccordion
title={`Aircraft`}
open={openAccordion == CategoryAccordion.AIRCRAFT}
onClick={() => {
setOpenAccordion(openAccordion === CategoryAccordion.AIRCRAFT ? CategoryAccordion.NONE : CategoryAccordion.AIRCRAFT);
setSelectedRole(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{roles.aircraft.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{filteredBlueprints
.filter((blueprint) => blueprint.category === "aircraft")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityAircraft} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
<OlAccordion
title={`Helicopters`}
open={openAccordion == CategoryAccordion.HELICOPTER}
onClick={() => {
setOpenAccordion(openAccordion === CategoryAccordion.HELICOPTER ? CategoryAccordion.NONE : CategoryAccordion.HELICOPTER);
setSelectedRole(null);
}}
>
<div className="mb-2 flex flex-wrap gap-1">
{roles.helicopter.sort().map((role) => {
return (
<div
key={role}
data-selected={selectedRole === role}
className={`
cursor-pointer rounded-full bg-olympus-800 px-2 py-0.5
text-xs font-bold text-olympus-50
data-[selected='true']:bg-blue-500
data-[selected='true']:text-gray-200
`}
onClick={() => {
selectedRole === role ? setSelectedRole(null) : setSelectedRole(role);
}}
>
{role}
</div>
);
})}
</div>
<div
className={`
flex max-h-[450px] flex-col gap-1 overflow-y-scroll
no-scrollbar
`}
>
{filteredBlueprints
.filter((blueprint) => blueprint.category === "helicopter")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityHelicopter} blueprint={entry} onClick={() => setBlueprint(entry)} />;
})}
</div>
</OlAccordion>
</div>
)}
<>
{blueprint === null && (
<div className="p-5">
<OlSearchBar onChange={(value) => setFilterString(value)} text={filterString} />
<OlAccordion title={`Aircraft`}>
<div
className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.entries(filteredAircraft).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityAircraft} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
</div>
</OlAccordion>
<OlAccordion title={`Helicopters`}>
<div
className={`
flex max-h-80 flex-col gap-1 overflow-y-scroll no-scrollbar
`}
>
{Object.entries(filteredHelicopters).map((entry) => {
return <OlUnitListEntry key={entry[0]} icon={olButtonsVisibilityHelicopter} blueprint={entry[1]} onClick={() => setBlueprint(entry[1])} />;
})}
</div>
</OlAccordion>
</div>
)}
{!(blueprint === null) && (
<UnitSpawnMenu
blueprint={blueprint}
spawnAtLocation={false}
airbase={props.airbase}
coalition={(props.airbase?.getCoalition() ?? "blue") as Coalition}
/>
<UnitSpawnMenu blueprint={blueprint} spawnAtLocation={false} airbase={airbase} coalition={(airbase?.getCoalition() ?? "blue") as Coalition} />
)}
</>
</div>

View File

@ -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 (
<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>
<div className="flex h-full flex-col gap-4 p-4">
{props.effect === "explosion" && (
<>
<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.effect === "smoke" && (
<>
<span className="text-white">Smoke color</span>
<div className="flex w-full gap-2">
{["white", "blue", "red", "green", "orange"].map((optionSmokeColor) => {
return (
<OlStateButton
checked={smokeColor === optionSmokeColor}
icon={faSmog}
onClick={() => {
setSmokeColor(optionSmokeColor);
}}
tooltip=""
borderColor={optionSmokeColor}
/>
);
})}
</div>
</>
)}
</div>
);
}

View File

@ -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 (
<Menu title="Game Master options" open={props.open} showBackButton={false} onClose={props.onClose}>
<div
className={`
flex flex-col gap-2 p-5 font-normal text-gray-800
dark:text-white
`}
>
You are operating as:
{commandModeOptions.commandMode === GAME_MASTER && (
<div
className={`
w-full rounded-md bg-olympus-400 p-2 text-center font-bold
`}
>
GAME MASTER
</div>
)}
{commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
w-full rounded-md bg-blue-600 p-2 text-center font-bold
`}>BLUE COMMANDER</div>}
{commandModeOptions.commandMode === RED_COMMANDER && <div className={`
w-full rounded-md bg-red-700 p-2 text-center font-bold
`}>RED COMMANDER</div>}
{serverStatus.elapsedTime > currentSetupTime && (
<div
className={`
w-full rounded-md bg-orange-600 p-2 text-center font-bold
`}
>
Setup time has ended
</div>
)}
{serverStatus.elapsedTime <= currentSetupTime && (
<div
className={`
w-full rounded-md bg-green-700 p-2 text-center font-bold
`}
>
SETUP ends in {(currentSetupTime - serverStatus.elapsedTime)?.toFixed()} seconds
</div>
)}
<span className="mt-5">Options: </span>
<div className="flex flex-col gap-2">
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
if (commandModeOptions.commandMode !== GAME_MASTER) return;
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.restrictSpawns = !commandModeOptions.restrictSpawns;
setCommandModeOptions(newCommandModeOptions);
}}
>
<OlCheckbox checked={commandModeOptions.restrictSpawns} onChange={() => {}} disabled={commandModeOptions.commandMode !== GAME_MASTER} />
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`data-[disabled='true']:text-gray-400`}
>
Restrict unit spanws
</span>
</div>
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
if (!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER) return;
const newCommandModeOptions = { ...commandModeOptions };
newCommandModeOptions.restrictToCoalition = !commandModeOptions.restrictToCoalition;
setCommandModeOptions(newCommandModeOptions);
}}
>
<OlCheckbox
checked={commandModeOptions.restrictToCoalition}
onChange={() => {}}
disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
/>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`data-[disabled='true']:text-gray-400`}
>
Restrict spawns to coalition
</span>
</div>
{ERAS.sort((a, b) => (a.chronologicalOrder > b.chronologicalOrder ? 1 : -1)).map((era) => {
return (
<div
className={`
group flex flex-row rounded-md justify-content cursor-pointer
gap-4 p-2
dark:hover:bg-olympus-400
`}
onClick={() => {
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);
}}
>
<OlCheckbox
checked={commandModeOptions.eras.includes(era.name)}
onChange={() => {}}
disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
/>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`data-[disabled='true']:text-gray-400`}
>
Allow {era.name} units
</span>
</div>
);
})}
<div
className={`
group flex flex-row rounded-md justify-content gap-4
bg-blue-600/40 px-4 py-2
`}
>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`
my-auto mr-auto
data-[disabled='true']:text-gray-400
`}
>
Blue spawn points
</span>
<OlNumberInput
min={0}
max={1e6}
value={commandModeOptions.spawnPoints.blue}
onChange={(e) => {
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);
}}
></OlNumberInput>
</div>
<div
className={`
group flex flex-row rounded-md justify-content gap-4 bg-red-600/40
px-4 py-2
`}
>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`
my-auto mr-auto
data-[disabled='true']:text-gray-400
`}
>
Red spawn points
</span>
<OlNumberInput
min={0}
max={1e6}
value={commandModeOptions.spawnPoints.red}
onChange={(e) => {
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);
}}
></OlNumberInput>
</div>
<div
className={`
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
`}
>
<span
data-disabled={!commandModeOptions.restrictSpawns || commandModeOptions.commandMode !== GAME_MASTER}
className={`
my-auto mr-auto
data-[disabled='true']:text-gray-400
`}
>
Setup time (seconds)
</span>
<OlNumberInput
min={0}
max={6000}
value={commandModeOptions.setupTime}
onChange={(e) => {
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);
}}
></OlNumberInput>
</div>
<div
className={`
group flex flex-row rounded-md justify-content gap-4 px-4 py-2
`}
>
<span className="mr-auto">Elapsed time (seconds)</span>{" "}
<span
className={`w-32 text-center`}
>
{serverStatus.elapsedTime?.toFixed()}
</span>
</div>
{commandModeOptions.commandMode === GAME_MASTER && (
<button
type="button"
onClick={() => {
if (commandModeOptions.commandMode !== GAME_MASTER) return;
getApp().getServerManager().setCommandModeOptions(commandModeOptions);
}}
className={`
w-full 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>
</div>
</Menu>
);
}

View File

@ -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() {
</div>
</div>
</div>
{commandModeOptions.commandMode === BLUE_COMMANDER && <div className={`
flex h-full rounded-md bg-blue-600 px-4 text-white
`}><span className="my-auto font-bold">BLUE Commander ({commandModeOptions.spawnPoints.blue} points)</span></div>}
<div
className={`flex h-fit flex-row items-center justify-start gap-1`}
>

View File

@ -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 && (
<>
<div
className={`
absolute left-[50%] top-16 flex max-w-[80%]
translate-x-[calc(-50%+2rem)] gap-2 rounded-md bg-gray-200
dark:bg-olympus-900
`}
>
{!scrolledLeft && (
<FaChevronLeft
className={`
absolute left-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
<div className="flex gap-2 overflow-x-auto no-scrollbar p-2" onScroll={(ev) => onScroll(ev.target)} ref={scrollRef}>
{reorderedActions.map((contextActionIt: ContextAction) => {
return (
<OlStateButton
key={contextActionIt.getId()}
checked={contextActionIt === contextAction}
icon={contextActionIt.getIcon()}
tooltip={contextActionIt.getLabel()}
borderColor={contextActionIt.getOptions().buttonColor}
onClick={() => {
if (contextActionIt.getOptions().executeImmediately) {
contextActionIt.executeCallback(null, null);
} else {
contextActionIt !== contextAction ? getApp().getMap().setContextAction(contextActionIt) : getApp().getMap().setContextAction(null);
}
}}
/>
);
})}
</div>
{!scrolledRight && (
<FaChevronRight
className={`
absolute right-0 h-full w-6 rounded-lg px-2 py-3.5
text-gray-200
dark:bg-olympus-900
`}
/>
)}
</div>
{contextAction && (
<div
className={`
absolute left-[50%] top-32 flex min-w-[300px]
translate-x-[calc(-50%+2rem)] items-center gap-2 rounded-md
bg-gray-200 p-4
dark:bg-olympus-800
`}
>
<FaInfoCircle
className={`
mr-2 hidden min-w-8 text-sm text-blue-500
md:block
`}
/>
<div
className={`
px-2
dark:text-gray-400
md:border-l-[1px] md:px-5
`}
>
{contextAction.getDescription()}
</div>
</div>
)}
</>
)}
</>
);
}

View File

@ -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"
></OlStateButton>
<OlStateButton
onClick={() => {
getApp().setState(appState !== OlympusState.GAME_MASTER ? OlympusState.GAME_MASTER : OlympusState.IDLE);
}}
checked={appState === OlympusState.GAME_MASTER}
icon={faCrown}
tooltip="Hide/show Game Master menu"
></OlStateButton>
</div>
</div>
<div className="flex w-16 flex-wrap content-end justify-center p-4">

View File

@ -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 (
<Menu
{...props}
@ -153,8 +159,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
>
{filteredBlueprints
.filter((blueprint) => blueprint.category === "aircraft")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityAircraft} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityAircraft}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -196,8 +211,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
>
{filteredBlueprints
.filter((blueprint) => blueprint.category === "helicopter")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityHelicopter} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityHelicopter}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -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 <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunitSam} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunitSam}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -240,8 +273,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
>
{filteredBlueprints
.filter((blueprint) => blueprint.canAAA)
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunitSam} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunitSam}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -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 <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityGroundunit} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityGroundunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -329,8 +380,17 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
>
{filteredBlueprints
.filter((blueprint) => blueprint.category === "navyunit")
.map((entry) => {
return <OlUnitListEntry key={entry.name} icon={olButtonsVisibilityNavyunit} blueprint={entry} onClick={() => setBlueprint(entry)} />;
.map((blueprint) => {
return (
<OlUnitListEntry
key={blueprint.name}
icon={olButtonsVisibilityNavyunit}
blueprint={blueprint}
onClick={() => setBlueprint(blueprint)}
showCost={showCost}
cost={blueprint.cost ?? 10}
/>
);
})}
</div>
</OlAccordion>
@ -370,7 +430,13 @@ export function SpawnMenu(props: { open: boolean; onClose: () => void; children?
</div>
)}
{!(blueprint === null) && <UnitSpawnMenu blueprint={blueprint} spawnAtLocation={true} />}
{!(blueprint === null) && (
<UnitSpawnMenu
blueprint={blueprint}
spawnAtLocation={true}
coalition={commandModeOptions.commandMode !== GAME_MASTER ? (commandModeOptions.commandMode === BLUE_COMMANDER ? "blue" : "red") : undefined}
/>
)}
{!(effect === null) && <EffectSpawnMenu effect={effect} />}
</>
</Menu>

View File

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

View File

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

View File

@ -143,38 +143,40 @@ export function UnitSpawnMenu(props: { blueprint: UnitBlueprint; spawnAtLocation
</div>
{["aircraft", "helicopter"].includes(props.blueprint.category) && (
<>
<div>
<div
className={`
flex flex-row content-center items-center justify-between
`}
>
<div className="flex flex-col">
<span
className={`
font-normal
dark:text-white
`}
>
Altitude
</span>
<span
className={`
font-bold
dark:text-blue-500
`}
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
{!props.airbase && (
<div>
<div
className={`
flex flex-row content-center items-center justify-between
`}
>
<div className="flex flex-col">
<span
className={`
font-normal
dark:text-white
`}
>
Altitude
</span>
<span
className={`
font-bold
dark:text-blue-500
`}
>{`${Intl.NumberFormat("en-US").format(spawnAltitude)} FT`}</span>
</div>
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
</div>
<OlLabelToggle toggled={spawnAltitudeType} leftLabel={"AGL"} rightLabel={"ASL"} onClick={() => setSpawnAltitudeType(!spawnAltitudeType)} />
<OlRangeSlider
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
value={spawnAltitude}
min={minAltitude}
max={maxAltitude}
step={altitudeStep}
/>
</div>
<OlRangeSlider
onChange={(ev) => setSpawnAltitude(Number(ev.target.value))}
value={spawnAltitude}
min={minAltitude}
max={maxAltitude}
step={altitudeStep}
/>
</div>
)}
<div>
<div className="flex flex-row content-center justify-between">
<span

View File

@ -35,6 +35,7 @@ import { ProtectionPrompt } from "./modals/protectionprompt";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import { AppStateChangedEvent, MapOptionsChangedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
export type OlympusUIState = {
mainMenuVisible: boolean;
@ -57,8 +58,6 @@ export function UI() {
const [loginError, setLoginError] = useState(false);
const [commandMode, setCommandMode] = useState(null as null | string);
const [airbase, setAirbase] = useState(null as null | Airbase);
const [formationLeader, setFormationLeader] = useState(null as null | Unit);
const [formationWingmen, setFormationWingmen] = useState(null as null | Unit[]);
@ -157,8 +156,9 @@ export function UI() {
/>
<DrawingMenu open={appState === OlympusState.DRAW} onClose={() => getApp().setState(OlympusState.IDLE)} />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)} airbase={airbase} /* TODO remove */ />
<AirbaseMenu open={appState === OlympusState.AIRBASE} onClose={() => getApp().setState(OlympusState.IDLE)}/>
<AudioMenu open={appState === OlympusState.AUDIO} onClose={() => getApp().setState(OlympusState.IDLE)} />
<GameMasterMenu open={appState === OlympusState.GAME_MASTER} onClose={() => getApp().setState(OlympusState.IDLE)} />
{/* TODO} <UnitExplosionMenu open={appState === OlympusState.MAIN_MENU} units={unitExplosionUnits} onClose={() => getApp().setState(OlympusState.IDLE)} /> {*/}
<JTACMenu open={appState === OlympusState.JTAC} onClose={() => getApp().setState(OlympusState.IDLE)} />

View File

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

View File

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

View File

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