mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
feat: Improved formation menu
This commit is contained in:
parent
0376d020e7
commit
711f6094f0
File diff suppressed because it is too large
Load Diff
172
databases/units/asd.py
Normal file
172
databases/units/asd.py
Normal file
@ -0,0 +1,172 @@
|
||||
import json
|
||||
file_path = 'aircraftdatabase.json'
|
||||
|
||||
aircraft_lengths = {
|
||||
"A-10C Warthog 2": 53 ,
|
||||
"A-20G Havoc": 52 ,
|
||||
"A-50 Mainstay": 152 ,
|
||||
"KJ-2000": 152 ,
|
||||
"AJS37 Viggen": 52 ,
|
||||
"AV8BNA Harrier": 46 ,
|
||||
"An-26B Curl": 78 ,
|
||||
"An-30M Clank": 80 ,
|
||||
"B-1B Lancer": 146,
|
||||
"B-52H Stratofortress": 159 ,
|
||||
"Bf-109K-4": 29 ,
|
||||
"C-101CC": 42 ,
|
||||
"C-130 Hercules": 97 ,
|
||||
"C-17A Globemaster": 174,
|
||||
"E-2D Hawkeye": 57 ,
|
||||
"E-3A Sentry": 152 ,
|
||||
"F-117A Nighthawk": 65 ,
|
||||
"F-14A-135-GR Tomcat": 62 ,
|
||||
"F-14B Tomcat": 62 ,
|
||||
"F-15C Eagle": 63 ,
|
||||
"F-16C Viper": 49 ,
|
||||
"F-4E Phantom II": 63 ,
|
||||
"F-5E Tiger": 47 ,
|
||||
"F-86F Sabre": 37 ,
|
||||
"F/A-18C": 56 ,
|
||||
"FW-190A8 Würger": 29 ,
|
||||
"FW-190D9 Dora": 33 ,
|
||||
"H-6 J Badger": 114 ,
|
||||
"I-16": 20 ,
|
||||
"IL-76MD Candid": 152 ,
|
||||
"IL-78M Midas": 152 ,
|
||||
"J-11A Flaming Dragon": 71 ,
|
||||
"JF-17 Thunder": 49 ,
|
||||
"KC-135 Stratotanker": 136 ,
|
||||
"KC-135 MPRS Stratotanker": 136 ,
|
||||
"L-39ZA": 40 ,
|
||||
"M-2000C Mirage": 47 ,
|
||||
"MB-339A": 36 ,
|
||||
"MQ-9 Reaper": 36 ,
|
||||
"MiG-15 Fagot": 33 ,
|
||||
"MiG-19 Farmer": 41 ,
|
||||
"MiG-21 Fishbed": 51 ,
|
||||
"MiG-23 Flogger": 54 ,
|
||||
"MiG-25PD Foxbat": 64 ,
|
||||
"MiG-25RBT Foxbat": 64 ,
|
||||
"MiG-27K Flogger-D": 56 ,
|
||||
"MiG-29A Fulcrum": 56 ,
|
||||
"MiG-29S Fulcrum": 56 ,
|
||||
"MiG-31 Foxhound": 74 ,
|
||||
"Mirage-F1EE": 49 ,
|
||||
"Mosquito FB MkVI": 41 ,
|
||||
"P-47D Thunderbolt": 36 ,
|
||||
"P-51D Mustang": 32 ,
|
||||
"S-3B Tanker": 53 ,
|
||||
"Su-17M4 Fitter": 61 ,
|
||||
"Su-24M Fencer": 75 ,
|
||||
"Su-25T Frogfoot": 50 ,
|
||||
"Su-27 Flanker": 71 ,
|
||||
"Su-30 Super Flanker": 72 ,
|
||||
"Su-33 Navy Flanker": 72 ,
|
||||
"Su-34 Hellduck": 76 ,
|
||||
"Tornado GR4": 54 ,
|
||||
"Tornado IDS": 54 ,
|
||||
"Tu-142 Bear": 177 ,
|
||||
"Tu-160 Blackjack": 177 ,
|
||||
"Tu-22M3 Backfire": 139 ,
|
||||
"Tu-95MS Bear": 162 ,
|
||||
"F-15E Strike Eagle": 63 ,
|
||||
"F-14A Tomcat": 62 ,
|
||||
"Su-25TM": 50 ,
|
||||
"Su-24MR Fencer": 75 ,
|
||||
"S-3B": 53 ,
|
||||
"Mirage 2000-5": 47 ,
|
||||
"MiG-29G Fulcrum": 56 ,
|
||||
"F-16C Viper Block 50": 49 ,
|
||||
"F-16C BL.52D": 49 ,
|
||||
"F-16A Viper": 49 ,
|
||||
"F-16A Viper MLU": 49 ,
|
||||
"RQ-1A Predator": 27 ,
|
||||
"Yak-40": 64 ,
|
||||
"Spitfire Mk IX": 30 ,
|
||||
"Spitfire Mk IX CW": 30 ,
|
||||
"P-51D Mustang": 32 ,
|
||||
"P-47D-30": 36 ,
|
||||
"A-10A Warthog": 53 ,
|
||||
"A-10C Warthog": 53 ,
|
||||
"KC-130": 97 ,
|
||||
"C-101EB": 42 ,
|
||||
"MQ-9 Reaper": 36 ,
|
||||
"Christen Eagle II": 19 ,
|
||||
"F-5E": 47 ,
|
||||
"F/A-18A": 56 ,
|
||||
"F/A-18C": 56 ,
|
||||
"Hawk": 39 ,
|
||||
"L-39C": 40 ,
|
||||
"MB-339APAN": 36 ,
|
||||
"Mirage-F1C": 49 ,
|
||||
"Mirage-F1CE": 49 ,
|
||||
"Mirage-F1M-EE": 49 ,
|
||||
"Mirage-F1M-CE": 49 ,
|
||||
"Mirage-F1C-200": 49 ,
|
||||
"Mirage-F1EH": 49 ,
|
||||
"Mirage-F1CH": 49 ,
|
||||
"Mirage-F1JA": 49 ,
|
||||
"Mirage-F1CG": 49 ,
|
||||
"Mirage-F1CZ": 49 ,
|
||||
"Mirage-F1CJ": 49 ,
|
||||
"Mirage-F1CK": 49 ,
|
||||
"Mirage-F1EQ": 49 ,
|
||||
"Mirage-F1ED": 49 ,
|
||||
"Mirage-F1EDA": 49 ,
|
||||
"Mirage-F1CR": 49 ,
|
||||
"Mirage-F1CT": 49 ,
|
||||
"Mirage-F1B": 49 ,
|
||||
"Mirage-F1BE": 49 ,
|
||||
"Mirage-F1BQ": 49 ,
|
||||
"Mirage-F1BD": 49 ,
|
||||
"Mirage-F1DDA": 49 ,
|
||||
"SU-25TM": 49.5 ,
|
||||
"MIRAGE 2000-5": 47.1 ,
|
||||
"RQ-1A PREDATOR": 27.2 ,
|
||||
"YAK-40": 77.1 ,
|
||||
"Spitfire Mk 9": 29.9 ,
|
||||
"Spitfire Mk 9 CW": 29.9 ,
|
||||
"CHRISTEN EAGLE II": 20.5 ,
|
||||
"MIRAGE-F1C": 49.5 ,
|
||||
"MIRAGE-F1CE": 49.5 ,
|
||||
"MIRAGE-F1M-EE": 49.5 ,
|
||||
"MIRAGE-F1M-CE": 49.5 ,
|
||||
"MIRAGE-F1C-200": 49.5 ,
|
||||
"MIRAGE-F1EH": 49.5 ,
|
||||
"MIRAGE-F1CH": 49.5 ,
|
||||
"MIRAGE-F1JA": 49.5 ,
|
||||
"MIRAGE-F1CG": 49.5 ,
|
||||
"MIRAGE-F1CZ": 49.5 ,
|
||||
"MIRAGE-F1CJ": 49.5 ,
|
||||
"MIRAGE-F1CK": 49.5 ,
|
||||
"MIRAGE-F1EQ": 49.5 ,
|
||||
"MIRAGE-F1ED": 49.5 ,
|
||||
"MIRAGE-F1EDA": 49.5 ,
|
||||
"MIRAGE-F1CR": 49.5 ,
|
||||
"MIRAGE-F1CT": 49.5 ,
|
||||
"MIRAGE-F1B": 49.5 ,
|
||||
"MIRAGE-F1BE": 49.5 ,
|
||||
"MIRAGE-F1BQ": 49.5 ,
|
||||
"MIRAGE-F1BD": 49.5 ,
|
||||
"MIRAGE-F1DDA": 49.5 ,
|
||||
"YAK-52": 26.2 ,
|
||||
"B-17G Flying Fortress": 74.7 ,
|
||||
"JU-88 A4": 56.8 ,
|
||||
"C-47 Dakota": 63.5 ,
|
||||
"TF-51D": 32.8 ,
|
||||
"F-4E-45MC": 58.3
|
||||
}
|
||||
|
||||
with open(file_path, 'r', -1, 'utf-8') as f:
|
||||
aircraft_data = json.load(f)
|
||||
|
||||
|
||||
aircraft_labels = [entry.get("label", "") for entry in aircraft_data.values()]
|
||||
|
||||
for entry in aircraft_data.values():
|
||||
if entry.get("label", "") in aircraft_lengths:
|
||||
entry["length"] = aircraft_lengths[entry.get("label", "")]
|
||||
|
||||
|
||||
with open(file_path, 'w', -1, 'utf-8') as f:
|
||||
json.dump(aircraft_data, f, indent=4)
|
||||
File diff suppressed because it is too large
Load Diff
1
frontend/react/public/images/others/caret.svg
Normal file
1
frontend/react/public/images/others/caret.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"/></svg>
|
||||
|
After Width: | Height: | Size: 388 B |
Binary file not shown.
|
Before Width: | Height: | Size: 2.6 KiB |
BIN
frontend/react/public/images/units/sh-3.png
Normal file
BIN
frontend/react/public/images/units/sh-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@ -273,6 +273,18 @@ export const defaultMapLayers = {
|
||||
},
|
||||
};
|
||||
|
||||
export const formationTypes = {
|
||||
"echelon-lh": "Echelon left",
|
||||
"echelon-rh": "Echelon right",
|
||||
"line-abreast-rh": "Line abreast right",
|
||||
"line-abreast-lh": "Line abreast left",
|
||||
trail: "Trail",
|
||||
front: "Front",
|
||||
diamond: "Diamond",
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
|
||||
export enum OlympusState {
|
||||
NOT_INITIALIZED = "Not initialized",
|
||||
SERVER = "Server",
|
||||
|
||||
@ -16,9 +16,13 @@ import { Weapon } from "./weapon/weapon";
|
||||
|
||||
export class BaseOlympusEvent {
|
||||
static on(callback: () => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback();
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback();
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch() {
|
||||
@ -29,9 +33,13 @@ export class BaseOlympusEvent {
|
||||
|
||||
export class BaseUnitEvent {
|
||||
static on(callback: (unit: Unit) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.unit);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.unit);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(unit: Unit) {
|
||||
@ -41,12 +49,33 @@ export class BaseUnitEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseUnitsEvent {
|
||||
static on(callback: (selectedUnits: Unit[]) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(units: Unit[]) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: units }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
/************** App events ***************/
|
||||
export class AppStateChangedEvent {
|
||||
static on(callback: (state: OlympusState, subState: OlympusSubState) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.state, ev.detail.subState);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.state, ev.detail.subState);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(state: OlympusState, subState: OlympusSubState) {
|
||||
@ -59,9 +88,13 @@ export class AppStateChangedEvent {
|
||||
|
||||
export class ConfigLoadedEvent {
|
||||
static on(callback: (config: OlympusConfig) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(config: OlympusConfig) {
|
||||
@ -73,9 +106,13 @@ export class ConfigLoadedEvent {
|
||||
|
||||
export class ServerStatusUpdatedEvent {
|
||||
static on(callback: (serverStatus: ServerStatus) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.serverStatus);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.serverStatus);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(serverStatus: ServerStatus) {
|
||||
@ -88,9 +125,13 @@ export class UnitDatabaseLoadedEvent extends BaseOlympusEvent {}
|
||||
|
||||
export class InfoPopupEvent {
|
||||
static on(callback: (messages: string[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.messages);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.messages);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(messages: string[]) {
|
||||
@ -101,9 +142,13 @@ export class InfoPopupEvent {
|
||||
|
||||
export class ShortcutsChangedEvent {
|
||||
static on(callback: (shortcuts: { [key: string]: Shortcut }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcuts);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcuts);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(shortcuts: { [key: string]: Shortcut }) {
|
||||
@ -114,9 +159,13 @@ export class ShortcutsChangedEvent {
|
||||
|
||||
export class ShortcutChangedEvent {
|
||||
static on(callback: (shortcut: Shortcut) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcut);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcut);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(shortcut: Shortcut) {
|
||||
@ -127,9 +176,13 @@ export class ShortcutChangedEvent {
|
||||
|
||||
export class BindShortcutRequestEvent {
|
||||
static on(callback: (shortcut: Shortcut) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcut);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.shortcut);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(shortcut: Shortcut) {
|
||||
@ -140,9 +193,13 @@ export class BindShortcutRequestEvent {
|
||||
|
||||
export class ModalEvent {
|
||||
static on(callback: (modal: boolean) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.modal);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.modal);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(modal: boolean) {
|
||||
@ -153,9 +210,13 @@ export class ModalEvent {
|
||||
|
||||
export class SessionDataChangedEvent {
|
||||
static on(callback: (sessionData: SessionData) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.sessionData);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.sessionData);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(sessionData: SessionData) {
|
||||
@ -170,9 +231,13 @@ export class SessionDataLoadedEvent extends SessionDataChangedEvent {}
|
||||
/************** Map events ***************/
|
||||
export class MouseMovedEvent {
|
||||
static on(callback: (latlng: LatLng, elevation: number) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng, ev.detail.elevation);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng, ev.detail.elevation);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(latlng: LatLng, elevation?: number) {
|
||||
@ -183,9 +248,13 @@ export class MouseMovedEvent {
|
||||
|
||||
export class HiddenTypesChangedEvent {
|
||||
static on(callback: (hiddenTypes: MapHiddenTypes) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.hiddenTypes);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.hiddenTypes);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(hiddenTypes: MapHiddenTypes) {
|
||||
@ -196,9 +265,13 @@ export class HiddenTypesChangedEvent {
|
||||
|
||||
export class MapOptionsChangedEvent {
|
||||
static on(callback: (mapOptions: MapOptions) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.mapOptions);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.mapOptions);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(mapOptions: MapOptions) {
|
||||
@ -209,9 +282,13 @@ export class MapOptionsChangedEvent {
|
||||
|
||||
export class MapSourceChangedEvent {
|
||||
static on(callback: (source: string) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.source);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.source);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(source: string) {
|
||||
@ -222,9 +299,13 @@ export class MapSourceChangedEvent {
|
||||
|
||||
export class CoalitionAreaSelectedEvent {
|
||||
static on(callback: (coalitionArea: CoalitionCircle | CoalitionPolygon | null) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.coalitionArea);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.coalitionArea);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(coalitionArea: CoalitionCircle | CoalitionPolygon | null) {
|
||||
@ -237,9 +318,13 @@ export class CoalitionAreaChangedEvent extends CoalitionAreaSelectedEvent {}
|
||||
|
||||
export class CoalitionAreasChangedEvent {
|
||||
static on(callback: (coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.coalitionAreas);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.coalitionAreas);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(coalitionAreas: (CoalitionCircle | CoalitionPolygon)[]) {
|
||||
@ -250,9 +335,13 @@ export class CoalitionAreasChangedEvent {
|
||||
|
||||
export class AirbaseSelectedEvent {
|
||||
static on(callback: (airbase: Airbase | null) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.airbase);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.airbase);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(airbase: Airbase | null) {
|
||||
@ -263,35 +352,47 @@ export class AirbaseSelectedEvent {
|
||||
|
||||
export class SelectionEnabledChangedEvent {
|
||||
static on(callback: (enabled: boolean) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.enabled);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.enabled);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(enabled: boolean) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class PasteEnabledChangedEvent {
|
||||
static on(callback: (enabled: boolean) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.enabled);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.enabled);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(enabled: boolean) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { enabled } }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class ContactsUpdatedEvent {
|
||||
static on(callback: () => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback();
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback();
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch() {
|
||||
@ -302,9 +403,13 @@ export class ContactsUpdatedEvent {
|
||||
|
||||
export class ContextActionSetChangedEvent {
|
||||
static on(callback: (contextActionSet: ContextActionSet | null) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.contextActionSet);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.contextActionSet);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(contextActionSet: ContextActionSet | null) {
|
||||
@ -315,9 +420,13 @@ export class ContextActionSetChangedEvent {
|
||||
|
||||
export class ContextActionChangedEvent {
|
||||
static on(callback: (contextAction: ContextAction | null) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.contextAction);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.contextAction);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(contextAction: ContextAction | null) {
|
||||
@ -328,9 +437,13 @@ export class ContextActionChangedEvent {
|
||||
|
||||
export class CopiedUnitsEvents {
|
||||
static on(callback: (unitsData: UnitData[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.unitsData);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.unitsData);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(unitsData: UnitData[]) {
|
||||
@ -339,6 +452,8 @@ export class CopiedUnitsEvents {
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectionClearedEvent extends BaseOlympusEvent {}
|
||||
|
||||
export class UnitUpdatedEvent extends BaseUnitEvent {
|
||||
static dispatch(unit: Unit) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: { unit } }));
|
||||
@ -348,53 +463,25 @@ export class UnitUpdatedEvent extends BaseUnitEvent {
|
||||
export class UnitSelectedEvent extends BaseUnitEvent {}
|
||||
export class UnitDeselectedEvent extends BaseUnitEvent {}
|
||||
export class UnitDeadEvent extends BaseUnitEvent {}
|
||||
export class SelectionClearedEvent extends BaseOlympusEvent {}
|
||||
|
||||
export class UnitsRefreshedEvent {
|
||||
static on(callback: (units: { [ID: number]: Unit }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
}
|
||||
|
||||
static dispatch(units: { [ID: number]: Unit }) {
|
||||
export class UnitsUpdatedEvent extends BaseUnitsEvent {
|
||||
static dispatch(units: Unit[]) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: units }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
export class WeaponsRefreshedEvent {
|
||||
static on(callback: (weapons: { [ID: number]: Weapon }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
}
|
||||
|
||||
static dispatch(weapons: { [ID: number]: Weapon }) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: weapons }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class SelectedUnitsChangedEvent {
|
||||
static on(callback: (selectedUnits: Unit[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
}
|
||||
|
||||
static dispatch(selectedUnits: Unit[]) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: selectedUnits }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
// Logging disabled since periodic
|
||||
}
|
||||
}
|
||||
export class UnitsRefreshedEvent extends BaseUnitsEvent {}
|
||||
export class SelectedUnitsChangedEvent extends BaseUnitsEvent {}
|
||||
|
||||
export class UnitExplosionRequestEvent {
|
||||
static on(callback: (units: Unit[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.units);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.units);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(units: Unit[]) {
|
||||
@ -405,9 +492,13 @@ export class UnitExplosionRequestEvent {
|
||||
|
||||
export class FormationCreationRequestEvent {
|
||||
static on(callback: (leader: Unit, wingmen: Unit[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.leader, ev.detail.wingmen);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.leader, ev.detail.wingmen);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(leader: Unit, wingmen: Unit[]) {
|
||||
@ -418,9 +509,13 @@ export class FormationCreationRequestEvent {
|
||||
|
||||
export class MapContextMenuRequestEvent {
|
||||
static on(callback: (latlng: L.LatLng) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(latlng: L.LatLng) {
|
||||
@ -431,9 +526,13 @@ export class MapContextMenuRequestEvent {
|
||||
|
||||
export class UnitContextMenuRequestEvent {
|
||||
static on(callback: (unit: Unit) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.unit);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.unit);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(unit: Unit) {
|
||||
@ -444,9 +543,13 @@ export class UnitContextMenuRequestEvent {
|
||||
|
||||
export class SpawnContextMenuRequestEvent {
|
||||
static on(callback: (latlng: L.LatLng) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.latlng);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(latlng: L.LatLng) {
|
||||
@ -457,9 +560,13 @@ export class SpawnContextMenuRequestEvent {
|
||||
|
||||
export class HotgroupsChangedEvent {
|
||||
static on(callback: (hotgroups: { [key: number]: Unit[] }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.hotgroups);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.hotgroups);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(hotgroups: { [key: number]: Unit[] }) {
|
||||
@ -470,9 +577,13 @@ export class HotgroupsChangedEvent {
|
||||
|
||||
export class StarredSpawnsChangedEvent {
|
||||
static on(callback: (starredSpawns: { [key: number]: SpawnRequestTable }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.starredSpawns);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.starredSpawns);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(starredSpawns: { [key: number]: SpawnRequestTable }) {
|
||||
@ -483,9 +594,13 @@ export class StarredSpawnsChangedEvent {
|
||||
|
||||
export class AWACSReferenceChangedEvent {
|
||||
static on(callback: (unit: Unit | null) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(unit: Unit | null) {
|
||||
@ -497,9 +612,13 @@ export class AWACSReferenceChangedEvent {
|
||||
/************** Command mode events ***************/
|
||||
export class CommandModeOptionsChangedEvent {
|
||||
static on(callback: (options: CommandModeOptions) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(options: CommandModeOptions) {
|
||||
@ -511,9 +630,13 @@ export class CommandModeOptionsChangedEvent {
|
||||
/************** Audio backend events ***************/
|
||||
export class AudioSourcesChangedEvent {
|
||||
static on(callback: (audioSources: AudioSource[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.audioSources);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.audioSources);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(audioSources: AudioSource[]) {
|
||||
@ -525,9 +648,13 @@ export class AudioSourcesChangedEvent {
|
||||
|
||||
export class AudioSinksChangedEvent {
|
||||
static on(callback: (audioSinks: AudioSink[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.audioSinks);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.audioSinks);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(audioSinks: AudioSink[]) {
|
||||
@ -539,9 +666,13 @@ export class AudioSinksChangedEvent {
|
||||
|
||||
export class SRSClientsChangedEvent {
|
||||
static on(callback: () => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback();
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback();
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch() {
|
||||
@ -552,9 +683,13 @@ export class SRSClientsChangedEvent {
|
||||
|
||||
export class AudioManagerStateChangedEvent {
|
||||
static on(callback: (state: boolean) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.state);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.state);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(state: boolean) {
|
||||
@ -565,9 +700,13 @@ export class AudioManagerStateChangedEvent {
|
||||
|
||||
export class AudioManagerDevicesChangedEvent {
|
||||
static on(callback: (devices: MediaDeviceInfo[]) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.devices);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.devices);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(devices: MediaDeviceInfo[]) {
|
||||
@ -578,9 +717,13 @@ export class AudioManagerDevicesChangedEvent {
|
||||
|
||||
export class AudioManagerInputChangedEvent {
|
||||
static on(callback: (input: MediaDeviceInfo) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.input);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.input);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(input: MediaDeviceInfo) {
|
||||
@ -591,9 +734,13 @@ export class AudioManagerInputChangedEvent {
|
||||
|
||||
export class AudioManagerOutputChangedEvent {
|
||||
static on(callback: (output: MediaDeviceInfo) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.output);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.output);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(output: MediaDeviceInfo) {
|
||||
@ -605,9 +752,13 @@ export class AudioManagerOutputChangedEvent {
|
||||
/************** Mission data events ***************/
|
||||
export class BullseyesDataChangedEvent {
|
||||
static on(callback: (bullseyes: { [name: string]: Bullseye }) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.bullseyes);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.bullseyes);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(bullseyes: { [name: string]: Bullseye }) {
|
||||
@ -618,9 +769,13 @@ export class BullseyesDataChangedEvent {
|
||||
|
||||
export class MissionDataChangedEvent {
|
||||
static on(callback: (missionData: MissionData) => void, singleShot = false) {
|
||||
document.addEventListener(this.name, (ev: CustomEventInit) => {
|
||||
callback(ev.detail.missionData);
|
||||
}, {once: singleShot});
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail.missionData);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(missionData: MissionData) {
|
||||
@ -628,3 +783,21 @@ export class MissionDataChangedEvent {
|
||||
// Logging disabled since periodic
|
||||
}
|
||||
}
|
||||
|
||||
/************** Other events ***************/
|
||||
export class WeaponsRefreshedEvent {
|
||||
static on(callback: (weapons: Weapon[]) => void, singleShot = false) {
|
||||
document.addEventListener(
|
||||
this.name,
|
||||
(ev: CustomEventInit) => {
|
||||
callback(ev.detail);
|
||||
},
|
||||
{ once: singleShot }
|
||||
);
|
||||
}
|
||||
|
||||
static dispatch(weapons: Weapon[]) {
|
||||
document.dispatchEvent(new CustomEvent(this.name, { detail: weapons }));
|
||||
console.log(`Event ${this.name} dispatched`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,6 +298,7 @@ export interface UnitBlueprint {
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
length?: number;
|
||||
}
|
||||
|
||||
export interface AirbaseOptions {
|
||||
|
||||
@ -1054,7 +1054,10 @@ export class Map extends L.Map {
|
||||
getApp().getCoalitionAreasManager().onDoubleClick(e);
|
||||
} else {
|
||||
if (getApp().getSubState() === NO_SUBSTATE) getApp().setState(OlympusState.IDLE);
|
||||
else getApp().setState(getApp().getState());
|
||||
else {
|
||||
if (getApp().getState() === OlympusState.UNIT_CONTROL) getApp().setState(OlympusState.IDLE);
|
||||
else getApp().setState(getApp().getState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -445,3 +445,52 @@ export function deepCopyTable(table) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function computeStandardFormationOffset(formation, idx) {
|
||||
let offset = { x: 0, y: 0 };
|
||||
if (formation === "trail") {
|
||||
offset.y = 50 * idx;
|
||||
offset.x = 0;
|
||||
} else if (formation === "echelon-lh" || formation == "custom" /* default fallback if needed */) {
|
||||
offset.y = 50 * idx;
|
||||
offset.x = -50 * idx;
|
||||
} else if (formation === "echelon-rh") {
|
||||
offset.y = 50 * idx;
|
||||
offset.x = 50 * idx;
|
||||
} else if (formation === "line-abreast-lh") {
|
||||
offset.y = 0;
|
||||
offset.x = -50 * idx;
|
||||
} else if (formation === "line-abreast-rh") {
|
||||
offset.y = 0;
|
||||
offset.x = 50 * idx;
|
||||
} else if (formation === "front") {
|
||||
offset.y = -100 * idx;
|
||||
offset.x = 0;
|
||||
} else if (formation === "diamond") {
|
||||
var xr = 0;
|
||||
var yr = 1;
|
||||
var zr = -1;
|
||||
var layer = 1;
|
||||
|
||||
for (let i = 0; i < idx; i++) {
|
||||
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
|
||||
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
|
||||
offset = { x: xl * 50, y: yl * 50 };
|
||||
if (yr == 0) {
|
||||
layer++;
|
||||
xr = 0;
|
||||
yr = layer;
|
||||
zr = -layer;
|
||||
} else {
|
||||
if (xr < layer) {
|
||||
xr++;
|
||||
zr--;
|
||||
} else {
|
||||
yr--;
|
||||
zr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
const [finalPosition, setFinalPosition] = useState({ x: props.initialPosition.x, y: props.initialPosition.y });
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [position, setPosition] = useState({ x: props.initialPosition.x, y: props.initialPosition.y });
|
||||
const [dragging, isDragging] = useState(false);
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
if (count !== props.count) {
|
||||
setCount(props.count)
|
||||
setFinalPosition({ x: props.initialPosition.x, y: props.initialPosition.y })
|
||||
setPosition({ x: props.initialPosition.x, y: props.initialPosition.y })
|
||||
}
|
||||
|
||||
const handleMouseUp = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
setIsDragging(false);
|
||||
isDragging(false);
|
||||
};
|
||||
|
||||
const handleMouseDown = (evt) => {
|
||||
@ -25,14 +25,14 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDragging(true);
|
||||
isDragging(true);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(evt) => {
|
||||
const { current: draggableElement } = props.ref;
|
||||
|
||||
if (!isDragging || !draggableElement) return;
|
||||
if (!dragging || !draggableElement) return;
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
@ -43,12 +43,12 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
const [mouseX, mouseY] = [evt.clientX, evt.clientY];
|
||||
const [parentTop, parentLeft, parentWidth, parentHeight] = [parentRect.top, parentRect.left, parentRect.width, parentRect.height];
|
||||
|
||||
setFinalPosition({
|
||||
x: Math.round(Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)) / 10) * 10,
|
||||
y: Math.round(Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)) / 10) * 10,
|
||||
setPosition({
|
||||
x: Math.max(width / 2, Math.min(mouseX - parentLeft, parentWidth - width / 2)),
|
||||
y: Math.max(height / 2, Math.min(mouseY - parentTop, parentHeight - height / 2)),
|
||||
});
|
||||
},
|
||||
[isDragging, props.ref]
|
||||
[dragging, props.ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,8 +61,13 @@ export const useDrag = (props: { ref, initialPosition, count}) => {
|
||||
};
|
||||
}, [handleMouseMove]);
|
||||
|
||||
const forcePosition = (x, y) => {
|
||||
setPosition({x, y});
|
||||
}
|
||||
|
||||
return {
|
||||
position: finalPosition,
|
||||
handleMouseDown
|
||||
position: position,
|
||||
handleMouseDown,
|
||||
forcePosition
|
||||
};
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ import { MissionData, UnitData } from "../../interfaces";
|
||||
export function ImportExportModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [units, setUnits] = useState({} as { [ID: number]: Unit });
|
||||
const [units, setUnits] = useState([] as Unit[]);
|
||||
const [missionData, setMissionData] = useState({} as MissionData);
|
||||
const [importData, setImportData] = useState({} as { [key: string]: UnitData[] });
|
||||
|
||||
@ -86,7 +86,7 @@ export function ImportExportModal(props: { open: boolean }) {
|
||||
}
|
||||
}, [appState, appSubState]);
|
||||
|
||||
const selectableUnits = Object.values(units).filter((unit) => {
|
||||
const selectableUnits = units.filter((unit) => {
|
||||
return (
|
||||
unit.getAlive() &&
|
||||
!unit.getHuman() &&
|
||||
|
||||
62
frontend/react/src/ui/panels/components/draggable.tsx
Normal file
62
frontend/react/src/ui/panels/components/draggable.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export function Draggable(props: {
|
||||
position: { x: number; y: number };
|
||||
children: JSX.Element | JSX.Element[];
|
||||
disabled: boolean;
|
||||
onPositionChange: (position: { x: number; y: number }) => void;
|
||||
}) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
if (!props.disabled) props.onPositionChange({ x: props.position.x + e.clientX - refPosition.x, y: props.position.y + e.clientY - refPosition.y });
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`absolute translate-x-[-50%] translate-y-[-50%]`}
|
||||
style={{
|
||||
top: props.position.y,
|
||||
left: props.position.x,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
setDragging(true);
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Draggable } from "./draggable";
|
||||
import { Unit } from "../../../unit/unit";
|
||||
|
||||
export function DraggableSilhouette(props: {
|
||||
position: { x: number; y: number };
|
||||
unit: Unit;
|
||||
zoom: number;
|
||||
scale: number;
|
||||
disabled: boolean;
|
||||
angle: number;
|
||||
onPositionChange: (position: { x: number; y: number }) => void;
|
||||
src?: string;
|
||||
}) {
|
||||
return (
|
||||
<Draggable position={props.position} onPositionChange={props.onPositionChange} disabled={props.disabled}>
|
||||
<img
|
||||
data-disabled = {props.disabled}
|
||||
className={`
|
||||
align-center opacity-80 invert
|
||||
data-[disabled=false]:cursor-move
|
||||
`}
|
||||
src={props.src ?? `./images/units/${props.unit?.getBlueprint()?.filename}`}
|
||||
style={{
|
||||
maxWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
|
||||
minWidth: `${Math.round((props.scale * (props.disabled ? 20 : (props.unit?.getBlueprint()?.length ?? 50))) / Math.min(3, props.disabled? 1: props.zoom))}px`,
|
||||
rotate: `${props.disabled? props.angle: 90}deg`,
|
||||
}}
|
||||
></img>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
259
frontend/react/src/ui/panels/components/formationcanvas.tsx
Normal file
259
frontend/react/src/ui/panels/components/formationcanvas.tsx
Normal file
@ -0,0 +1,259 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Unit } from "../../../unit/unit";
|
||||
import { DraggableSilhouette } from "./draggablesilhouette";
|
||||
import { FaCompressArrowsAlt, FaExclamationTriangle, FaExpand, FaExpandArrowsAlt } from "react-icons/fa";
|
||||
|
||||
const FT_TO_PX = 1;
|
||||
|
||||
export function FormationCanvas(props: {
|
||||
units: Unit[];
|
||||
unitPositions: { x: number; y: number }[];
|
||||
setUnitPositions: (positions: { x: number; y: number }[]) => void;
|
||||
}) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [refPosition, setRefPosition] = useState({ x: 0, y: 0 });
|
||||
const [dragDelta, setDragDelta] = useState({ x: 0, y: 0 });
|
||||
const [zoom, setZoom] = useState(1);
|
||||
|
||||
/* Init references and hooks */
|
||||
const containerRef = useRef(null);
|
||||
|
||||
let containerCenter = { x: 0, y: 0 };
|
||||
let containerSize = { width: 0, height: 0 };
|
||||
if (containerRef.current) {
|
||||
const containerDiv = containerRef.current as HTMLDivElement;
|
||||
containerCenter = {
|
||||
x: containerDiv.clientWidth / 2,
|
||||
y: containerDiv.clientHeight / 3,
|
||||
};
|
||||
containerSize = { width: containerDiv.clientWidth, height: containerDiv.clientHeight };
|
||||
}
|
||||
|
||||
/* Handle mouse movement, for dragging of the scene */
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragDelta({
|
||||
x: dragDelta.x + e.clientX - refPosition.x,
|
||||
y: dragDelta.y + e.clientY - refPosition.y,
|
||||
});
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
/* Handle mouse up, to stop dragging the scene */
|
||||
const handleMouseUp = useCallback(
|
||||
(e) => {
|
||||
if (dragging) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[dragging, refPosition]
|
||||
);
|
||||
|
||||
/* Register the dragging handlers */
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [handleMouseMove, handleMouseUp]);
|
||||
|
||||
let referenceDistance = 200 * zoom;
|
||||
if (referenceDistance < 250) referenceDistance = 100;
|
||||
else if (referenceDistance < 500) referenceDistance = 250;
|
||||
else if (referenceDistance < 1000) referenceDistance = 500;
|
||||
else if (referenceDistance < 3000) referenceDistance = 1000;
|
||||
else if (referenceDistance < 5280 * 2) referenceDistance = 5280;
|
||||
else referenceDistance = 5280 * 2;
|
||||
const referenceWidth = referenceDistance / zoom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-fit gap-1 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position) => {
|
||||
return {
|
||||
x: position.x * 1.1,
|
||||
y: position.y * 1.1,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg p-2 text-md flex content-center justify-center gap-2
|
||||
bg-gray-600 font-medium text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaExpandArrowsAlt className="my-auto" /> <div> Loosen </div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setUnitPositions(
|
||||
props.unitPositions.map((position) => {
|
||||
return {
|
||||
x: position.x * 0.9,
|
||||
y: position.y * 0.9,
|
||||
};
|
||||
})
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
rounded-lg p-2 text-md flex content-center justify-center gap-2
|
||||
bg-gray-600 font-medium text-white
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
<FaCompressArrowsAlt className="my-auto" /> <div>Tighten</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-dragging={dragging}
|
||||
className={`
|
||||
relative h-full w-full cursor-grab overflow-hidden rounded-md
|
||||
border-[1px] border-white/20 bg-white/10
|
||||
data-[dragging=true]:cursor-grabbing
|
||||
`}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY > 0) setZoom(Math.max(Math.min(zoom * 1.1, 100), 0.8));
|
||||
else setZoom(Math.max(Math.min(zoom * 0.9, 100), 0.8));
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setRefPosition({ x: e.clientX, y: e.clientY });
|
||||
setDragging(true);
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
setDragDelta({ x: 0, y: 0 });
|
||||
setZoom(1);
|
||||
}}
|
||||
>
|
||||
<div className={`h-full w-full`} ref={containerRef}>
|
||||
{props.units.map((unit, idx) => {
|
||||
let unitPosition = props.unitPositions[idx]
|
||||
? {
|
||||
x:
|
||||
props.unitPositions[0].x +
|
||||
(((props.unitPositions[idx].x - props.unitPositions[0].x) * 1) / zoom) * FT_TO_PX +
|
||||
dragDelta.x +
|
||||
containerCenter.x,
|
||||
y:
|
||||
props.unitPositions[0].y +
|
||||
(((props.unitPositions[idx].y - props.unitPositions[0].y) * 1) / zoom) * FT_TO_PX +
|
||||
dragDelta.y +
|
||||
containerCenter.y,
|
||||
}
|
||||
: { x: 0, y: 0 };
|
||||
|
||||
let disabled = false;
|
||||
let overflowX = null as null | string;
|
||||
let overflowY = null as null | string;
|
||||
if (unitPosition.x < 0) {
|
||||
disabled = true;
|
||||
unitPosition.x = 10;
|
||||
overflowX = "left";
|
||||
} else if (unitPosition.x > containerSize.width) {
|
||||
disabled = true;
|
||||
unitPosition.x = containerSize.width - 10;
|
||||
overflowX = "right";
|
||||
}
|
||||
|
||||
if (unitPosition.y < 0) {
|
||||
disabled = true;
|
||||
unitPosition.y = 10;
|
||||
overflowY = "top";
|
||||
} else if (unitPosition.y > containerSize.height) {
|
||||
disabled = true;
|
||||
unitPosition.y = containerSize.height - 10;
|
||||
overflowY = "bottom";
|
||||
}
|
||||
|
||||
let angle = 0;
|
||||
if (overflowX === "right") {
|
||||
if (overflowY === "top") angle = 45;
|
||||
else if (overflowY === "bottom") angle = 135;
|
||||
else angle = 90;
|
||||
} else if (overflowX === "left") {
|
||||
if (overflowY === "top") angle = 360 - 45;
|
||||
else if (overflowY === "bottom") angle = 360 - 135;
|
||||
else angle = 360 - 90;
|
||||
} else {
|
||||
if (overflowY === "top") angle = 0;
|
||||
else if (overflowY === "bottom") angle = 180;
|
||||
else angle = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<DraggableSilhouette
|
||||
key={idx}
|
||||
zoom={zoom}
|
||||
position={unitPosition}
|
||||
unit={unit}
|
||||
scale={FT_TO_PX}
|
||||
disabled={disabled}
|
||||
onPositionChange={({ x, y }) => {
|
||||
props.unitPositions[idx] = {
|
||||
x: ((x - props.unitPositions[0].x - dragDelta.x - containerCenter.x) * zoom) / FT_TO_PX - props.unitPositions[0].x,
|
||||
y: ((y - props.unitPositions[0].y - dragDelta.y - containerCenter.y) * zoom) / FT_TO_PX - props.unitPositions[0].y,
|
||||
};
|
||||
props.setUnitPositions([...props.unitPositions]);
|
||||
}}
|
||||
src={disabled ? `./images/others/caret.svg` : undefined}
|
||||
angle={angle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{zoom > 3 && (
|
||||
<div className="absolute bottom-2 left-2 flex gap-2">
|
||||
<FaExclamationTriangle className={`text-xl text-yellow-400`} />
|
||||
<div className="text-white">Silhouettes not to scale!</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute left-0 top-2 m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
relative left-6 top-4 h-4 border-2 border-white
|
||||
border-t-transparent text-center text-white
|
||||
`}
|
||||
style={{
|
||||
width: `${referenceWidth}px`,
|
||||
}}
|
||||
>
|
||||
{referenceDistance === 5280 && <div className="translate-y-[-8px]">1 NM</div>}
|
||||
{referenceDistance === 5280 * 2 && (
|
||||
<div
|
||||
className={`translate-y-[-8px]`}
|
||||
>
|
||||
2 NM
|
||||
</div>
|
||||
)}
|
||||
{referenceDistance < 5280 && <div className="translate-y-[-8px]">{referenceDistance} ft</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -2,13 +2,12 @@ import React, { useEffect, useState } from "react";
|
||||
import { OlLocation } from "../components/ollocation";
|
||||
import { LatLng } from "leaflet";
|
||||
import { FaBullseye, FaChevronDown, FaChevronUp, FaJetFighter, FaMountain } from "react-icons/fa6";
|
||||
import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent } from "../../events";
|
||||
import { BullseyesDataChangedEvent, MouseMovedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
|
||||
import { computeBearingRangeString, mToFt } from "../../other/utils";
|
||||
import { Bullseye } from "../../mission/bullseye";
|
||||
import { Unit } from "../../unit/unit";
|
||||
|
||||
export function CoordinatesPanel(props: {}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [latlng, setLatlng] = useState(new LatLng(0, 0));
|
||||
const [elevation, setElevation] = useState(0);
|
||||
const [bullseyes, setBullseyes] = useState(null as null | { [name: string]: Bullseye });
|
||||
@ -22,6 +21,7 @@ export function CoordinatesPanel(props: {}) {
|
||||
|
||||
BullseyesDataChangedEvent.on((bullseyes) => setBullseyes(bullseyes));
|
||||
SelectedUnitsChangedEvent.on((selectedUnits) => setSelectedUnits(selectedUnits));
|
||||
SelectionClearedEvent.on(() => setSelectedUnits([]))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -33,59 +33,53 @@ export function CoordinatesPanel(props: {}) {
|
||||
dark:bg-olympus-800/90 dark:text-gray-200
|
||||
`}
|
||||
>
|
||||
{" "}
|
||||
{open && (
|
||||
<>
|
||||
{bullseyes && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<div
|
||||
{bullseyes && (
|
||||
<div className="flex w-full items-center justify-start">
|
||||
<div
|
||||
className={`
|
||||
mr-[11px] flex min-w-64 max-w-64 items-center justify-between
|
||||
gap-2
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
flex min-w-64 max-w-64 items-center justify-between gap-2
|
||||
rounded-sm bg-blue-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-blue-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>{" "}
|
||||
{computeBearingRangeString(bullseyes[2].getLatLng(), latlng)}
|
||||
</div>
|
||||
<div className="flex w-[50%] justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-red-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>
|
||||
{computeBearingRangeString(bullseyes[1].getLatLng(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedUnits.length == 1 && (
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaJetFighter />
|
||||
</span>
|
||||
<div>
|
||||
{" "}
|
||||
{computeBearingRangeString(selectedUnits[0].getPosition(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FaBullseye />
|
||||
</span>{" "}
|
||||
{computeBearingRangeString(bullseyes[2].getLatLng(), latlng)}
|
||||
</div>
|
||||
<div className="flex w-[50%] justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-red-500 px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaBullseye />
|
||||
</span>
|
||||
{computeBearingRangeString(bullseyes[1].getLatLng(), latlng)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedUnits.length == 1 && (
|
||||
<div className="flex justify-start gap-2">
|
||||
<span
|
||||
className={`
|
||||
rounded-sm bg-white px-1 py-1 text-center font-bold
|
||||
text-olympus-700
|
||||
`}
|
||||
>
|
||||
<FaJetFighter />
|
||||
</span>
|
||||
<div> {computeBearingRangeString(selectedUnits[0].getPosition(), latlng)}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<OlLocation className="!min-w-64 !max-w-64 bg-transparent !p-0" location={latlng} />
|
||||
<span
|
||||
@ -97,11 +91,6 @@ export function CoordinatesPanel(props: {}) {
|
||||
<FaMountain />
|
||||
</span>
|
||||
<div className="min-w-12">{mToFt(elevation).toFixed()}ft</div>
|
||||
{open ? (
|
||||
<FaChevronDown className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
|
||||
) : (
|
||||
<FaChevronUp className="w-10 cursor-pointer" onClick={() => setOpen(!open)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,106 +1,55 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { useDrag } from "../libs/useDrag";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { FormationCreationRequestEvent } from "../../events";
|
||||
import { computeStandardFormationOffset } from "../../other/utils";
|
||||
import { formationTypes } from "../../constants/constants";
|
||||
import { FormationCanvas } from "./components/formationcanvas";
|
||||
|
||||
export function FormationMenu(props: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
}) {
|
||||
const [leader, setLeader] = useState(null as Unit | null)
|
||||
const [wingmen, setWingmen] = useState(null as Unit[] | null)
|
||||
|
||||
/* The useDrag custom hook used to handle the dragging of the units requires that the number of hooks remains unchanged.
|
||||
The units array is therefore initialized to 128 units maximum. */
|
||||
let units = Array(128).fill(null) as (Unit | null)[];
|
||||
units[0] = leader;
|
||||
wingmen?.forEach((unit, idx) => {
|
||||
if (idx < units.length) units[idx + 1] = unit;
|
||||
});
|
||||
export function FormationMenu(props: { open: boolean; onClose: () => void; children?: JSX.Element | JSX.Element[] }) {
|
||||
const [leader, setLeader] = useState(null as Unit | null);
|
||||
const [wingmen, setWingmen] = useState([] as Unit[]);
|
||||
|
||||
/* Init state variables */
|
||||
const [formationType, setFormationType] = useState("echelon-lh");
|
||||
const [horizontalScale, setHorizontalScale] = useState(0);
|
||||
const [verticalScale, setVerticalScale] = useState(30);
|
||||
const [offsets, setOffsets] = useState(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
const [unitPositions, setUnitPositions] = useState([] as { x: number; y: number }[]);
|
||||
|
||||
/* The count state is used to force the reset of the initial position of the silhouettes */
|
||||
// TODO it works but I don't like it, it feels like a hack
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
/* Init references and hooks */
|
||||
const containerRef = useRef(null);
|
||||
const scrollRef = useRef(null);
|
||||
const silhouetteReferences = units.map((_) => useRef(null));
|
||||
const silhouetteHandles = units.map((_, idx) => {
|
||||
/* Set the initial position of the unit to be centered in the drawing canvas, depending on the currently loaded formation */
|
||||
let offset = offsets[idx] ?? { x: 0, y: 0, z: 0 };
|
||||
let center = { x: 0, y: 0 };
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
}
|
||||
return useDrag({
|
||||
ref: silhouetteReferences[idx],
|
||||
initialPosition: { x: offset.z + center.x, y: -offset.x + center.y },
|
||||
count: count,
|
||||
});
|
||||
});
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
/* Listen for the setting of a new leader and wingmen and check if the formation is too big */
|
||||
useEffect(() => {
|
||||
FormationCreationRequestEvent.on((leader, wingmen) => {
|
||||
setLeader(leader);
|
||||
setWingmen(wingmen);
|
||||
})
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* When the formation type is changed, reset the position to the center and the position of the silhouettes depending on the aircraft */
|
||||
useEffect(() => {
|
||||
if (scrollRef.current && containerRef.current) {
|
||||
const containerDiv = containerRef.current as HTMLDivElement;
|
||||
const scrollDiv = scrollRef.current as HTMLDivElement;
|
||||
scrollDiv.scrollTop = (containerDiv.clientHeight - scrollDiv.clientHeight) / 2 + 150;
|
||||
scrollDiv.scrollLeft = (containerDiv.clientWidth - scrollDiv.clientWidth) / 2;
|
||||
}
|
||||
|
||||
const setStandardFormation = useCallback(() => {
|
||||
/* If a standard formation is chosen, compute the positions */
|
||||
if (formationType !== "custom") {
|
||||
setOffsets(
|
||||
units.map((unit, idx) => {
|
||||
return computeFormationOffset(formationType, idx);
|
||||
setUnitPositions(
|
||||
[leader, ...wingmen].map((unit, idx) => {
|
||||
return computeStandardFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
setCount(count + 1);
|
||||
}
|
||||
}, [formationType]);
|
||||
useEffect(setStandardFormation, [formationType]);
|
||||
|
||||
const horizontalRatio = 1 + (horizontalScale / 100) ** 2 * 100;
|
||||
const verticalRatio = (verticalScale - 50) / 50;
|
||||
|
||||
let referenceDistance = 200 * horizontalRatio;
|
||||
if (referenceDistance < 250) {
|
||||
referenceDistance = 100;
|
||||
} else if (referenceDistance < 500) {
|
||||
referenceDistance = 250;
|
||||
} else if (referenceDistance < 1000) {
|
||||
referenceDistance = 500;
|
||||
} else if (referenceDistance < 3000) {
|
||||
referenceDistance = 1000;
|
||||
} else if (referenceDistance < 10000) {
|
||||
referenceDistance = 5000;
|
||||
} else {
|
||||
referenceDistance = 10000;
|
||||
if (leader && unitPositions.length < [leader, ...wingmen].length) {
|
||||
/* If more units are added to the group keep the existing positions */
|
||||
setUnitPositions(
|
||||
[leader, ...wingmen].map((unit, idx) => {
|
||||
if (idx < unitPositions.length) return unitPositions[idx];
|
||||
else return computeStandardFormationOffset(formationType, idx);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const referenceWidth = referenceDistance / horizontalRatio;
|
||||
|
||||
return (
|
||||
<Menu title="Formation menu" open={props.open} showBackButton={false} onClose={props.onClose}>
|
||||
<div className="flex h-full flex-col gap-4 p-4">
|
||||
@ -111,13 +60,7 @@ export function FormationMenu(props: {
|
||||
.filter((type) => type !== "custom")
|
||||
.map((optionFormationType) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={optionFormationType}
|
||||
onClick={() => {
|
||||
setCount(count + 1);
|
||||
setFormationType(optionFormationType);
|
||||
}}
|
||||
>
|
||||
<OlDropdownItem key={optionFormationType} onClick={() => setFormationType(optionFormationType)}>
|
||||
{formationTypes[optionFormationType]}
|
||||
</OlDropdownItem>
|
||||
);
|
||||
@ -126,25 +69,7 @@ export function FormationMenu(props: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let content = JSON.stringify(
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.map((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null) {
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx * horizontalRatio,
|
||||
y: distance * verticalRatio,
|
||||
z: dz * horizontalRatio,
|
||||
};
|
||||
return offset;
|
||||
}
|
||||
})
|
||||
);
|
||||
let content = JSON.stringify(unitPositions);
|
||||
var a = document.createElement("a");
|
||||
var file = new Blob([content], { type: "text/plain" });
|
||||
a.href = URL.createObjectURL(file);
|
||||
@ -176,8 +101,7 @@ export function FormationMenu(props: {
|
||||
// @ts-ignore TODO
|
||||
var content = readerEvent.target.result;
|
||||
if (content) {
|
||||
setOffsets(JSON.parse(content.toString()));
|
||||
setCount(count + 1);
|
||||
setUnitPositions(JSON.parse(content.toString()));
|
||||
setFormationType("custom");
|
||||
}
|
||||
};
|
||||
@ -196,24 +120,6 @@ export function FormationMenu(props: {
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-white">Formation distance</span>
|
||||
<div className="flex h-fit content-center gap-4">
|
||||
<span
|
||||
className={`
|
||||
my-auto min-w-16 text-center align-middle text-sm text-white
|
||||
`}
|
||||
>
|
||||
Parade
|
||||
</span>
|
||||
<OlRangeSlider
|
||||
className="my-auto"
|
||||
value={horizontalScale}
|
||||
onChange={(ev) => {
|
||||
setHorizontalScale(Number(ev.target.value));
|
||||
}}
|
||||
/>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Tactical</span>
|
||||
</div>
|
||||
<span className="text-white">Vertical separation</span>
|
||||
<div className="flex h-fit content-center gap-4">
|
||||
<span className="ml-auto min-w-16 text-center text-sm text-white">Down</span>
|
||||
@ -226,39 +132,36 @@ export function FormationMenu(props: {
|
||||
/>
|
||||
<span className="my-auto min-w-16 text-center text-sm text-white">Up</span>
|
||||
</div>
|
||||
<FormationCanvas
|
||||
units={leader ? [leader, ...wingmen] : []}
|
||||
unitPositions={unitPositions}
|
||||
setUnitPositions={(positions) => {
|
||||
setUnitPositions(positions);
|
||||
setFormationType("custom");
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
let center = { x: 0, y: 0 };
|
||||
|
||||
if (containerRef.current) {
|
||||
center.x = (containerRef.current as HTMLDivElement).getBoundingClientRect().width / 2;
|
||||
center.y = (containerRef.current as HTMLDivElement).getBoundingClientRect().height / 2;
|
||||
if (leader) {
|
||||
[leader, ...wingmen]
|
||||
.filter((unit) => unit !== null)
|
||||
.forEach((unit, idx) => {
|
||||
if (idx != 0) {
|
||||
const [dx, dz] = [-(unitPositions[idx].y - unitPositions[0].y), unitPositions[idx].x - unitPositions[0].x];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx,
|
||||
y: distance * verticalRatio,
|
||||
z: dz,
|
||||
};
|
||||
unit.followUnit(leader.ID, offset);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
units
|
||||
.filter((unit) => unit !== null)
|
||||
.forEach((unit, idx) => {
|
||||
if (units.length > 0 && units[0] !== null && idx != 0) {
|
||||
const ID = units[0].ID;
|
||||
|
||||
const [dx, dz] = [
|
||||
-(silhouetteHandles[idx].position.y - silhouetteHandles[0].position.y),
|
||||
silhouetteHandles[idx].position.x - silhouetteHandles[0].position.x,
|
||||
];
|
||||
const distance = Math.sqrt(dx ** 2 + dz ** 2);
|
||||
const offset = {
|
||||
x: dx * horizontalRatio,
|
||||
y: distance * verticalRatio,
|
||||
z: dz * horizontalRatio,
|
||||
};
|
||||
unit.followUnit(ID, offset);
|
||||
}
|
||||
});
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 rounded-lg bg-blue-700 px-5 py-2.5 text-md font-medium
|
||||
text-white
|
||||
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
|
||||
@ -266,158 +169,7 @@ export function FormationMenu(props: {
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<div className="relative m-[-0.75rem] h-0">
|
||||
<div
|
||||
className={`
|
||||
relative left-6 top-4 h-4 border-2 border-white
|
||||
border-t-transparent text-center text-white
|
||||
`}
|
||||
style={{
|
||||
width: `${referenceWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className="translate-y-[-8px]">{referenceDistance}ft</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
relative h-full w-full overflow-scroll rounded-md border-[1px]
|
||||
border-white/20 bg-white/10
|
||||
`}
|
||||
ref={scrollRef}
|
||||
>
|
||||
<div className={`h-[1000px] w-[1000px] h-max-[1000px] w-max-[1000px]`} ref={containerRef}>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute top-0 h-[1000px] w-[1px] border-[1px]
|
||||
border-white/10
|
||||
`}
|
||||
style={{
|
||||
left: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{Array(100)
|
||||
.fill(0)
|
||||
.map((_, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`
|
||||
absolute left-0 h-[1px] w-[1000px] border-[1px]
|
||||
border-white/5
|
||||
`}
|
||||
style={{
|
||||
top: `${idx * 10}px`,
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
<>
|
||||
{units.map((unit, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={`${count}-${idx}`}
|
||||
className={`
|
||||
absolute
|
||||
${unit ? "" : "hidden"}
|
||||
`}
|
||||
ref={silhouetteReferences[idx]}
|
||||
style={{
|
||||
top: silhouetteHandles[idx].position.y,
|
||||
left: silhouetteHandles[idx].position.x,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
silhouetteHandles[idx].handleMouseDown(e);
|
||||
setFormationType("custom");
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className={`
|
||||
h-10 min-h-10 w-10 min-w-10 translate-x-[-50%]
|
||||
translate-y-[-50%] rotate-90 cursor-move opacity-80
|
||||
invert
|
||||
`}
|
||||
src={`./images/units/${unit?.getBlueprint()?.filename}`}
|
||||
></img>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
function computeFormationOffset(formation, idx) {
|
||||
let offset = { x: 0, z: 0 };
|
||||
if (formation === "trail") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "echelon-lh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "echelon-rh") {
|
||||
offset.x = -50 * idx;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "line-abreast-lh") {
|
||||
offset.x = 0;
|
||||
offset.z = -50 * idx;
|
||||
} else if (formation === "line-abreast-rh") {
|
||||
offset.x = 0;
|
||||
offset.z = 50 * idx;
|
||||
} else if (formation === "front") {
|
||||
offset.x = 100 * idx;
|
||||
offset.z = 0;
|
||||
} else if (formation === "diamond") {
|
||||
var xr = 0;
|
||||
var yr = 1;
|
||||
var zr = -1;
|
||||
var layer = 1;
|
||||
|
||||
for (let i = 0; i < idx; i++) {
|
||||
var xl = xr * Math.cos(Math.PI / 4) - yr * Math.sin(Math.PI / 4);
|
||||
var yl = xr * Math.sin(Math.PI / 4) + yr * Math.cos(Math.PI / 4);
|
||||
offset = { x: -yl * 50, z: xl * 50 };
|
||||
if (yr == 0) {
|
||||
layer++;
|
||||
xr = 0;
|
||||
yr = layer;
|
||||
zr = -layer;
|
||||
} else {
|
||||
if (xr < layer) {
|
||||
xr++;
|
||||
zr--;
|
||||
} else {
|
||||
yr--;
|
||||
zr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
let formationTypes = {
|
||||
"echelon-lh": "Echelon left",
|
||||
"echelon-rh": "Echelon right",
|
||||
"line-abreast-rh": "Line abreast right",
|
||||
"line-abreast-lh": "Line abreast left",
|
||||
trail: "Trail",
|
||||
front: "Front",
|
||||
diamond: "Diamond",
|
||||
custom: "Custom",
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Menu } from "./components/menu";
|
||||
import { Unit } from "../../unit/unit";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
@ -51,37 +51,41 @@ import { FaCog, FaGasPump, FaSignal, FaTag } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { OlSearchBar } from "../components/olsearchbar";
|
||||
import { OlDropdown, OlDropdownItem } from "../components/oldropdown";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { FaRadio, FaVolumeHigh } from "react-icons/fa6";
|
||||
import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { Radio, TACAN } from "../../interfaces";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
import { OlFrequencyInput } from "../components/olfrequencyinput";
|
||||
import { UnitSink } from "../../audio/unitsink";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent } from "../../events";
|
||||
import { AudioManagerStateChangedEvent, SelectedUnitsChangedEvent, SelectionClearedEvent, UnitsUpdatedEvent } from "../../events";
|
||||
|
||||
export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
function initializeUnitsData() {
|
||||
return {
|
||||
desiredAltitude: undefined as undefined | number,
|
||||
desiredAltitudeType: undefined as undefined | string,
|
||||
desiredSpeed: undefined as undefined | number,
|
||||
desiredSpeedType: undefined as undefined | string,
|
||||
ROE: undefined as undefined | string,
|
||||
reactionToThreat: undefined as undefined | string,
|
||||
emissionsCountermeasures: undefined as undefined | string,
|
||||
scenicAAA: undefined as undefined | boolean,
|
||||
missOnPurpose: undefined as undefined | boolean,
|
||||
shotsScatter: undefined as undefined | number,
|
||||
shotsIntensity: undefined as undefined | number,
|
||||
operateAs: undefined as undefined | Coalition,
|
||||
followRoads: undefined as undefined | boolean,
|
||||
isActiveAWACS: undefined as undefined | boolean,
|
||||
isActiveTanker: undefined as undefined | boolean,
|
||||
onOff: undefined as undefined | boolean,
|
||||
isAudioSink: undefined as undefined | boolean,
|
||||
};
|
||||
}
|
||||
|
||||
const [selectedUnits, setSelectedUnits] = useState([] as Unit[]);
|
||||
const [audioManagerState, setAudioManagerState] = useState(false);
|
||||
const [selectedUnitsData, setSelectedUnitsData] = useState({
|
||||
desiredAltitude: undefined as undefined | number,
|
||||
desiredAltitudeType: undefined as undefined | string,
|
||||
desiredSpeed: undefined as undefined | number,
|
||||
desiredSpeedType: undefined as undefined | string,
|
||||
ROE: undefined as undefined | string,
|
||||
reactionToThreat: undefined as undefined | string,
|
||||
emissionsCountermeasures: undefined as undefined | string,
|
||||
scenicAAA: undefined as undefined | boolean,
|
||||
missOnPurpose: undefined as undefined | boolean,
|
||||
shotsScatter: undefined as undefined | number,
|
||||
shotsIntensity: undefined as undefined | number,
|
||||
operateAs: undefined as undefined | Coalition,
|
||||
followRoads: undefined as undefined | boolean,
|
||||
isActiveAWACS: undefined as undefined | boolean,
|
||||
isActiveTanker: undefined as undefined | boolean,
|
||||
onOff: undefined as undefined | boolean,
|
||||
isAudioSink: undefined as undefined | boolean,
|
||||
});
|
||||
const [selectedUnitsData, setSelectedUnitsData] = useState(initializeUnitsData);
|
||||
const [forcedUnitsData, setForcedUnitsData] = useState(initializeUnitsData);
|
||||
const [selectionFilter, setSelectionFilter] = useState({
|
||||
control: {
|
||||
human: true,
|
||||
@ -115,6 +119,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
const [activeAdvancedSettings, setActiveAdvancedSettings] = useState(null as null | { radio: Radio; TACAN: TACAN });
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(0);
|
||||
|
||||
var searchBarRef = useRef(null);
|
||||
|
||||
@ -122,6 +127,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
SelectedUnitsChangedEvent.on((units) => setSelectedUnits(units));
|
||||
SelectionClearedEvent.on(() => setSelectedUnits([]));
|
||||
AudioManagerStateChangedEvent.on((state) => setAudioManagerState(state));
|
||||
UnitsUpdatedEvent.on((units) => units.find((unit) => unit.getSelected()) && setLastUpdateTime(Date.now()));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -130,7 +136,7 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
if (!props.open && filterString !== "") setFilterString("");
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const updateData = useCallback(() => {
|
||||
setShowAdvancedSettings(false);
|
||||
|
||||
const getters = {
|
||||
@ -169,10 +175,24 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
} as { [key in keyof typeof selectedUnitsData]: (unit: Unit) => void };
|
||||
|
||||
var updatedData = {};
|
||||
let anyForcedDataUpdated = false;
|
||||
Object.entries(getters).forEach(([key, getter]) => {
|
||||
updatedData[key] = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
|
||||
let newDatum = getApp()?.getUnitsManager()?.getSelectedUnitsVariable(getter);
|
||||
if (forcedUnitsData[key] !== undefined) {
|
||||
if (newDatum === updatedData[key]) {
|
||||
anyForcedDataUpdated = true;
|
||||
forcedUnitsData[key] === undefined;
|
||||
}
|
||||
updatedData[key] = forcedUnitsData[key];
|
||||
} else updatedData[key] = newDatum;
|
||||
});
|
||||
setSelectedUnitsData(updatedData as typeof selectedUnitsData);
|
||||
if (anyForcedDataUpdated) setForcedUnitsData({...forcedUnitsData})
|
||||
}, [forcedUnitsData])
|
||||
useEffect(updateData, [selectedUnits, lastUpdateTime, forcedUnitsData]);
|
||||
|
||||
useEffect(() => {
|
||||
setForcedUnitsData(initializeUnitsData);
|
||||
}, [selectedUnits]);
|
||||
|
||||
/* Count how many units are selected of each type, divided by coalition */
|
||||
@ -538,8 +558,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setAltitudeType(selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredAltitudeType: selectedUnitsData.desiredAltitudeType === "ASL" ? "AGL" : "ASL",
|
||||
});
|
||||
});
|
||||
@ -550,8 +570,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onChange={(ev) => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setAltitude(ftToM(Number(ev.target.value)));
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredAltitude: Number(ev.target.value),
|
||||
});
|
||||
});
|
||||
@ -600,8 +620,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeedType(selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredSpeedType: selectedUnitsData.desiredSpeedType === "CAS" ? "GS" : "CAS",
|
||||
});
|
||||
});
|
||||
@ -613,8 +633,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onChange={(ev) => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setSpeed(knotsToMs(Number(ev.target.value)));
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
desiredSpeed: Number(ev.target.value),
|
||||
});
|
||||
});
|
||||
@ -645,8 +665,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setROE(ROEs[convertROE(idx)]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
ROE: ROEs[convertROE(idx)],
|
||||
});
|
||||
});
|
||||
@ -682,8 +702,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setReactionToThreat(reactionsToThreat[idx]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
reactionToThreat: reactionsToThreat[idx],
|
||||
});
|
||||
});
|
||||
@ -714,8 +734,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setEmissionsCountermeasures(emissionsCountermeasures[idx]);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
emissionsCountermeasures: emissionsCountermeasures[idx],
|
||||
});
|
||||
});
|
||||
@ -756,8 +776,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
unit.getRadio(),
|
||||
unit.getGeneralSettings()
|
||||
);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isActiveTanker: !selectedUnitsData.isActiveTanker,
|
||||
});
|
||||
});
|
||||
@ -790,8 +810,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
unit.getRadio(),
|
||||
unit.getGeneralSettings()
|
||||
);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isActiveAWACS: !selectedUnitsData.isActiveAWACS,
|
||||
});
|
||||
});
|
||||
@ -848,8 +868,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.scenicAAA ? unit.changeSpeed("stop") : unit.scenicAAA();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: !selectedUnitsData.scenicAAA,
|
||||
missOnPurpose: false,
|
||||
});
|
||||
@ -873,8 +893,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
selectedUnitsData.missOnPurpose ? unit.changeSpeed("stop") : unit.missOnPurpose();
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
scenicAAA: false,
|
||||
missOnPurpose: !selectedUnitsData.missOnPurpose,
|
||||
});
|
||||
@ -902,8 +922,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsScatter(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsScatter: idx + 1,
|
||||
});
|
||||
});
|
||||
@ -934,8 +954,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setShotsIntensity(idx + 1);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
shotsIntensity: idx + 1,
|
||||
});
|
||||
});
|
||||
@ -964,8 +984,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOperateAs(selectedUnitsData.operateAs === "blue" ? "red" : "blue");
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
operateAs: selectedUnitsData.operateAs === "blue" ? "red" : "blue",
|
||||
});
|
||||
});
|
||||
@ -989,8 +1009,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setFollowRoads(!selectedUnitsData.followRoads);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
followRoads: !selectedUnitsData.followRoads,
|
||||
});
|
||||
});
|
||||
@ -1013,8 +1033,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
onClick={() => {
|
||||
selectedUnits.forEach((unit) => {
|
||||
unit.setOnOff(!selectedUnitsData.onOff);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
onOff: !selectedUnitsData.onOff,
|
||||
});
|
||||
});
|
||||
@ -1041,8 +1061,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
selectedUnits.forEach((unit) => {
|
||||
if (!selectedUnitsData.isAudioSink) {
|
||||
getApp()?.getAudioManager().addUnitSink(unit);
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: true,
|
||||
});
|
||||
} else {
|
||||
@ -1054,8 +1074,8 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
});
|
||||
if (sink !== undefined) getApp()?.getAudioManager().removeSink(sink);
|
||||
|
||||
setSelectedUnitsData({
|
||||
...selectedUnitsData,
|
||||
setForcedUnitsData({
|
||||
...forcedUnitsData,
|
||||
isAudioSink: false,
|
||||
});
|
||||
}
|
||||
@ -1178,10 +1198,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
value={activeAdvancedSettings ? activeAdvancedSettings.TACAN.channel : 1}
|
||||
></OlNumberInput>
|
||||
|
||||
<OlDropdown
|
||||
label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"}
|
||||
className={`my-auto w-20`}
|
||||
>
|
||||
<OlDropdown label={activeAdvancedSettings ? activeAdvancedSettings.TACAN.XY : "X"} className={`
|
||||
my-auto w-20
|
||||
`}>
|
||||
<OlDropdownItem
|
||||
key={"X"}
|
||||
onClick={() => {
|
||||
@ -1300,11 +1319,9 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
|
||||
className={`
|
||||
flex content-center gap-2 rounded-full
|
||||
${selectedUnits[0].getFuel() > 40 && `bg-green-700`}
|
||||
${
|
||||
selectedUnits[0].getFuel() > 10 &&
|
||||
selectedUnits[0].getFuel() <= 40 &&
|
||||
`bg-yellow-700`
|
||||
}
|
||||
${selectedUnits[0].getFuel() > 10 && selectedUnits[0].getFuel() <= 40 && `
|
||||
bg-yellow-700
|
||||
`}
|
||||
${selectedUnits[0].getFuel() <= 10 && `bg-red-700`}
|
||||
px-2 py-1 text-sm font-bold text-white
|
||||
`}
|
||||
|
||||
@ -420,7 +420,7 @@ export abstract class Unit extends CustomMarker {
|
||||
*/
|
||||
setData(dataExtractor: DataExtractor) {
|
||||
/* This variable controls if the marker must be updated. This is not always true since not all variables have an effect on the marker */
|
||||
var updateMarker = !getApp().getMap().hasLayer(this);
|
||||
var updateMarker = !getApp().getMap().hasLayer(this) && this.getAlive();
|
||||
|
||||
var oldIsLeader = this.#isLeader;
|
||||
var datumIndex = 0;
|
||||
|
||||
@ -36,6 +36,7 @@ import {
|
||||
UnitDeselectedEvent,
|
||||
UnitSelectedEvent,
|
||||
UnitsRefreshedEvent,
|
||||
UnitsUpdatedEvent,
|
||||
} from "../events";
|
||||
import { UnitDatabase } from "./databases/unitdatabase";
|
||||
import * as turf from "@turf/turf";
|
||||
@ -49,7 +50,6 @@ export class UnitsManager {
|
||||
#deselectionEventDisabled: boolean = false;
|
||||
#requestDetectionUpdate: boolean = false;
|
||||
#selectionEventDisabled: boolean = false;
|
||||
//#slowDeleteDialog!: Dialog;
|
||||
#units: { [ID: number]: Unit } = {};
|
||||
#groups: { [groupName: string]: Group } = {};
|
||||
#unitDataExport!: UnitDataFileExport;
|
||||
@ -171,8 +171,6 @@ export class UnitsManager {
|
||||
altKey: false,
|
||||
});
|
||||
});
|
||||
|
||||
//this.#slowDeleteDialog = new Dialog("slow-delete-dialog");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -231,6 +229,7 @@ export class UnitsManager {
|
||||
var dataExtractor = new DataExtractor(buffer);
|
||||
|
||||
var updateTime = Number(dataExtractor.extractUInt64());
|
||||
let updatedUnits: Unit[] = [];
|
||||
|
||||
/* Run until all data is extracted or an error occurs */
|
||||
while (dataExtractor.getSeekPosition() < buffer.byteLength) {
|
||||
@ -249,7 +248,10 @@ export class UnitsManager {
|
||||
}
|
||||
}
|
||||
/* Update the data of the unit */
|
||||
this.#units[ID]?.setData(dataExtractor);
|
||||
if (ID in this.#units) {
|
||||
this.#units[ID].setData(dataExtractor);
|
||||
this.#units[ID].getAlive() && updatedUnits.push(this.#units[ID]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Update the unit groups */
|
||||
@ -311,7 +313,8 @@ export class UnitsManager {
|
||||
/* Compute the base clusters */
|
||||
this.#clusters = this.computeClusters();
|
||||
|
||||
if (fullUpdate) UnitsRefreshedEvent.dispatch(this.#units);
|
||||
if (fullUpdate) UnitsRefreshedEvent.dispatch(Object.values(this.#units));
|
||||
else UnitsUpdatedEvent.dispatch(updatedUnits);
|
||||
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export class WeaponsManager {
|
||||
this.#weapons[ID]?.setData(dataExtractor);
|
||||
}
|
||||
|
||||
if (fullUpdate) WeaponsRefreshedEvent.dispatch(this.#weapons);
|
||||
if (fullUpdate) WeaponsRefreshedEvent.dispatch(Object.values(this.#weapons));
|
||||
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user