mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Completed custom weapon wizard modal
This commit is contained in:
parent
94d0b4d10e
commit
b42280133c
@ -185,6 +185,7 @@ struct SpawnOptions {
|
||||
string skill;
|
||||
string liveryID;
|
||||
double heading;
|
||||
string payload;
|
||||
};
|
||||
|
||||
struct CloneOptions {
|
||||
|
||||
@ -102,6 +102,7 @@ string SpawnAircrafts::getString()
|
||||
<< "alt = " << spawnOptions[i].location.alt << ", "
|
||||
<< "heading = " << spawnOptions[i].heading << ", "
|
||||
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
|
||||
<< "payload = " << spawnOptions[i].payload << ", "
|
||||
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
|
||||
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
|
||||
}
|
||||
@ -132,6 +133,7 @@ string SpawnHelicopters::getString()
|
||||
<< "alt = " << spawnOptions[i].location.alt << ", "
|
||||
<< "heading = " << spawnOptions[i].heading << ", "
|
||||
<< "loadout = \"" << spawnOptions[i].loadout << "\"" << ", "
|
||||
<< "payload = " << spawnOptions[i].payload << ", "
|
||||
<< "liveryID = " << "\"" << spawnOptions[i].liveryID << "\"" << ", "
|
||||
<< "skill = \"" << spawnOptions[i].skill << "\"" << "}, ";
|
||||
}
|
||||
|
||||
@ -223,7 +223,11 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
|
||||
string liveryID = to_string(unit[L"liveryID"]);
|
||||
string skill = to_string(unit[L"skill"]);
|
||||
|
||||
spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading });
|
||||
string payload = "nil";
|
||||
if (unit.has_string_field(L"payload"))
|
||||
payload = to_string(unit[L"payload"]);
|
||||
|
||||
spawnOptions.push_back({ unitType, location, loadout, skill, liveryID, heading, payload });
|
||||
log(username + " spawned a " + coalition + " " + unitType, true);
|
||||
}
|
||||
|
||||
|
||||
@ -399,6 +399,7 @@ export enum SpawnSubState {
|
||||
NO_SUBSTATE = "No substate",
|
||||
SPAWN_UNIT = "Unit",
|
||||
SPAWN_EFFECT = "Effect",
|
||||
LOADOUT_WIZARD = "Loadout wizard"
|
||||
}
|
||||
|
||||
export enum OptionsSubstate {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,403 +2,405 @@ import { LatLng } from "leaflet";
|
||||
import { AudioOptions, Coalition, MapOptions } from "./types/types";
|
||||
|
||||
export interface OlympusConfig {
|
||||
/* Set by user */
|
||||
frontend: {
|
||||
port: number;
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
mapLayers: {
|
||||
[key: string]: {
|
||||
urlTemplate: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
attribution?: string;
|
||||
};
|
||||
};
|
||||
mapMirrors: {
|
||||
[key: string]: string;
|
||||
/* Set by user */
|
||||
frontend: {
|
||||
port: number;
|
||||
elevationProvider: {
|
||||
provider: string;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
mapLayers: {
|
||||
[key: string]: {
|
||||
urlTemplate: string;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
attribution?: string;
|
||||
};
|
||||
};
|
||||
mapMirrors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
customAuthHeaders?: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
};
|
||||
autoconnectWhenLocal?: boolean;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
customAuthHeaders?: {
|
||||
enabled: boolean;
|
||||
username: string;
|
||||
group: string;
|
||||
audio?: {
|
||||
SRSPort: number;
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
autoconnectWhenLocal?: boolean;
|
||||
};
|
||||
/* New with v2.0.0 */
|
||||
audio?: {
|
||||
SRSPort: number;
|
||||
WSPort?: number;
|
||||
WSEndpoint?: string;
|
||||
};
|
||||
controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
profiles?: { [key: string]: ProfileOptions };
|
||||
controllers?: [{ type: string; coalition: Coalition; frequency: number; modulation: number; callsign: string }];
|
||||
profiles?: { [key: string]: ProfileOptions };
|
||||
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPassword: string;
|
||||
redCommanderPassword: string;
|
||||
};
|
||||
/* Set by server */
|
||||
local?: boolean;
|
||||
authentication?: {
|
||||
// Only sent when in localhost mode for autologin
|
||||
gameMasterPassword: string;
|
||||
blueCommanderPassword: string;
|
||||
redCommanderPassword: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
radios?: { frequency: number; modulation: number; pan: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: { ID: number }[];
|
||||
connections?: any[];
|
||||
coalitionAreas?: (
|
||||
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
)[];
|
||||
hotgroups?: { [key: string]: number[] };
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable };
|
||||
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
mapSource?: { id: string };
|
||||
radios?: { frequency: number; modulation: number; pan: number }[];
|
||||
fileSources?: { filename: string; volume: number }[];
|
||||
unitSinks?: { ID: number }[];
|
||||
connections?: any[];
|
||||
coalitionAreas?: (
|
||||
| { type: "circle"; label: string; latlng: { lat: number; lng: number }; radius: number; coalition: Coalition }
|
||||
| { type: "polygon"; label: string; latlngs: { lat: number; lng: number }[]; coalition: Coalition }
|
||||
)[];
|
||||
hotgroups?: { [key: string]: number[] };
|
||||
starredSpawns?: { [key: number]: SpawnRequestTable };
|
||||
drawings?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
navpoints?: { [key: string]: { visibility: boolean; opacity: number; name: string; guid: string; containers: any; drawings: any } };
|
||||
mapSource?: { id: string };
|
||||
customLoadouts?: { [key: string]: LoadoutBlueprint[] };
|
||||
}
|
||||
|
||||
export interface ProfileOptions {
|
||||
mapOptions?: MapOptions;
|
||||
shortcuts?: { [key: string]: ShortcutOptions };
|
||||
audioOptions?: AudioOptions;
|
||||
mapOptions?: MapOptions;
|
||||
shortcuts?: { [key: string]: ShortcutOptions };
|
||||
audioOptions?: AudioOptions;
|
||||
}
|
||||
|
||||
export interface ContextMenuOption {
|
||||
tooltip: string;
|
||||
src: string;
|
||||
callback: CallableFunction;
|
||||
tooltip: string;
|
||||
src: string;
|
||||
callback: CallableFunction;
|
||||
}
|
||||
|
||||
export interface AirbasesData {
|
||||
airbases: { [key: string]: any };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
airbases: { [key: string]: any };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface BullseyesData {
|
||||
bullseyes: {
|
||||
[key: string]: { latitude: number; longitude: number; coalition: string };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
bullseyes: {
|
||||
[key: string]: { latitude: number; longitude: number; coalition: string };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface SpotsData {
|
||||
spots: {
|
||||
[key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
spots: {
|
||||
[key: string]: { active: boolean; type: string; targetPosition: { lat: number; lng: number }; sourceUnitID: number; code?: number };
|
||||
};
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface MissionData {
|
||||
mission: {
|
||||
theatre: string;
|
||||
dateAndTime: DateAndTime;
|
||||
commandModeOptions: CommandModeOptions;
|
||||
coalitions: { red: string[]; blue: string[] };
|
||||
};
|
||||
time: number;
|
||||
sessionHash: string;
|
||||
mission: {
|
||||
theatre: string;
|
||||
dateAndTime: DateAndTime;
|
||||
commandModeOptions: CommandModeOptions;
|
||||
coalitions: { red: string[]; blue: string[] };
|
||||
};
|
||||
time: number;
|
||||
sessionHash: string;
|
||||
}
|
||||
|
||||
export interface CommandModeOptions {
|
||||
commandMode: string;
|
||||
restrictSpawns: boolean;
|
||||
restrictToCoalition: boolean;
|
||||
setupTime: number;
|
||||
spawnPoints: {
|
||||
red: number;
|
||||
blue: number;
|
||||
};
|
||||
eras: string[];
|
||||
commandMode: string;
|
||||
restrictSpawns: boolean;
|
||||
restrictToCoalition: boolean;
|
||||
setupTime: number;
|
||||
spawnPoints: {
|
||||
red: number;
|
||||
blue: number;
|
||||
};
|
||||
eras: string[];
|
||||
}
|
||||
|
||||
export interface DateAndTime {
|
||||
date: { Year: number; Month: number; Day: number };
|
||||
time: { h: number; m: number; s: number };
|
||||
elapsedTime: number;
|
||||
startTime: number;
|
||||
date: { Year: number; Month: number; Day: number };
|
||||
time: { h: number; m: number; s: number };
|
||||
elapsedTime: number;
|
||||
startTime: number;
|
||||
}
|
||||
|
||||
export interface LogData {
|
||||
logs: { [key: string]: string };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
logs: { [key: string]: string };
|
||||
sessionHash: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface ServerRequestOptions {
|
||||
time?: number;
|
||||
commandHash?: string;
|
||||
time?: number;
|
||||
commandHash?: string;
|
||||
}
|
||||
|
||||
export interface SpawnRequestTable {
|
||||
category: string;
|
||||
coalition: string;
|
||||
unit: UnitSpawnTable;
|
||||
amount: number;
|
||||
quickAccessName?: string;
|
||||
category: string;
|
||||
coalition: string;
|
||||
unit: UnitSpawnTable;
|
||||
amount: number;
|
||||
quickAccessName?: string;
|
||||
}
|
||||
|
||||
export interface EffectRequestTable {
|
||||
type: string;
|
||||
explosionType?: string;
|
||||
smokeColor?: string;
|
||||
type: string;
|
||||
explosionType?: string;
|
||||
smokeColor?: string;
|
||||
}
|
||||
|
||||
export interface UnitSpawnTable {
|
||||
unitType: string;
|
||||
location: LatLng;
|
||||
skill: string;
|
||||
liveryID: string;
|
||||
altitude?: number;
|
||||
loadout?: string;
|
||||
heading?: number;
|
||||
unitType: string;
|
||||
location: LatLng;
|
||||
skill: string;
|
||||
liveryID: string;
|
||||
altitude?: number;
|
||||
loadout?: string;
|
||||
heading?: number;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface ObjectIconOptions {
|
||||
showState: boolean;
|
||||
showVvi: boolean;
|
||||
showHealth: boolean;
|
||||
showHotgroup: boolean;
|
||||
showUnitIcon: boolean;
|
||||
showShortLabel: boolean;
|
||||
showFuel: boolean;
|
||||
showAmmo: boolean;
|
||||
showSummary: boolean;
|
||||
showCallsign: boolean;
|
||||
rotateToHeading: boolean;
|
||||
showCluster: boolean;
|
||||
showAlarmState: boolean;
|
||||
showState: boolean;
|
||||
showVvi: boolean;
|
||||
showHealth: boolean;
|
||||
showHotgroup: boolean;
|
||||
showUnitIcon: boolean;
|
||||
showShortLabel: boolean;
|
||||
showFuel: boolean;
|
||||
showAmmo: boolean;
|
||||
showSummary: boolean;
|
||||
showCallsign: boolean;
|
||||
rotateToHeading: boolean;
|
||||
showCluster: boolean;
|
||||
showAlarmState: boolean;
|
||||
}
|
||||
|
||||
export interface GeneralSettings {
|
||||
prohibitJettison: boolean;
|
||||
prohibitAA: boolean;
|
||||
prohibitAG: boolean;
|
||||
prohibitAfterburner: boolean;
|
||||
prohibitAirWpn: boolean;
|
||||
prohibitJettison: boolean;
|
||||
prohibitAA: boolean;
|
||||
prohibitAG: boolean;
|
||||
prohibitAfterburner: boolean;
|
||||
prohibitAirWpn: boolean;
|
||||
}
|
||||
|
||||
export interface TACAN {
|
||||
isOn: boolean;
|
||||
channel: number;
|
||||
XY: string;
|
||||
callsign: string;
|
||||
isOn: boolean;
|
||||
channel: number;
|
||||
XY: string;
|
||||
callsign: string;
|
||||
}
|
||||
|
||||
export interface Radio {
|
||||
frequency: number;
|
||||
callsign: number;
|
||||
callsignNumber: number;
|
||||
frequency: number;
|
||||
callsign: number;
|
||||
callsignNumber: number;
|
||||
}
|
||||
|
||||
export interface Ammo {
|
||||
quantity: number;
|
||||
name: string;
|
||||
guidance: number;
|
||||
category: number;
|
||||
missileCategory: number;
|
||||
quantity: number;
|
||||
name: string;
|
||||
guidance: number;
|
||||
category: number;
|
||||
missileCategory: number;
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
ID: number;
|
||||
detectionMethod: number;
|
||||
ID: number;
|
||||
detectionMethod: number;
|
||||
}
|
||||
|
||||
export interface Offset {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
export interface DrawingArgument {
|
||||
argument: number;
|
||||
value: number;
|
||||
argument: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface UnitData {
|
||||
category: string;
|
||||
markerCategory: string;
|
||||
ID: number;
|
||||
alive: boolean;
|
||||
alarmState: AlarmState;
|
||||
human: boolean;
|
||||
controlled: boolean;
|
||||
coalition: string;
|
||||
country: number;
|
||||
name: string;
|
||||
unitName: string;
|
||||
callsign: string;
|
||||
unitID: number;
|
||||
groupID: number;
|
||||
groupName: string;
|
||||
state: string;
|
||||
task: string;
|
||||
hasTask: boolean;
|
||||
position: LatLng;
|
||||
speed: number;
|
||||
horizontalVelocity: number;
|
||||
verticalVelocity: number;
|
||||
heading: number;
|
||||
track: number;
|
||||
isActiveTanker: boolean;
|
||||
isActiveAWACS: boolean;
|
||||
onOff: boolean;
|
||||
followRoads: boolean;
|
||||
fuel: number;
|
||||
desiredSpeed: number;
|
||||
desiredSpeedType: string;
|
||||
desiredAltitude: number;
|
||||
desiredAltitudeType: string;
|
||||
leaderID: number;
|
||||
formationOffset: Offset;
|
||||
targetID: number;
|
||||
targetPosition: LatLng;
|
||||
ROE: string;
|
||||
reactionToThreat: string;
|
||||
emissionsCountermeasures: string;
|
||||
TACAN: TACAN;
|
||||
radio: Radio;
|
||||
generalSettings: GeneralSettings;
|
||||
ammo: Ammo[];
|
||||
contacts: Contact[];
|
||||
activePath: LatLng[];
|
||||
isLeader: boolean;
|
||||
operateAs: string;
|
||||
shotsScatter: number;
|
||||
shotsIntensity: number;
|
||||
health: number;
|
||||
racetrackLength: number;
|
||||
racetrackAnchor: LatLng;
|
||||
racetrackBearing: number;
|
||||
timeToNextTasking: number;
|
||||
barrelHeight: number;
|
||||
muzzleVelocity: number;
|
||||
aimTime: number;
|
||||
shotsToFire: number;
|
||||
shotsBaseInterval: number;
|
||||
shotsBaseScatter: number;
|
||||
engagementRange: number;
|
||||
targetingRange: number;
|
||||
aimMethodRange: number;
|
||||
acquisitionRange: number;
|
||||
airborne: boolean;
|
||||
cargoWeight: number;
|
||||
drawingArguments: DrawingArgument[];
|
||||
customString: string;
|
||||
customInteger: number;
|
||||
category: string;
|
||||
markerCategory: string;
|
||||
ID: number;
|
||||
alive: boolean;
|
||||
alarmState: AlarmState;
|
||||
human: boolean;
|
||||
controlled: boolean;
|
||||
coalition: string;
|
||||
country: number;
|
||||
name: string;
|
||||
unitName: string;
|
||||
callsign: string;
|
||||
unitID: number;
|
||||
groupID: number;
|
||||
groupName: string;
|
||||
state: string;
|
||||
task: string;
|
||||
hasTask: boolean;
|
||||
position: LatLng;
|
||||
speed: number;
|
||||
horizontalVelocity: number;
|
||||
verticalVelocity: number;
|
||||
heading: number;
|
||||
track: number;
|
||||
isActiveTanker: boolean;
|
||||
isActiveAWACS: boolean;
|
||||
onOff: boolean;
|
||||
followRoads: boolean;
|
||||
fuel: number;
|
||||
desiredSpeed: number;
|
||||
desiredSpeedType: string;
|
||||
desiredAltitude: number;
|
||||
desiredAltitudeType: string;
|
||||
leaderID: number;
|
||||
formationOffset: Offset;
|
||||
targetID: number;
|
||||
targetPosition: LatLng;
|
||||
ROE: string;
|
||||
reactionToThreat: string;
|
||||
emissionsCountermeasures: string;
|
||||
TACAN: TACAN;
|
||||
radio: Radio;
|
||||
generalSettings: GeneralSettings;
|
||||
ammo: Ammo[];
|
||||
contacts: Contact[];
|
||||
activePath: LatLng[];
|
||||
isLeader: boolean;
|
||||
operateAs: string;
|
||||
shotsScatter: number;
|
||||
shotsIntensity: number;
|
||||
health: number;
|
||||
racetrackLength: number;
|
||||
racetrackAnchor: LatLng;
|
||||
racetrackBearing: number;
|
||||
timeToNextTasking: number;
|
||||
barrelHeight: number;
|
||||
muzzleVelocity: number;
|
||||
aimTime: number;
|
||||
shotsToFire: number;
|
||||
shotsBaseInterval: number;
|
||||
shotsBaseScatter: number;
|
||||
engagementRange: number;
|
||||
targetingRange: number;
|
||||
aimMethodRange: number;
|
||||
acquisitionRange: number;
|
||||
airborne: boolean;
|
||||
cargoWeight: number;
|
||||
drawingArguments: DrawingArgument[];
|
||||
customString: string;
|
||||
customInteger: number;
|
||||
}
|
||||
|
||||
export interface LoadoutItemBlueprint {
|
||||
name: string;
|
||||
quantity: number;
|
||||
type: string;
|
||||
effectiveAgainst?: string;
|
||||
name: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface LoadoutBlueprint {
|
||||
fuel: number;
|
||||
items: LoadoutItemBlueprint[];
|
||||
roles: string[];
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
items: LoadoutItemBlueprint[];
|
||||
roles: string[];
|
||||
code: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
isCustom?: boolean;
|
||||
persistent?: boolean;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface UnitBlueprint {
|
||||
name: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
coalition: string;
|
||||
era: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
roles?: string[];
|
||||
type?: string;
|
||||
loadouts?: LoadoutBlueprint[];
|
||||
acceptedPayloads?: { [key: string]: { clsids: string; name: string, weight: number }[] };
|
||||
filename?: string;
|
||||
liveries?: { [key: string]: { name: string; countries: string[] } };
|
||||
cost?: number;
|
||||
barrelHeight?: number;
|
||||
muzzleVelocity?: number;
|
||||
aimTime?: number;
|
||||
shotsToFire?: number;
|
||||
shotsBaseInterval?: number;
|
||||
shotsBaseScatter?: number;
|
||||
description?: string;
|
||||
abilities?: string;
|
||||
tags?: string;
|
||||
acquisitionRange?: number;
|
||||
engagementRange?: number;
|
||||
targetingRange?: number;
|
||||
aimMethodRange?: number;
|
||||
alertnessTimeConstant?: number;
|
||||
canTargetPoint?: boolean;
|
||||
canRearm?: boolean;
|
||||
canAAA?: boolean;
|
||||
indirectFire?: boolean;
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
length?: number;
|
||||
carrierFilename?: string;
|
||||
name: string;
|
||||
category: string;
|
||||
enabled: boolean;
|
||||
coalition: string;
|
||||
era: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
roles?: string[];
|
||||
type?: string;
|
||||
loadouts?: LoadoutBlueprint[];
|
||||
acceptedPayloads?: { [key: string]: { clsid: string; name: string; weight: number }[] };
|
||||
filename?: string;
|
||||
liveries?: { [key: string]: { name: string; countries: string[] } };
|
||||
cost?: number;
|
||||
barrelHeight?: number;
|
||||
muzzleVelocity?: number;
|
||||
aimTime?: number;
|
||||
shotsToFire?: number;
|
||||
shotsBaseInterval?: number;
|
||||
shotsBaseScatter?: number;
|
||||
description?: string;
|
||||
abilities?: string;
|
||||
tags?: string;
|
||||
acquisitionRange?: number;
|
||||
engagementRange?: number;
|
||||
targetingRange?: number;
|
||||
aimMethodRange?: number;
|
||||
alertnessTimeConstant?: number;
|
||||
canTargetPoint?: boolean;
|
||||
canRearm?: boolean;
|
||||
canAAA?: boolean;
|
||||
indirectFire?: boolean;
|
||||
markerFile?: string;
|
||||
unitWhenGrouped?: string;
|
||||
mainRole?: string;
|
||||
length?: number;
|
||||
carrierFilename?: string;
|
||||
}
|
||||
|
||||
export interface AirbaseOptions {
|
||||
name: string;
|
||||
position: L.LatLng;
|
||||
name: string;
|
||||
position: L.LatLng;
|
||||
}
|
||||
|
||||
export interface AirbaseChartData {
|
||||
elevation: string;
|
||||
ICAO: string;
|
||||
TACAN: string;
|
||||
runways: AirbaseChartRunwayData[];
|
||||
elevation: string;
|
||||
ICAO: string;
|
||||
TACAN: string;
|
||||
runways: AirbaseChartRunwayData[];
|
||||
}
|
||||
|
||||
export interface AirbaseChartRunwayHeadingData {
|
||||
[index: string]: {
|
||||
magHeading: string;
|
||||
ILS: string;
|
||||
};
|
||||
[index: string]: {
|
||||
magHeading: string;
|
||||
ILS: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AirbaseChartRunwayData {
|
||||
headings: AirbaseChartRunwayHeadingData[];
|
||||
length: string;
|
||||
headings: AirbaseChartRunwayHeadingData[];
|
||||
length: string;
|
||||
}
|
||||
|
||||
export interface ShortcutOptions {
|
||||
label: string;
|
||||
keyUpCallback: (e: KeyboardEvent) => void;
|
||||
keyDownCallback?: (e: KeyboardEvent) => void;
|
||||
code: string;
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
label: string;
|
||||
keyUpCallback: (e: KeyboardEvent) => void;
|
||||
keyDownCallback?: (e: KeyboardEvent) => void;
|
||||
code: string;
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerStatus {
|
||||
frameRate: number;
|
||||
load: number;
|
||||
elapsedTime: number;
|
||||
missionTime: DateAndTime["time"];
|
||||
connected: boolean;
|
||||
paused: boolean;
|
||||
frameRate: number;
|
||||
load: number;
|
||||
elapsedTime: number;
|
||||
missionTime: DateAndTime["time"];
|
||||
connected: boolean;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
export type DrawingPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type PolygonPoints = DrawingPoint[] | DrawingPoint;
|
||||
@ -406,36 +408,36 @@ export type PolygonPoints = DrawingPoint[] | DrawingPoint;
|
||||
export type DrawingPrimitiveType = "TextBox" | "Polygon" | "Line" | "Icon";
|
||||
|
||||
export interface Drawing {
|
||||
name: string;
|
||||
visible: boolean;
|
||||
mapX: number;
|
||||
mapY: number;
|
||||
layerName: string;
|
||||
layer: string;
|
||||
primitiveType: DrawingPrimitiveType;
|
||||
colorString: string;
|
||||
fillColorString?: string;
|
||||
borderThickness?: number;
|
||||
fontSize?: number;
|
||||
font?: string;
|
||||
text?: string;
|
||||
angle?: number;
|
||||
radius?: number;
|
||||
points?: PolygonPoints;
|
||||
style?: string;
|
||||
polygonMode?: string;
|
||||
thickness?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
closed?: boolean;
|
||||
lineMode?: string;
|
||||
hiddenOnPlanner?: boolean;
|
||||
file?: string;
|
||||
scale?: number;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
mapX: number;
|
||||
mapY: number;
|
||||
layerName: string;
|
||||
layer: string;
|
||||
primitiveType: DrawingPrimitiveType;
|
||||
colorString: string;
|
||||
fillColorString?: string;
|
||||
borderThickness?: number;
|
||||
fontSize?: number;
|
||||
font?: string;
|
||||
text?: string;
|
||||
angle?: number;
|
||||
radius?: number;
|
||||
points?: PolygonPoints;
|
||||
style?: string;
|
||||
polygonMode?: string;
|
||||
thickness?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
closed?: boolean;
|
||||
lineMode?: string;
|
||||
hiddenOnPlanner?: boolean;
|
||||
file?: string;
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export enum AlarmState {
|
||||
RED = 'red',
|
||||
GREEN = 'green',
|
||||
AUTO = 'auto'
|
||||
RED = "red",
|
||||
GREEN = "green",
|
||||
AUTO = "auto",
|
||||
}
|
||||
|
||||
@ -310,11 +310,13 @@ export class OlympusApp {
|
||||
}
|
||||
|
||||
setState(state: OlympusState, subState: OlympusSubState = NO_SUBSTATE) {
|
||||
const previousState = this.#state;
|
||||
const previousSubState = this.#subState;
|
||||
this.#state = state;
|
||||
this.#subState = subState;
|
||||
|
||||
console.log(`App state set to ${state}, substate ${subState}`);
|
||||
AppStateChangedEvent.dispatch(state, subState);
|
||||
AppStateChangedEvent.dispatch(state, subState, previousState, previousSubState);
|
||||
}
|
||||
|
||||
getState() {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
AudioSinksChangedEvent,
|
||||
AudioSourcesChangedEvent,
|
||||
CoalitionAreasChangedEvent,
|
||||
CustomLoadoutsUpdatedEvent,
|
||||
DrawingsUpdatedEvent,
|
||||
HotgroupsChangedEvent,
|
||||
MapSourceChangedEvent,
|
||||
@ -16,7 +17,7 @@ import {
|
||||
SessionDataSavedEvent,
|
||||
StarredSpawnsChangedEvent,
|
||||
} from "./events";
|
||||
import { SessionData } from "./interfaces";
|
||||
import { LoadoutBlueprint, SessionData } from "./interfaces";
|
||||
import { CoalitionCircle } from "./map/coalitionarea/coalitioncircle";
|
||||
import { CoalitionPolygon } from "./map/coalitionarea/coalitionpolygon";
|
||||
import { getApp } from "./olympusapp";
|
||||
@ -124,7 +125,7 @@ export class SessionDataManager {
|
||||
HotgroupsChangedEvent.on((hotgroups) => {
|
||||
this.#sessionData.hotgroups = {};
|
||||
Object.keys(hotgroups).forEach((hotgroup) => {
|
||||
(this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[hotgroup].map((unit) => unit.ID);
|
||||
(this.#sessionData.hotgroups as { [key: string]: number[] })[hotgroup] = hotgroups[parseInt(hotgroup)].map((unit) => unit.ID);
|
||||
});
|
||||
this.#saveSessionData();
|
||||
});
|
||||
@ -146,6 +147,16 @@ export class SessionDataManager {
|
||||
this.#sessionData.mapSource = { id: source };
|
||||
this.#saveSessionData();
|
||||
});
|
||||
|
||||
CustomLoadoutsUpdatedEvent.on((unitName, loadout) => {
|
||||
// If the loadout is of type isPersistent, update the session data
|
||||
if (loadout.persistent) {
|
||||
if (!this.#sessionData.customLoadouts) this.#sessionData.customLoadouts = {};
|
||||
if (!this.#sessionData.customLoadouts[unitName]) this.#sessionData.customLoadouts[unitName] = [];
|
||||
this.#sessionData.customLoadouts[unitName].push({...loadout});
|
||||
}
|
||||
this.#saveSessionData();
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,8 +8,9 @@ export function Modal(props: {
|
||||
open: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
size?: "sm" | "md" | "lg" | "full";
|
||||
size?: "sm" | "md" | "lg" | "full" | "tall";
|
||||
disableClose?: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [splash, setSplash] = useState(Math.ceil(Math.random() * 7));
|
||||
|
||||
@ -54,6 +55,14 @@ export function Modal(props: {
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
props.size === "tall"
|
||||
? `
|
||||
h-[80%] w-[800px]
|
||||
max-md:h-full max-md:w-full
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${props.size === "full" ? "h-full w-full" : ""}
|
||||
`}
|
||||
>
|
||||
@ -90,7 +99,7 @@ export function Modal(props: {
|
||||
>
|
||||
<FaXmark
|
||||
onClick={() => {
|
||||
getApp().setState(OlympusState.IDLE);
|
||||
props.onClose ? props.onClose() : getApp().setState(OlympusState.IDLE);
|
||||
}}
|
||||
/>{" "}
|
||||
</div>
|
||||
|
||||
160
frontend/react/src/ui/modals/loadoutwizardmodal.tsx
Normal file
160
frontend/react/src/ui/modals/loadoutwizardmodal.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Modal } from "./components/modal";
|
||||
import { FaMagic, FaStar } from "react-icons/fa";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { NO_SUBSTATE, OlympusState } from "../../constants/constants";
|
||||
import { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent } from "../../events";
|
||||
import { WeaponsWizard } from "../panels/components/weaponswizard";
|
||||
import { LoadoutBlueprint, LoadoutItemBlueprint, UnitBlueprint } from "../../interfaces";
|
||||
import { OlToggle } from "../components/oltoggle";
|
||||
|
||||
export function LoadoutWizardModal(props: { open: boolean }) {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
|
||||
const [previousState, setPreviousState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
const [previousSubState, setPreviousSubState] = useState(NO_SUBSTATE);
|
||||
const [blueprint, setBlueprint] = useState(null as UnitBlueprint | null);
|
||||
const [isPersistent, setIsPersistent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppStateChangedEvent.on((appState, appSubState, previousState, previousSubState) => {
|
||||
setAppState(appState);
|
||||
setAppSubState(appSubState);
|
||||
setPreviousState(previousState);
|
||||
setPreviousSubState(previousSubState);
|
||||
});
|
||||
SetLoadoutWizardBlueprintEvent.on((blueprint) => {
|
||||
setBlueprint(blueprint);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear blueprint when modal is closed
|
||||
if (!props.open) {
|
||||
setBlueprint(null);
|
||||
}
|
||||
}, [props.open]);
|
||||
|
||||
const [selectedWeapons, setSelectedWeapons] = useState({} as { [key: string]: { clsid: string; name: string; weight: number } });
|
||||
const [loadoutName, setLoadoutName] = useState("New loadout");
|
||||
const [loadoutRole, setLoadoutRole] = useState("Custom");
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWeapons({});
|
||||
}, [props.open]);
|
||||
|
||||
// If "New Loadout" already exists in the blueprint loadouts, append a number to make it unique
|
||||
useEffect(() => {
|
||||
if (!blueprint) return;
|
||||
let name = "New loadout";
|
||||
let counter = 1;
|
||||
const existingLoadoutNames = blueprint.loadouts?.map((loadout) => loadout.name) || [];
|
||||
|
||||
while (existingLoadoutNames.includes(name)) {
|
||||
name = `New loadout ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
setLoadoutName(name);
|
||||
}, [blueprint]);
|
||||
|
||||
return (
|
||||
<Modal open={props.open} size={"tall"} onClose={() => getApp().setState(previousState, previousSubState)}>
|
||||
<div className="flex gap-4 text-xl text-white">
|
||||
<FaMagic
|
||||
className={`
|
||||
my-auto text-4xl text-gray-300
|
||||
`}
|
||||
/>
|
||||
<div className="my-auto">Loadout wizard</div>
|
||||
</div>
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={blueprint?.acceptedPayloads ?? {}}
|
||||
loadoutName={loadoutName}
|
||||
setLoadoutName={setLoadoutName}
|
||||
loadoutRole={loadoutRole}
|
||||
setLoadoutRole={setLoadoutRole}
|
||||
/>
|
||||
<div className="mt-auto flex justify-between">
|
||||
<div className="flex gap-2 text-gray-200">
|
||||
<FaStar className={`my-auto text-2xl text-gray-200`}/>
|
||||
<div className={`my-auto mr-auto`}>Keep for the rest of the session</div>
|
||||
<OlToggle toggled={isPersistent} onClick={() => setIsPersistent(!isPersistent)} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Add a new loadout to the blueprint if it doesn't exist already
|
||||
if (blueprint) {
|
||||
const items: LoadoutItemBlueprint[] = [];
|
||||
for (const pylon in selectedWeapons) {
|
||||
const weapon = selectedWeapons[pylon];
|
||||
items.push({
|
||||
name: weapon.name,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Group the weapon items and sum their quantities if there are duplicates
|
||||
const groupedItems: LoadoutItemBlueprint[] = [];
|
||||
const itemMap: { [key: string]: LoadoutItemBlueprint } = {};
|
||||
for (const item of items) {
|
||||
if (itemMap[item.name]) {
|
||||
itemMap[item.name].quantity += item.quantity;
|
||||
} else {
|
||||
itemMap[item.name] = { ...item };
|
||||
}
|
||||
}
|
||||
for (const itemName in itemMap) {
|
||||
groupedItems.push(itemMap[itemName]);
|
||||
}
|
||||
|
||||
// Assemble the loadout payload section as a stringified lua table containing the payload number as key and the clsid as values
|
||||
// This must already be lua compatible
|
||||
let payloadLuaTable = "{pylons = {";
|
||||
for (const pylon in selectedWeapons) {
|
||||
const weapon = selectedWeapons[pylon];
|
||||
if (weapon) payloadLuaTable += `[${pylon}] = {CLSID = "${weapon.clsid}"},`;
|
||||
}
|
||||
payloadLuaTable += "}, fuel = 999999, flare=60, chaff=60, gun=100, ammo_type = 1}";
|
||||
|
||||
const newLoadout: LoadoutBlueprint = {
|
||||
items: groupedItems,
|
||||
roles: [loadoutRole],
|
||||
code: "",
|
||||
name: loadoutName,
|
||||
enabled: true,
|
||||
isCustom: true,
|
||||
persistent: isPersistent,
|
||||
payload: payloadLuaTable,
|
||||
};
|
||||
|
||||
if (!blueprint.loadouts) {
|
||||
blueprint.loadouts = [];
|
||||
}
|
||||
blueprint.loadouts.push(newLoadout);
|
||||
CustomLoadoutsUpdatedEvent.dispatch(blueprint.name, newLoadout);
|
||||
}
|
||||
getApp().setState(previousState, previousSubState);
|
||||
}}
|
||||
className={`
|
||||
mb-2 me-2 flex content-center items-center gap-2
|
||||
rounded-sm bg-blue-700 px-5 py-2.5 text-sm 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
|
||||
`}
|
||||
>
|
||||
Continue
|
||||
<FontAwesomeIcon className={`my-auto`} icon={faArrowRight} />
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -1,20 +1,38 @@
|
||||
import React, { useState } from "react";
|
||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FaArrowsRotate, FaTrash, FaXmark } from "react-icons/fa6";
|
||||
import { OlSearchBar } from "../../components/olsearchbar";
|
||||
import { OlCheckbox } from "../../components/olcheckbox";
|
||||
import { OlToggle } from "../../components/oltoggle";
|
||||
export function WeaponsWizard(props: {
|
||||
selectedWeapons: { [key: string]: { clsids: string; name: string; weight: number } };
|
||||
setSelectedWeapons: (weapons: { [key: string]: { clsids: string; name: string; weight: number } }) => void;
|
||||
weaponsByPylon: { [key: string]: { clsids: string; name: string; weight: number }[] };
|
||||
selectedWeapons: { [key: string]: { clsid: string; name: string; weight: number } };
|
||||
setSelectedWeapons: (weapons: { [key: string]: { clsid: string; name: string; weight: number } }) => void;
|
||||
weaponsByPylon: { [key: string]: { clsid: string; name: string; weight: number }[] };
|
||||
loadoutName: string;
|
||||
setLoadoutName: (name: string) => void;
|
||||
loadoutRole: string;
|
||||
setLoadoutRole: (role: string) => void;
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [selectedPylons, setSelectedPylons] = useState<string[]>([]);
|
||||
const [autofillPylons, setAutofillPylons] = useState(false);
|
||||
const [fillEmptyOnly, setFillEmptyOnly] = useState(true);
|
||||
const [weaponLetters, setWeaponLetters] = useState<{ [key: string]: string }>({}); // Letter to weapon name mapping
|
||||
const [hoveredWeapon, setHoveredWeapon] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
// If autofill is enabled, clear selected pylons
|
||||
if (autofillPylons) {
|
||||
setSelectedPylons([]);
|
||||
}
|
||||
}, [autofillPylons]);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear search text when weaponsByPylon changes
|
||||
setSearchText("");
|
||||
setSelectedPylons([]);
|
||||
}, [props.weaponsByPylon]);
|
||||
|
||||
// Find the weapons that are availabile in all the selected pylons, meaning the intersection of the weapons in each pylon
|
||||
let availableWeapons: { clsids: string; name: string; weight: number }[] = [];
|
||||
let availableWeapons: { clsid: string; name: string; weight: number }[] = [];
|
||||
if (autofillPylons) {
|
||||
// If autofill is enabled, show all weapons
|
||||
availableWeapons = Object.values(props.weaponsByPylon).flat();
|
||||
@ -39,91 +57,266 @@ export function WeaponsWizard(props: {
|
||||
availableWeapons = availableWeapons.filter((weapon) => weapon.name.toLowerCase().includes(searchText.toLowerCase()));
|
||||
}
|
||||
|
||||
// If autofill is enabled and fillEmptyOnly is enabled, remove weapons that have no compatible empty pylons
|
||||
if (autofillPylons && fillEmptyOnly) {
|
||||
availableWeapons = availableWeapons.filter((weapon) => {
|
||||
// Check if there is at least one pylon that is compatible with this weapon and is empty
|
||||
return Object.keys(props.weaponsByPylon).some((pylon) => {
|
||||
const weaponsInPylon = props.weaponsByPylon[pylon];
|
||||
return weaponsInPylon.some((w) => w.name === weapon.name) && !props.selectedWeapons[pylon];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Assign a letter to each indiviual type of weapon selected in selectedWeapons for display in the pylon selection
|
||||
// Find the first unused letter
|
||||
Object.values(props.selectedWeapons).forEach((weapon) => {
|
||||
if (Object.entries(weaponLetters).findIndex(([letter, name]) => name === weapon.name) === -1) {
|
||||
// Find the first unused letter starting from A
|
||||
let currentLetter = "A";
|
||||
while (weaponLetters[currentLetter]) {
|
||||
currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1);
|
||||
}
|
||||
weaponLetters[currentLetter] = weapon.name;
|
||||
currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove letters for weapons that are no longer selected
|
||||
Object.entries(weaponLetters).forEach(([letter, name]) => {
|
||||
if (Object.values(props.selectedWeapons).findIndex((weapon) => weapon.name === name) === -1) {
|
||||
delete weaponLetters[letter];
|
||||
}
|
||||
});
|
||||
|
||||
if (JSON.stringify(weaponLetters) !== JSON.stringify(weaponLetters)) setWeaponLetters({ ...weaponLetters });
|
||||
|
||||
// List of very bright and distinct colors
|
||||
const colors = {
|
||||
A: "#FF5733",
|
||||
B: "#33FF57",
|
||||
C: "#3357FF",
|
||||
D: "#F333FF",
|
||||
E: "#33FFF5",
|
||||
F: "#F5FF33",
|
||||
G: "#FF33A8",
|
||||
H: "#A833FF",
|
||||
I: "#33FFA8",
|
||||
J: "#FFA833",
|
||||
K: "#33A8FF",
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-center">
|
||||
{Object.keys(props.weaponsByPylon).map((pylon) => (
|
||||
<div key={pylon} className={``}>
|
||||
<div className="flex flex-col gap-6 text-white" onMouseEnter={() => setHoveredWeapon("")}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="my-auto font-semibold">Loadout Name</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.loadoutName}
|
||||
onChange={(e) => props.setLoadoutName(e.target.value)}
|
||||
className={`
|
||||
rounded-md border border-gray-300 bg-gray-800 p-2
|
||||
text-sm text-white
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="my-auto font-semibold">Loadout Role</div>
|
||||
<input
|
||||
type="text"
|
||||
value={props.loadoutRole}
|
||||
onChange={(e) => props.setLoadoutRole(e.target.value)}
|
||||
className={`
|
||||
rounded-md border border-gray-300 bg-gray-800 p-2
|
||||
text-sm text-white
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400">Select weapons for each pylon</span>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="mx-auto flex flex-col gap-2">
|
||||
{/* Draw an airplane seen from the front using only gray lines */}
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={`
|
||||
flex h-20 flex-col items-center justify-center
|
||||
rounded-md border px-1
|
||||
${
|
||||
autofillPylons
|
||||
? `
|
||||
text-gray-400
|
||||
`
|
||||
: `
|
||||
cursor-pointer
|
||||
hover:bg-gray-700
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedPylons.includes(pylon)
|
||||
? `
|
||||
border-gray-200
|
||||
`
|
||||
: `border-transparent`
|
||||
}
|
||||
border-b-2 border- b-2 w-full border-gray-300
|
||||
`}
|
||||
onClick={() => {
|
||||
if (autofillPylons) return;
|
||||
if (selectedPylons.includes(pylon)) {
|
||||
setSelectedPylons(selectedPylons.filter((p) => p !== pylon));
|
||||
} else {
|
||||
setSelectedPylons([...selectedPylons, pylon]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`text-center text-xs`}>{pylon}</div>
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
h-3 w-0 rounded-full border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
`}
|
||||
></div>
|
||||
{props.selectedWeapons[pylon] ? (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
flex h-6 w-6 items-center
|
||||
justify-center rounded-full border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
`}
|
||||
>
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
h-5 w-5 rounded-full
|
||||
data-[autofill='false']:bg-white
|
||||
data-[autofill='true']:bg-gray-400
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
h-6 w-6 rounded-full border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
h-14 min-w-14 rounded-full border-2
|
||||
border-gray-300
|
||||
`}
|
||||
></div>
|
||||
<div
|
||||
className={`
|
||||
border-b-2 border- b-2 w-full border-gray-300
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Buttons to select/deselect all pylons, clear all weapons and remove weapons from selected pylons */}
|
||||
<div>
|
||||
<div className="flex justify-center gap-2">
|
||||
{selectedPylons.length > 0 && (
|
||||
<>
|
||||
<div className="flex justify-center gap-1">
|
||||
{Object.keys(props.weaponsByPylon).map((pylon) => {
|
||||
let weapon = props.selectedWeapons[pylon];
|
||||
let letter = Object.entries(weaponLetters).find(([letter, name]) => name === weapon?.name)?.[0] || "";
|
||||
// If the currently hovered weapon is compatible with this pylon, show "Hovered" else "Not Hovered"
|
||||
let isHovered = props.weaponsByPylon[pylon].some((w) => w.name === hoveredWeapon);
|
||||
return (
|
||||
<div key={pylon} className={``}>
|
||||
<div
|
||||
className={`
|
||||
flex h-20 flex-col items-center
|
||||
justify-center rounded-md border
|
||||
px-1
|
||||
${
|
||||
autofillPylons
|
||||
? `text-gray-400`
|
||||
: `
|
||||
cursor-pointer
|
||||
hover:bg-gray-700
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedPylons.includes(pylon)
|
||||
? `
|
||||
border-gray-200
|
||||
`
|
||||
: `
|
||||
border-transparent
|
||||
`
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (autofillPylons) return;
|
||||
if (selectedPylons.includes(pylon)) {
|
||||
setSelectedPylons(selectedPylons.filter((p) => p !== pylon));
|
||||
} else {
|
||||
setSelectedPylons([...selectedPylons, pylon]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`text-center text-xs`}>{pylon}</div>
|
||||
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
className={`
|
||||
h-3 w-0 border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
`}
|
||||
></div>
|
||||
{props.selectedWeapons[pylon] ? (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
data-hovered={isHovered ? "true" : "false"}
|
||||
className={`
|
||||
flex h-6 w-6 items-center
|
||||
justify-center
|
||||
rounded-full border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
data-[hovered='true']:border-green-400
|
||||
`}
|
||||
>
|
||||
{/* Show the letter of the group the weapon belongs to from weaponLetters */}
|
||||
<span
|
||||
className={`
|
||||
text-sm font-bold
|
||||
`}
|
||||
style={{
|
||||
color: letter in colors ? colors[letter as keyof typeof colors] : "inherit",
|
||||
}}
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-autofill={autofillPylons ? "true" : "false"}
|
||||
data-hovered={isHovered ? "true" : "false"}
|
||||
className={`
|
||||
h-6 w-6 rounded-full
|
||||
border
|
||||
data-[autofill='false']:border-white
|
||||
data-[autofill='true']:border-gray-400
|
||||
data-[hovered='true']:border-green-400
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* List all the groups from weaponLetters */}
|
||||
<div className="flex flex-col gap-1">
|
||||
{Object.entries(weaponLetters).map(([letter, weapon]) => (
|
||||
<div
|
||||
key={letter}
|
||||
className={`
|
||||
flex items-center text-sm
|
||||
`}
|
||||
>
|
||||
<span className="font-bold" style={{ color: letter in colors ? colors[letter as keyof typeof colors] : "inherit" }}>
|
||||
{letter}:
|
||||
</span>
|
||||
<span className="ml-1 text-gray-400">{weapon}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Buttons to select/deselect all pylons, clear all weapons and remove weapons from selected pylons */}
|
||||
<div>
|
||||
<div className="flex justify-center gap-2">
|
||||
{selectedPylons.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700
|
||||
px-2 py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
setSelectedPylons([]);
|
||||
}}
|
||||
>
|
||||
<FaArrowsRotate className="inline" /> Reset selection
|
||||
</button>
|
||||
|
||||
{
|
||||
/* Checjk if any of the selected pylons have a weapon selected */
|
||||
props.selectedWeapons && selectedPylons.some((pylon) => props.selectedWeapons[pylon] !== undefined) && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md
|
||||
bg-gray-700 px-2 py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
// Remove weapons from selected pylons
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
selectedPylons.forEach((pylon) => {
|
||||
delete newSelectedWeapons[pylon];
|
||||
});
|
||||
props.setSelectedWeapons(newSelectedWeapons);
|
||||
}}
|
||||
>
|
||||
<FaXmark
|
||||
className={`
|
||||
inline text-red-500
|
||||
`}
|
||||
/>{" "}
|
||||
Remove
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{props.selectedWeapons && Object.keys(props.selectedWeapons).length > 0 && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700 px-2
|
||||
@ -131,56 +324,14 @@ export function WeaponsWizard(props: {
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
setSelectedPylons([]);
|
||||
// Clear all selected weapons
|
||||
props.setSelectedWeapons({});
|
||||
}}
|
||||
>
|
||||
<FaArrowsRotate className="inline" /> Reset selection
|
||||
<FaTrash className="inline text-red-500" /> Delete all
|
||||
</button>
|
||||
|
||||
{
|
||||
/* Checjk if any of the selected pylons have a weapon selected */
|
||||
props.selectedWeapons && selectedPylons.some((pylon) => props.selectedWeapons[pylon] !== undefined) && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700
|
||||
px-2 py-1 text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
// Remove weapons from selected pylons
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
selectedPylons.forEach((pylon) => {
|
||||
delete newSelectedWeapons[pylon];
|
||||
});
|
||||
props.setSelectedWeapons(newSelectedWeapons);
|
||||
}}
|
||||
>
|
||||
<FaXmark
|
||||
className={`
|
||||
inline text-red-500
|
||||
`}
|
||||
/>{" "}
|
||||
Remove
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
{props.selectedWeapons && Object.keys(props.selectedWeapons).length > 0 && (
|
||||
<button
|
||||
className={`
|
||||
text-nowrap rounded-md bg-gray-700 px-2 py-1
|
||||
text-sm
|
||||
hover:bg-gray-600
|
||||
`}
|
||||
onClick={() => {
|
||||
// Clear all selected weapons
|
||||
props.setSelectedWeapons({});
|
||||
}}
|
||||
>
|
||||
<FaTrash className="inline text-red-500" /> Delete all
|
||||
</button>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -193,6 +344,17 @@ export function WeaponsWizard(props: {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{autofillPylons && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="ml-2 text-sm">Only fill empty pylons</span>
|
||||
<OlToggle
|
||||
toggled={fillEmptyOnly}
|
||||
onClick={() => {
|
||||
setFillEmptyOnly(!fillEmptyOnly);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<OlSearchBar onChange={setSearchText} text={searchText} />
|
||||
|
||||
@ -202,13 +364,35 @@ export function WeaponsWizard(props: {
|
||||
border-gray-700 px-2
|
||||
`}
|
||||
>
|
||||
{availableWeapons.length === 0 ? (
|
||||
selectedPylons.length === 0 ? (
|
||||
<div className="p-2 text-sm text-gray-400">No pylons selected</div>
|
||||
) : (
|
||||
<div className="p-2 text-sm text-gray-400">No weapons compatible with all selected pylons</div>
|
||||
)
|
||||
) : (
|
||||
{selectedPylons.length === 0 && !autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No pylons selected
|
||||
</div>
|
||||
)}
|
||||
{availableWeapons.length === 0 && selectedPylons.length !== 0 && !autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No weapons compatible with all selected pylons
|
||||
</div>
|
||||
)}
|
||||
{availableWeapons.length === 0 && selectedPylons.length === 0 && autofillPylons && (
|
||||
<div
|
||||
className={`
|
||||
p-2 text-sm text-gray-400
|
||||
`}
|
||||
>
|
||||
No empty pylons available
|
||||
</div>
|
||||
)}
|
||||
|
||||
{availableWeapons.length !== 0 &&
|
||||
availableWeapons.map((weapon) => (
|
||||
<div
|
||||
key={weapon.name}
|
||||
@ -218,6 +402,10 @@ export function WeaponsWizard(props: {
|
||||
let newSelectedWeapons = { ...props.selectedWeapons };
|
||||
Object.keys(props.weaponsByPylon).forEach((pylon) => {
|
||||
const weaponsInPylon = props.weaponsByPylon[pylon];
|
||||
if (fillEmptyOnly && props.selectedWeapons[pylon]) {
|
||||
// If "Only fill empty pylons" is enabled, skip filled pylons
|
||||
return;
|
||||
}
|
||||
if (weaponsInPylon.some((w) => w.name === weapon.name)) {
|
||||
newSelectedWeapons[pylon] = weapon;
|
||||
}
|
||||
@ -233,6 +421,8 @@ export function WeaponsWizard(props: {
|
||||
setSelectedPylons([]);
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setHoveredWeapon(weapon.name)}
|
||||
onMouseLeave={() => setHoveredWeapon("")}
|
||||
className={`
|
||||
cursor-pointer rounded-md p-1 text-sm
|
||||
hover:bg-gray-700
|
||||
@ -240,8 +430,7 @@ export function WeaponsWizard(props: {
|
||||
>
|
||||
{weapon.name}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import { OlNumberInput } from "../components/olnumberinput";
|
||||
import { OlLabelToggle } from "../components/ollabeltoggle";
|
||||
import { OlRangeSlider } from "../components/olrangeslider";
|
||||
import { OlDropdownItem, OlDropdown } from "../components/oldropdown";
|
||||
import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint } from "../../interfaces";
|
||||
import { LoadoutBlueprint, SpawnRequestTable, UnitBlueprint, UnitSpawnTable } from "../../interfaces";
|
||||
import { OlStateButton } from "../components/olstatebutton";
|
||||
import { Coalition } from "../../types/types";
|
||||
import { getApp } from "../../olympusapp";
|
||||
@ -17,11 +17,10 @@ import { faArrowLeft, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { OlStringInput } from "../components/olstringinput";
|
||||
import { countryCodes } from "../data/codes";
|
||||
import { OlAccordion } from "../components/olaccordion";
|
||||
import { AppStateChangedEvent, SpawnHeadingChangedEvent } from "../../events";
|
||||
import { AppStateChangedEvent, CustomLoadoutsUpdatedEvent, SetLoadoutWizardBlueprintEvent, SpawnHeadingChangedEvent } from "../../events";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { FaQuestionCircle } from "react-icons/fa";
|
||||
import { FaMagic, FaQuestionCircle } from "react-icons/fa";
|
||||
import { OlExpandingTooltip } from "../components/olexpandingtooltip";
|
||||
import { WeaponsWizard } from "./components/weaponswizard";
|
||||
import { LoadoutViewer } from "./components/loadoutviewer";
|
||||
|
||||
enum OpenAccordion {
|
||||
@ -59,7 +58,7 @@ export function UnitSpawnMenu(props: {
|
||||
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
|
||||
const [spawnLiveryID, setSpawnLiveryID] = useState("");
|
||||
const [spawnSkill, setSpawnSkill] = useState("High");
|
||||
const [selectedWeapons, setSelectedWeapons] = useState({} as { [key: string]: { clsids: string; name: string; weight: number } });
|
||||
|
||||
const [quickAccessName, setQuickAccessName] = useState("Preset 1");
|
||||
const [key, setKey] = useState("");
|
||||
const [spawnRequestTable, setSpawnRequestTable] = useState(null as null | SpawnRequestTable);
|
||||
@ -70,13 +69,16 @@ export function UnitSpawnMenu(props: {
|
||||
useEffect(() => {
|
||||
setAppState(getApp()?.getState());
|
||||
AppStateChangedEvent.on((state, subState) => setAppState(state));
|
||||
CustomLoadoutsUpdatedEvent.on((unitName, loadout) => {
|
||||
setSpawnRole(loadout.roles[0]);
|
||||
setSpawnLoadout(loadout);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSpawnRole("");
|
||||
setSpawnLoadout(null);
|
||||
setSpawnLiveryID("");
|
||||
setSelectedWeapons({});
|
||||
}, [props.blueprint]);
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
@ -115,16 +117,25 @@ export function UnitSpawnMenu(props: {
|
||||
/* Callback and effect to update the spawn request table */
|
||||
const updateSpawnRequestTable = useCallback(() => {
|
||||
if (props.blueprint !== null) {
|
||||
const loadoutCode = spawnLoadout ? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.code ?? "") : "";
|
||||
const loadoutPayload = spawnLoadout
|
||||
? (props.blueprint.loadouts?.find((loadout) => loadout.name === spawnLoadout.name)?.payload ?? undefined)
|
||||
: undefined;
|
||||
|
||||
const unitTable: UnitSpawnTable = {
|
||||
unitType: props.blueprint?.name,
|
||||
location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
|
||||
skill: spawnSkill,
|
||||
liveryID: spawnLiveryID,
|
||||
altitude: ftToM(spawnAltitude),
|
||||
loadout: loadoutCode,
|
||||
};
|
||||
|
||||
if (loadoutPayload) unitTable.payload = loadoutPayload;
|
||||
|
||||
setSpawnRequestTable({
|
||||
category: props.blueprint?.category,
|
||||
unit: {
|
||||
unitType: props.blueprint?.name,
|
||||
location: props.latlng ?? new LatLng(0, 0), // This will be filled when the user clicks on the map to spawn the unit
|
||||
skill: spawnSkill,
|
||||
liveryID: spawnLiveryID,
|
||||
altitude: ftToM(spawnAltitude),
|
||||
loadout: props.blueprint?.loadouts?.find((loadout) => loadout.name === spawnLoadout?.name)?.code ?? "",
|
||||
},
|
||||
unit: unitTable,
|
||||
amount: spawnNumber,
|
||||
coalition: spawnCoalition,
|
||||
});
|
||||
@ -192,10 +203,49 @@ export function UnitSpawnMenu(props: {
|
||||
|
||||
/* Initialize the role */
|
||||
let allRoles = props.blueprint?.loadouts?.flatMap((loadout) => loadout.roles).filter((role) => role !== "No task");
|
||||
let mainRole = roles[0];
|
||||
|
||||
// If there are loadouts with Custom role, include Custom in the role selection
|
||||
const hasCustomRoleLoadouts = props.blueprint?.loadouts?.some((loadout) => loadout.roles.includes("Custom"));
|
||||
if (hasCustomRoleLoadouts && allRoles) allRoles.push("Custom");
|
||||
|
||||
// If there are custom loadouts, select "Custom" as the main role
|
||||
let mainRole = hasCustomRoleLoadouts ? "Custom" : roles[0];
|
||||
if (allRoles !== undefined) mainRole = mode(allRoles);
|
||||
spawnRole === "" && roles.length > 0 && setSpawnRole(mainRole);
|
||||
|
||||
// Filter the loadouts based on the selected role
|
||||
const filteredLoadouts = props.blueprint?.loadouts?.filter((loadout) => loadout.roles.includes(spawnRole));
|
||||
|
||||
// Order the loadouts so that custom loadouts appear first and "Empty loadout" appears last
|
||||
if (filteredLoadouts) {
|
||||
filteredLoadouts.sort((a, b) => {
|
||||
if (a.isCustom && !b.isCustom) return -1;
|
||||
if (!a.isCustom && b.isCustom) return 1;
|
||||
if (a.name === "Empty loadout") return 1;
|
||||
if (b.name === "Empty loadout") return -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
/* Effect to reset the loadout if the role changes */
|
||||
useEffect(() => {
|
||||
// If the current loadout is not in the filtered loadouts, reset it
|
||||
if (spawnLoadout && filteredLoadouts && !filteredLoadouts.includes(spawnLoadout)) {
|
||||
setSpawnLoadout(null);
|
||||
}
|
||||
}, [spawnRole, props.blueprint]);
|
||||
|
||||
/* Initialize the loadout */
|
||||
const initializeLoadout = useCallback(() => {
|
||||
if (spawnLoadout && filteredLoadouts && filteredLoadouts.includes(spawnLoadout)) return;
|
||||
if (filteredLoadouts && filteredLoadouts.length > 0) {
|
||||
if (filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout").length > 0)
|
||||
setSpawnLoadout(filteredLoadouts.filter((loadout) => loadout.name !== "Empty loadout")[0]);
|
||||
else setSpawnLoadout(filteredLoadouts[0]);
|
||||
}
|
||||
}, [filteredLoadouts, spawnLoadout]);
|
||||
useEffect(initializeLoadout, [filteredLoadouts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.compact ? (
|
||||
@ -207,7 +257,7 @@ export function UnitSpawnMenu(props: {
|
||||
`}
|
||||
>
|
||||
<div className="mr-2">
|
||||
<div className="flex h-fit flex-col gap-3">
|
||||
<div className="flex h-fit flex-col gap-2">
|
||||
<div className="flex">
|
||||
<FontAwesomeIcon
|
||||
onClick={props.onBack}
|
||||
@ -246,10 +296,11 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
my-auto text-sm text-white
|
||||
my-auto text-nowrap text-sm
|
||||
text-white
|
||||
`}
|
||||
>
|
||||
Quick access:{" "}
|
||||
Quick access:
|
||||
</div>
|
||||
<OlStringInput
|
||||
onChange={(e) => {
|
||||
@ -372,6 +423,87 @@ export function UnitSpawnMenu(props: {
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center
|
||||
justify-between gap-2
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Loadout
|
||||
</span>
|
||||
<OlDropdown
|
||||
label={spawnLoadout ? spawnLoadout.name : "Default"}
|
||||
className={`w-full`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit loadout"
|
||||
content="Selects the loadout of the spawned unit. Depending on the selection, the unit will be equipped with different weapons and equipment."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{filteredLoadouts?.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={loadout.name}
|
||||
onClick={() => {
|
||||
setSpawnLoadout(loadout);
|
||||
}}
|
||||
className={`
|
||||
w-full
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full
|
||||
content-center
|
||||
overflow-hidden
|
||||
text-ellipsis
|
||||
text-nowrap
|
||||
text-left
|
||||
w-max-full
|
||||
flex gap-2
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
{loadout.name}
|
||||
</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
flex w-full justify-center
|
||||
gap-2 rounded-md
|
||||
border-[1px] p-2
|
||||
align-middle text-sm
|
||||
dark:text-white
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
SetLoadoutWizardBlueprintEvent.dispatch(props.blueprint!);
|
||||
getApp().setState(OlympusState.SPAWN, SpawnSubState.LOADOUT_WIZARD);
|
||||
}}
|
||||
>
|
||||
<FaMagic className="my-auto" />
|
||||
Loadout wizard
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<OlAccordion
|
||||
@ -640,21 +772,6 @@ export function UnitSpawnMenu(props: {
|
||||
>
|
||||
{props.blueprint ? <OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} /> : <span></span>}
|
||||
</OlAccordion>
|
||||
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setOpenAccordion(openAccordion === OpenAccordion.LOADOUT ? OpenAccordion.NONE : OpenAccordion.LOADOUT);
|
||||
}}
|
||||
open={openAccordion === OpenAccordion.LOADOUT}
|
||||
title="Loadout wizard"
|
||||
>
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={props.blueprint?.acceptedPayloads ?? {}}
|
||||
/>
|
||||
</OlAccordion>
|
||||
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
@ -1056,6 +1173,75 @@ export function UnitSpawnMenu(props: {
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div
|
||||
className={`
|
||||
flex flex-col content-center
|
||||
justify-between gap-2
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
my-auto font-normal
|
||||
dark:text-gray-400
|
||||
`}
|
||||
>
|
||||
Loadout
|
||||
</span>
|
||||
<OlDropdown
|
||||
label={spawnLoadout ? spawnLoadout.name : "Default"}
|
||||
className={`w-full`}
|
||||
tooltip={() => (
|
||||
<OlExpandingTooltip
|
||||
title="Unit loadout"
|
||||
content="Selects the loadout of the spawned unit. Depending on the selection, the unit will be equipped with different weapons and equipment."
|
||||
/>
|
||||
)}
|
||||
tooltipRelativeToParent={true}
|
||||
>
|
||||
{filteredLoadouts?.map((loadout) => {
|
||||
return (
|
||||
<OlDropdownItem
|
||||
key={loadout.name}
|
||||
onClick={() => {
|
||||
setSpawnLoadout(loadout);
|
||||
}}
|
||||
className={`w-full`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
w-full content-center
|
||||
overflow-hidden
|
||||
text-ellipsis
|
||||
text-nowrap text-left
|
||||
w-max-full flex gap-2
|
||||
`}
|
||||
>
|
||||
<div className="my-auto">{loadout.name}</div>
|
||||
</span>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</OlDropdown>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={`
|
||||
flex w-full justify-center gap-2
|
||||
rounded-md border-[1px] p-2
|
||||
align-middle text-sm
|
||||
dark:text-white
|
||||
hover:bg-white/10
|
||||
`}
|
||||
onClick={() => {
|
||||
SetLoadoutWizardBlueprintEvent.dispatch(props.blueprint!);
|
||||
getApp().setState(OlympusState.SPAWN, SpawnSubState.LOADOUT_WIZARD);
|
||||
}}
|
||||
>
|
||||
<FaMagic className="my-auto" />
|
||||
Loadout wizard
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-5 flex justify-between">
|
||||
<div className="my-auto flex flex-col gap-2">
|
||||
<span className="text-white">Spawn heading</span>
|
||||
@ -1069,13 +1255,7 @@ export function UnitSpawnMenu(props: {
|
||||
my-auto
|
||||
`}
|
||||
/>{" "}
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
`}
|
||||
>
|
||||
Drag to change
|
||||
</div>
|
||||
<div className={`my-auto`}>Drag to change</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1128,38 +1308,8 @@ export function UnitSpawnMenu(props: {
|
||||
></img>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex h-fit flex-col gap-1 px-4 py-2
|
||||
dark:bg-olympus-200/30
|
||||
`}
|
||||
>
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setOpenAccordion(openAccordion === OpenAccordion.LOADOUT ? OpenAccordion.NONE : OpenAccordion.LOADOUT);
|
||||
}}
|
||||
open={openAccordion === OpenAccordion.LOADOUT}
|
||||
title="Loadout wizard"
|
||||
>
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={props.blueprint?.acceptedPayloads ?? {}}
|
||||
/>
|
||||
</OlAccordion>
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<OlAccordion
|
||||
onClick={() => {
|
||||
setShowLoadout(!showLoadout);
|
||||
}}
|
||||
open={showLoadout}
|
||||
title="Loadout"
|
||||
>
|
||||
<LoadoutViewer spawnLoadout={spawnLoadout} />
|
||||
</OlAccordion>
|
||||
)}
|
||||
<div className="text-gray-200">Loadout</div>
|
||||
{spawnLoadout && <LoadoutViewer spawnLoadout={spawnLoadout} />}
|
||||
</div>
|
||||
|
||||
{props.airbase && (
|
||||
|
||||
@ -7,7 +7,7 @@ import { UnitControlMenu } from "./panels/unitcontrolmenu";
|
||||
import { MainMenu } from "./panels/mainmenu";
|
||||
import { SideBar } from "./panels/sidebar";
|
||||
import { OptionsMenu } from "./panels/optionsmenu";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
|
||||
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, SpawnSubState, UnitControlSubState } from "../constants/constants";
|
||||
import { getApp, setupApp } from "../olympusapp";
|
||||
import { LoginModal } from "./modals/loginmodal";
|
||||
|
||||
@ -32,6 +32,7 @@ import { WarningModal } from "./modals/warningmodal";
|
||||
import { TrainingModal } from "./modals/trainingmodal";
|
||||
import { AdminModal } from "./modals/adminmodal";
|
||||
import { ImageOverlayModal } from "./modals/imageoverlaymodal";
|
||||
import { LoadoutWizardModal } from "./modals/loadoutwizardmodal";
|
||||
|
||||
export function UI() {
|
||||
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
|
||||
@ -76,6 +77,7 @@ export function UI() {
|
||||
<TrainingModal open={appState === OlympusState.TRAINING} />
|
||||
<AdminModal open={appState === OlympusState.ADMIN} />
|
||||
<ImageOverlayModal open={appState === OlympusState.IMPORT_IMAGE_OVERLAY} />
|
||||
<LoadoutWizardModal open={appState === OlympusState.SPAWN && appSubState === SpawnSubState.LOADOUT_WIZARD} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,12 +1,26 @@
|
||||
import { getApp } from "../../olympusapp";
|
||||
import { GAME_MASTER } from "../../constants/constants";
|
||||
import { UnitBlueprint } from "../../interfaces";
|
||||
import { UnitDatabaseLoadedEvent } from "../../events";
|
||||
import { SessionDataLoadedEvent, UnitDatabaseLoadedEvent } from "../../events";
|
||||
|
||||
export class UnitDatabase {
|
||||
blueprints: { [key: string]: UnitBlueprint } = {};
|
||||
|
||||
constructor() {}
|
||||
constructor() {
|
||||
SessionDataLoadedEvent.on((sessionData) => {
|
||||
// Check if the sessionData customloadouts contains any loadouts for units, and if so, update the blueprints
|
||||
if (sessionData.customLoadouts) {
|
||||
for (let unitName in sessionData.customLoadouts) {
|
||||
if (this.blueprints[unitName]) {
|
||||
if (!this.blueprints[unitName].loadouts) this.blueprints[unitName].loadouts = [];
|
||||
sessionData.customLoadouts[unitName].forEach((loadout) => {
|
||||
this.blueprints[unitName].loadouts?.push(loadout);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
load(url: string, category?: string) {
|
||||
if (url !== "") {
|
||||
@ -204,7 +218,7 @@ export class UnitDatabase {
|
||||
getLoadoutNamesByRole(name: string, role: string) {
|
||||
var filteredBlueprints = this.getBlueprints();
|
||||
var loadoutsByRole: string[] = [];
|
||||
var loadouts = filteredBlueprints[name].loadouts;
|
||||
var loadouts = filteredBlueprints[name as any].loadouts;
|
||||
if (loadouts) {
|
||||
for (let loadout of loadouts) {
|
||||
if (loadout.roles.includes(role) || loadout.roles.includes("")) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user