feat: Improved formation menu

This commit is contained in:
Davide Passoni 2025-01-13 09:36:43 +01:00
parent 0376d020e7
commit 711f6094f0
22 changed files with 48261 additions and 42134 deletions

File diff suppressed because it is too large Load Diff

172
databases/units/asd.py Normal file
View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

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

View File

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

View File

@ -298,6 +298,7 @@ export interface UnitBlueprint {
markerFile?: string;
unitWhenGrouped?: string;
mainRole?: string;
length?: number;
}
export interface AirbaseOptions {

View File

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

View File

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

View File

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

View File

@ -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() &&

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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