16 Commits

Author SHA1 Message Date
Pax1601
b42280133c Completed custom weapon wizard modal 2025-10-26 16:34:27 +01:00
Pax1601
94d0b4d10e Merge branch 'release-candidate' into weapon-wizard 2025-10-25 15:17:18 +02:00
Pax1601
057603f926 First implementation of weapon wizard 2025-10-23 18:07:55 +02:00
Pax1601
bfd11c49af Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate 2025-10-23 18:07:35 +02:00
Pax1601
2a9723b932 Add image overlay import modal and menu option
Introduces ImageOverlayModal for importing image overlays with user-specified corner coordinates. Adds a menu item to trigger the modal and integrates it into the main UI component. Also updates OlNumberInput to support an internalClassName prop for styling flexibility.
2025-10-23 18:06:29 +02:00
Pax1601
f565b9ee6e Add type annotations and key conversions in Map class
Improves type safety by adding explicit type annotations to method parameters and callback functions in the Map class. Updates key handling for object properties to ensure correct types, particularly when interacting with ContextActions, MapOptions, MapHiddenTypes, and destination preview markers.
2025-10-21 17:34:20 +02:00
Pax1601
504c0a0ed9 fix: Map now works with capitalized map names (thanks for creating this need Wirts :P) 2025-10-15 18:50:40 +02:00
Pax1601
c77173f7c9 Enable AAA capability for infantry and fix map layer key case
Set 'canAAA' to true for several infantry units in groundunitdatabase.json, allowing them to engage air targets. Updated map.ts to handle map layer keys in a case-insensitive manner, preventing issues with mismatched key casing.
2025-10-13 22:50:36 +02:00
Pax1601
73af60d91b Updated unit databases and new spawn loadout system 2025-10-12 15:11:55 +02:00
Pax1601
31d7fb6051 Update mist.lua 2025-09-28 12:10:20 +02:00
Pax1601
def15f5565 Merge branch 'python-api' into release-candidate 2025-09-27 18:08:13 +02:00
Pax1601
dca8f9189f Merge branch 'python-api' into release-candidate 2025-09-11 21:47:29 +02:00
Pax1601
74b446d157 Updated unit database creation files 2025-08-16 17:09:32 +02:00
Pax1601
151196e5f2 Merge branch 'python-api' into release-candidate 2025-08-08 10:18:01 +02:00
Pax1601
8404d4d956 Merge pull request #1128 from Rob2816/release-candidate
Added groupID property to dead units
2025-08-07 11:02:19 +02:00
Rob2816
37fa86dce8 Added groupID property to dead units 2025-08-07 10:52:28 +02:00
38 changed files with 151747 additions and 40966 deletions

View File

@@ -185,6 +185,7 @@ struct SpawnOptions {
string skill;
string liveryID;
double heading;
string payload;
};
struct CloneOptions {

View File

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

View File

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

View File

@@ -271,6 +271,8 @@ void Unit::getData(stringstream& ss, unsigned long long time)
appendNumeric(ss, datumIndex, alive);
datumIndex = DataIndex::unitID;
appendNumeric(ss, datumIndex, unitID);
datumIndex = DataIndex::groupID;
appendNumeric(ss, datumIndex, groupID);
}
else {
for (unsigned char datumIndex = DataIndex::startOfData + 1; datumIndex < DataIndex::lastIndex; datumIndex++)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -360,6 +360,7 @@ export enum OlympusState {
MEASURE = "Measure",
TRAINING = "Training",
ADMIN = "Admin",
IMPORT_IMAGE_OVERLAY = "Import image overlay"
}
export const NO_SUBSTATE = "No substate";
@@ -398,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

View File

@@ -2,401 +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;
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[];
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;
@@ -404,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",
}

View File

@@ -443,7 +443,7 @@ export class Map extends L.Map {
ctrlKey: false,
});
for (let contextActionName in ContextActions) {
for (const contextActionName of Object.keys(ContextActions) as Array<keyof typeof ContextActions>) {
const contextAction = ContextActions[contextActionName] as ContextAction;
if (contextAction.getOptions().code) {
getApp()
@@ -560,10 +560,15 @@ export class Map extends L.Map {
}
})
.then((res: any) => {
if ("alt-" + theatre.toLowerCase() in res) {
let template = `${mirror}/alt-${theatre.toLowerCase()}/{z}/{x}/{y}.png`;
// Convert the result keys to lower case to avoid case sensitivity issues
let key = undefined;
if ("alt-" + theatre.toLowerCase() in res) key = "alt-" + theatre.toLowerCase();
else if ("alt-" + theatre in res) key = "alt-" + theatre;
if (key) {
let template = `${mirror}/${key}/{z}/{x}/{y}.png`;
layers.push(
...res["alt-" + theatre.toLowerCase()].map((layerConfig: any) => {
...res[key].map((layerConfig: any) => {
return new L.TileLayer(template, {
...layerConfig,
crossOrigin: "",
@@ -626,13 +631,13 @@ export class Map extends L.Map {
return this.#spawnHeading;
}
addStarredSpawnRequestTable(key, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
addStarredSpawnRequestTable(key: string, spawnRequestTable: SpawnRequestTable, quickAccessName: string) {
this.#starredSpawnRequestTables[key] = spawnRequestTable;
this.#starredSpawnRequestTables[key].quickAccessName = quickAccessName;
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
}
removeStarredSpawnRequestTable(key) {
removeStarredSpawnRequestTable(key: string) {
if (key in this.#starredSpawnRequestTables) delete this.#starredSpawnRequestTables[key];
StarredSpawnsChangedEvent.dispatch(this.#starredSpawnRequestTables);
}
@@ -673,7 +678,7 @@ export class Map extends L.Map {
}
setHiddenType(key: string, value: boolean) {
this.#hiddenTypes[key] = value;
this.#hiddenTypes[key as keyof MapHiddenTypes] = value;
HiddenTypesChangedEvent.dispatch(this.#hiddenTypes);
}
@@ -783,13 +788,13 @@ export class Map extends L.Map {
return smokeMarker;
}
setOption(key, value) {
setOption<K extends keyof MapOptions>(key: K, value: MapOptions[K]) {
this.#options[key] = value;
MapOptionsChangedEvent.dispatch(this.#options, key);
MapOptionsChangedEvent.dispatch(this.#options, key as keyof MapOptions);
}
setOptions(options) {
this.#options = { ...options };
setOptions(options: Partial<MapOptions>) {
this.#options = { ...this.#options, ...options } as MapOptions;
MapOptionsChangedEvent.dispatch(this.#options);
}
@@ -1066,7 +1071,7 @@ export class Map extends L.Map {
false,
undefined,
undefined,
(hash) => {
(hash: string) => {
this.addTemporaryMarker(
e.latlng,
this.#spawnRequestTable?.unit.unitType ?? "unknown",
@@ -1234,7 +1239,7 @@ export class Map extends L.Map {
this.#lastMouseCoordinates = e.latlng;
MouseMovedEvent.dispatch(e.latlng);
getGroundElevation(e.latlng, (elevation) => {
getGroundElevation(e.latlng, (elevation: number) => {
MouseMovedEvent.dispatch(e.latlng, elevation);
});
@@ -1361,8 +1366,8 @@ export class Map extends L.Map {
.filter((unit) => !unit.getHuman());
Object.keys(this.#destinationPreviewMarkers).forEach((ID) => {
this.#destinationPreviewMarkers[ID].removeFrom(this);
delete this.#destinationPreviewMarkers[ID];
this.#destinationPreviewMarkers[parseInt(ID)].removeFrom(this);
delete this.#destinationPreviewMarkers[parseInt(ID)];
});
if (this.#keepRelativePositions) {
@@ -1380,7 +1385,7 @@ export class Map extends L.Map {
#moveDestinationPreviewMarkers() {
if (this.#keepRelativePositions) {
Object.entries(getApp().getUnitsManager().computeGroupDestination(this.#destinationRotationCenter, this.#destinationRotation)).forEach(([ID, latlng]) => {
this.#destinationPreviewMarkers[ID]?.setLatLng(latlng);
this.#destinationPreviewMarkers[parseInt(ID)]?.setLatLng(latlng);
});
} else {
Object.values(this.#destinationPreviewMarkers).forEach((marker) => {

View File

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

View File

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

View File

@@ -156,8 +156,8 @@ export function OlDropdown(props: {
data-open={open}
className={`
absolute z-40 divide-y divide-gray-100 overflow-y-scroll
no-scrollbar rounded-lg bg-white p-2 shadow
dark:bg-gray-700
no-scrollbar rounded-lg border border-2 border-gray-600 bg-gray-700
p-2 shadow
data-[open='false']:hidden
`}
>
@@ -187,7 +187,7 @@ export function OlDropdown(props: {
}
/* Conveniency Component for dropdown elements */
export function OlDropdownItem(props: { onClick?: () => void; className?: string; borderColor?: string; children?: string | JSX.Element | JSX.Element[] }) {
export function OlDropdownItem(props: { onClick?: () => void; className?: string; borderColor?: string; children?: string | JSX.Element | JSX.Element[], disabled?: boolean }) {
return (
<button
onClick={props.onClick ?? (() => {})}
@@ -195,8 +195,11 @@ export function OlDropdownItem(props: { onClick?: () => void; className?: string
${props.className ?? ""}
flex w-full cursor-pointer select-none flex-row content-center
rounded-md px-4 py-2
dark:hover:bg-gray-600 dark:hover:text-white
hover:bg-gray-100
hover:bg-gray-600 hover:text-white
${props.disabled ? `
cursor-default opacity-50
hover:bg-transparent hover:text-gray-200
` : ``}
`}
style={{
border: props.borderColor ? `2px solid ${props.borderColor}` : "2px solid transparent",

View File

@@ -8,6 +8,7 @@ export function OlNumberInput(props: {
max: number;
minLength?: number;
className?: string;
internalClassName?: string;
tooltip?: string | (() => JSX.Element | JSX.Element[]);
tooltipPosition?: string;
tooltipRelativeToParent?: boolean;
@@ -34,7 +35,10 @@ export function OlNumberInput(props: {
`}
>
<div
className="relative flex max-w-[8rem] items-center"
className={`
relative flex max-w-[8rem] items-center
${props.internalClassName ?? ""}
`}
ref={buttonRef}
onMouseEnter={() => {
setHoverTimeout(

View File

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

View File

@@ -0,0 +1,180 @@
import React, { useEffect, useState } from "react";
import { Modal } from "./components/modal";
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 } from "../../events";
import { ImageOverlay, LatLng, LatLngBounds } from "leaflet";
import { OlNumberInput } from "../components/olnumberinput";
import { OlStringInput } from "../components/olstringinput";
export function ImageOverlayModal(props: { open: boolean }) {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE);
const [bound1Lat, setBound1Lat] = useState("0");
const [bound1Lon, setBound1Lon] = useState("0");
const [bound2Lat, setBound2Lat] = useState("0");
const [bound2Lon, setBound2Lon] = useState("0");
const [importData, setImportData] = useState("");
const [showWarning, setShowWarning] = useState(false);
useEffect(() => {
AppStateChangedEvent.on((appState, appSubState) => {
setAppState(appState);
setAppSubState(appSubState);
});
}, []);
useEffect(() => {
if (appState !== OlympusState.IMPORT_IMAGE_OVERLAY) return;
setImportData("");
var input = document.createElement("input");
input.type = "file";
input.onchange = async (e) => {
// @ts-ignore TODO
var file = e.target?.files[0];
var reader = new FileReader();
// Read the file content as image data URL
reader.readAsDataURL(file);
reader.onload = (readerEvent) => {
// @ts-ignore TODO
var content = readerEvent.target.result;
if (content) {
setImportData(content as string);
}
};
};
input.click();
}, [appState, appSubState]);
return (
<Modal open={props.open} size="sm">
<div className="flex h-full w-full flex-col justify-between">
<div className={`flex flex-col justify-between gap-2`}>
<span
className={`
text-gray-800 text-md
dark:text-white
`}
>
Import Image Overlay
</span>
<span className="text-gray-400">Enter the corner coordinates of the image overlay to be imported.</span>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="text-gray-300">Corner 1 latitude </div>
<div>
<OlStringInput
value={String(bound1Lat)}
onChange={(ev) => {
setBound1Lat(ev.target.value);
}}
/>
</div>
<div className="text-gray-300">Corner 1 longitude </div>
<div>
<OlStringInput
value={String(bound1Lon)}
onChange={(ev) => {
setBound1Lon(ev.target.value);
}}
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-gray-300">Corner 2 latitude </div>
<div>
<OlStringInput
value={String(bound2Lat)}
onChange={(ev) => {
setBound2Lat(ev.target.value);
}}
/>
</div>
<div className="text-gray-300">Corner 2 longitude </div>
<div>
<OlStringInput
value={String(bound2Lon)}
onChange={(ev) => {
setBound2Lon(ev.target.value);
}}
/>
</div>
</div>
<div className={`
${(showWarning ? "text-red-500" : `
text-gray-400
`)}
text-sm
`}>
Please enter valid latitude and longitude values in decimal degrees format (e.g. 37.7749, -122.4194). Latitude must be between -90 and 90, and longitude must be between -180 and 180.
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => {
if (
isNaN(Number(bound1Lat)) || Number(bound1Lat) < -90 || Number(bound1Lat) > 90 ||
isNaN(Number(bound1Lon)) || Number(bound1Lon) < -180 || Number(bound1Lon) > 180 ||
isNaN(Number(bound2Lat)) || Number(bound2Lat) < -90 || Number(bound2Lat) > 90 ||
isNaN(Number(bound2Lon)) || Number(bound2Lon) < -180 || Number(bound2Lon) > 180
) {
setShowWarning(true)
return;
}
setShowWarning(false)
const bounds = new LatLngBounds([
[Number(bound1Lat), Number(bound1Lon)],
[Number(bound2Lat), Number(bound2Lon)]
]
)
let overlay = new ImageOverlay(importData, bounds);
overlay.addTo(getApp().getMap());
getApp().setState(OlympusState.IDLE);
}}
className={`
mb-2 me-2 ml-auto 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 icon={faArrowRight} />
</button>
<button
type="button"
onClick={() => getApp().setState(OlympusState.IDLE)}
className={`
mb-2 me-2 flex content-center items-center gap-2
rounded-sm border-[1px] bg-blue-700 px-5 py-2.5
text-sm font-medium text-white
dark:border-gray-600 dark:bg-gray-800
dark:text-gray-400 dark:hover:bg-gray-700
dark:focus:ring-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
hover:bg-blue-800
`}
>
Back
</button>
</div>
</div>
</Modal>
);
}

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

View File

@@ -0,0 +1,35 @@
import React from "react";
export function LoadoutViewer(props: { spawnLoadout: { items: { name: string; quantity: number }[] } }) {
return (
<div>
{props.spawnLoadout.items.map((item) => {
return (
<div
className={`flex content-center gap-2`}
key={item.name}
>
<div
className={`
my-auto w-6 min-w-6 rounded-full py-0.5
text-center text-sm font-bold text-gray-500
dark:bg-[#17212D]
`}
>
{item.quantity}x
</div>
<div
className={`
my-auto overflow-hidden text-ellipsis text-nowrap
text-sm
dark:text-gray-300
`}
>
{item.name}
</div>
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,438 @@
import React, { useEffect, useState } from "react";
import { FaArrowsRotate, FaTrash, FaXmark } from "react-icons/fa6";
import { OlSearchBar } from "../../components/olsearchbar";
import { OlToggle } from "../../components/oltoggle";
export function WeaponsWizard(props: {
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: { clsid: string; name: string; weight: number }[] = [];
if (autofillPylons) {
// If autofill is enabled, show all weapons
availableWeapons = Object.values(props.weaponsByPylon).flat();
} else {
if (selectedPylons.length > 0) {
// If pylons are selected, show only weapons that are in all selected pylons
const weaponsInSelectedPylons = selectedPylons.map((pylon) => props.weaponsByPylon[pylon] || []);
availableWeapons = weaponsInSelectedPylons.reduce((acc, weapons) => {
return acc.filter((w) => weapons.some((w2) => w2.name === w.name));
});
}
}
// Sort alphabetically
availableWeapons.sort((a, b) => a.name.localeCompare(b.name));
// Remove duplicates
availableWeapons = availableWeapons.filter((weapon, index, self) => index === self.findIndex((w) => w.name === weapon.name));
// Filter by search text
if (searchText.trim() !== "") {
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-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={`
border-b-2 border- b-2 w-full border-gray-300
`}
></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 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
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>
<div className="flex items-center justify-between gap-2">
<span className="ml-2 text-sm">Autofill compatible pylons with weapon</span>
<OlToggle
toggled={autofillPylons}
onClick={() => {
setAutofillPylons(!autofillPylons);
}}
/>
</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} />
<div
className={`
flex max-h-48 flex-col overflow-y-auto border
border-gray-700 px-2
`}
>
{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}
onClick={() => {
if (autofillPylons) {
// Autofill all compatible pylons with the selected weapon
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;
}
});
props.setSelectedWeapons(newSelectedWeapons);
} else {
let newSelectedWeapons = { ...props.selectedWeapons };
// Add the weapon to the selected pylons
selectedPylons.forEach((pylon) => {
newSelectedWeapons[pylon] = weapon;
});
props.setSelectedWeapons(newSelectedWeapons);
setSelectedPylons([]);
}
}}
onMouseEnter={() => setHoveredWeapon(weapon.name)}
onMouseLeave={() => setHoveredWeapon("")}
className={`
cursor-pointer rounded-md p-1 text-sm
hover:bg-gray-700
`}
>
{weapon.name}
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -142,6 +142,31 @@ export function MainMenu(props: { open: boolean; onClose: () => void; children?:
/>
</div>
</div>
<div
className={`
group flex cursor-pointer select-none content-center gap-3
rounded-md p-2
dark:hover:bg-olympus-500
hover:bg-gray-900/10
`}
onClick={() => {
getApp().setState(OlympusState.IMPORT_IMAGE_OVERLAY);
}}
>
{/*<FontAwesomeIcon icon={faFileImport} className="my-auto w-4 text-gray-800 dark:text-gray-500" />*/}
Import image overlay
<div className={`ml-auto flex items-center`}>
<FontAwesomeIcon
icon={faArrowRightLong}
className={`
my-auto px-2 text-right text-gray-800 transition-transform
dark:text-olympus-50
group-hover:translate-x-2
`}
/>
</div>
</div>
</div>
</Menu>
);

View File

@@ -304,7 +304,6 @@ export function UnitControlMenu(props: { open: boolean; onClose: () => void }) {
try {
customStringJson = JSON.parse(customString);
} catch (e) {
console.error("Invalid JSON string:", customString);
}
// Used to show custom strings as json, recusively returns divs for arrays

File diff suppressed because it is too large Load Diff

View File

@@ -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";
@@ -31,6 +31,8 @@ import { ImportExportModal } from "./modals/importexportmodal";
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);
@@ -74,6 +76,8 @@ export function UI() {
<WarningModal open={appState === OlympusState.WARNING} />
<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} />
</>
)}

View File

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

View File

@@ -35,7 +35,7 @@ mist = {}
-- don't change these
mist.majorVersion = 4
mist.minorVersion = 5
mist.build = 122
mist.build = 125
-- forward declaration of log shorthand
local log
@@ -695,7 +695,6 @@ do -- the main scope
["FARP"] = "farps",
["Fueltank"] = "fueltank_cargo",
["Gate"] = "gate",
["FARP Fuel Depot"] = "gsm rus",
["Armed house"] = "home1_a",
["FARP Command Post"] = "kp-ug",
["Watch Tower Armed"] = "ohr-vyshka",
@@ -704,7 +703,6 @@ do -- the main scope
["Pipes big"] = "pipes_big_cargo",
["Oil platform"] = "plavbaza",
["Tetrapod"] = "tetrapod_cargo",
["Fuel tank"] = "toplivo",
["Trunks long"] = "trunks_long_cargo",
["Trunks small"] = "trunks_small_cargo",
["Passenger liner"] = "yastrebow",
@@ -1152,6 +1150,7 @@ do -- the main scope
end
end
end
--dbLog:warn(newTable)
--mist.debug.writeData(mist.utils.serialize,{'msg', newTable}, timer.getAbsTime() ..'Group.lua')
newTable.timeAdded = timer.getAbsTime() -- only on the dynGroupsAdded table. For other reference, see start time
--mist.debug.dumpDBs()
@@ -1493,7 +1492,7 @@ do -- the main scope
task.t = timer.getTime() + task.rep --schedule next run
local err, errmsg = pcall(task.f, unpack(task.vars, 1, table.maxn(task.vars)))
if not err then
log:error('Error in scheduled function: $1' .. errmsg)
log:error('Error in scheduled function: $1', errmsg)
end
--scheduledTasks[i].f(unpack(scheduledTasks[i].vars, 1, table.maxn(scheduledTasks[i].vars))) -- do the task
i = i + 1
@@ -1519,7 +1518,7 @@ do -- the main scope
id = tostring(original_id) .. ' #' .. tostring(id_ind)
id_ind = id_ind + 1
end
local valid
if mist.DBs.aliveUnits and mist.DBs.aliveUnits[val.object.id_] then
--log:info('object found in alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.aliveUnits[val.object.id_])
@@ -1532,6 +1531,7 @@ do -- the main scope
--trigger.action.outText('remove via death: ' .. Unit.getName(val.object),20)
mist.DBs.activeHumans[Unit.getName(val.object)] = nil
end]]
valid = true
elseif mist.DBs.removedAliveUnits and mist.DBs.removedAliveUnits[val.object.id_] then -- it didn't exist in alive_units, check old_alive_units
--log:info('object found in old_alive_units')
val.objectData = mist.utils.deepCopy(mist.DBs.removedAliveUnits[val.object.id_])
@@ -1540,32 +1540,37 @@ do -- the main scope
val.objectPos = pos.p
end
val.objectType = mist.DBs.removedAliveUnits[val.object.id_].category
valid = true
else --attempt to determine if static object...
--log:info('object not found in alive units or old alive units')
local pos = Object.getPosition(val.object)
if pos then
local static_found = false
for ind, static in pairs(mist.DBs.unitsByCat.static) do
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
--log:info('correlated dead static object to position')
val.objectData = static
val.objectPos = pos.p
val.objectType = 'static'
static_found = true
break
if Object.isExist(val.object) then
local pos = Object.getPosition(val.object)
if pos then
local static_found = false
for ind, static in pairs(mist.DBs.unitsByCat.static) do
if ((pos.p.x - static.point.x)^2 + (pos.p.z - static.point.y)^2)^0.5 < 0.1 then --really, it should be zero...
--log:info('correlated dead static object to position')
val.objectData = static
val.objectPos = pos.p
val.objectType = 'static'
static_found = true
break
end
end
if not static_found then
val.objectPos = pos.p
val.objectType = 'building'
val.typeName = Object.getTypeName(val.object)
end
else
val.objectType = 'unknown'
end
if not static_found then
val.objectPos = pos.p
val.objectType = 'building'
val.typeName = Object.getTypeName(val.object)
end
else
val.objectType = 'unknown'
valid = true
end
end
mist.DBs.deadObjects[id] = val
if valid then
mist.DBs.deadObjects[id] = val
end
end
end
end
@@ -2019,7 +2024,7 @@ do -- the main scope
end
end
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, 'newGroupPushedToAddGroup.lua')
--mist.debug.writeData(mist.utils.serialize,{'msg', newGroup}, newGroup.name ..'.lua')
--log:warn(newGroup)
-- sanitize table
newGroup.groupName = nil
@@ -3560,7 +3565,7 @@ function mist.getUnitsInMovingZones(unit_names, zone_unit_names, radius, zone_ty
end
function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
--log:info("$1, $2, $3, $4, $5", unitset1, altoffset1, unitset2, altoffset2, radius)
radius = radius or math.huge
local unit_info1 = {}
local unit_info2 = {}
@@ -3568,21 +3573,25 @@ function mist.getUnitsLOS(unitset1, altoffset1, unitset2, altoffset2, radius)
-- get the positions all in one step, saves execution time.
for unitset1_ind = 1, #unitset1 do
local unit1 = Unit.getByName(unitset1[unitset1_ind])
local lCat = Object.getCategory(unit1)
if unit1 and ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit:isExist() == true then
unit_info1[#unit_info1 + 1] = {}
unit_info1[#unit_info1].unit = unit1
unit_info1[#unit_info1].pos = unit1:getPosition().p
if unit1 then
local lCat = Object.getCategory(unit1)
if ((lCat == 1 and unit1:isActive()) or lCat ~= 1) and unit1:isExist() == true then
unit_info1[#unit_info1 + 1] = {}
unit_info1[#unit_info1].unit = unit1
unit_info1[#unit_info1].pos = unit1:getPosition().p
end
end
end
for unitset2_ind = 1, #unitset2 do
local unit2 = Unit.getByName(unitset2[unitset2_ind])
local lCat = Object.getCategory(unit2)
if unit2 and ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit:isExist() == true then
unit_info2[#unit_info2 + 1] = {}
unit_info2[#unit_info2].unit = unit2
unit_info2[#unit_info2].pos = unit2:getPosition().p
if unit2 then
local lCat = Object.getCategory(unit2)
if ((lCat == 1 and unit2:isActive()) or lCat ~= 1) and unit2:isExist() == true then
unit_info2[#unit_info2 + 1] = {}
unit_info2[#unit_info2].unit = unit2
unit_info2[#unit_info2].pos = unit2:getPosition().p
end
end
end
@@ -4012,13 +4021,14 @@ do -- group functions scope
if Group.getByName(gpName) and Group.getByName(gpName):isExist() == true then
local newGroup = Group.getByName(gpName)
local newData = {}
local newData = mist.utils.deepCopy(dbData)
newData.name = gpName
newData.groupId = tonumber(newGroup:getID())
newData.category = newGroup:getCategory()
newData.groupName = gpName
newData.hidden = dbData.hidden
if newData.category == 2 then
newData.category = 'vehicle'
elseif newData.category == 3 then
@@ -5193,7 +5203,8 @@ do -- mist.util scope
function mist.utils.getHeadingPoints(point1, point2, north) -- sick of writing this out.
if north then
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)), (mist.utils.makeVec3(point1)))
local p1 = mist.utils.get3DDist(point1)
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), p1), p1)
else
return mist.utils.getDir(mist.vec.sub(mist.utils.makeVec3(point2), mist.utils.makeVec3(point1)))
end
@@ -5837,8 +5848,8 @@ do -- mist.debug scope
log:alert('insufficient libraries to run mist.debug.dump_G, you must disable the sanitization of the io and lfs libraries in ./Scripts/MissionScripting.lua')
--trigger.action.outText(errmsg, 10)
end
end
end
--- Write debug data to file.
-- This function requires you to disable script sanitization
-- in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io
@@ -7653,7 +7664,10 @@ do
--log:warn(s)
if type(s) == 'table' then
local mType = s.markType
if mType == 'panel' then
--log:echo(s)
if mType == 'panel' then
local markScope = s.markScope or "all"
if markScope == 'coa' then
trigger.action.markToCoalition(s.markId, s.text, s.pos, s.markFor, s.readOnly)
elseif markScope == 'group' then
@@ -7711,10 +7725,15 @@ do
local function validateColor(val)
if type(val) == 'table' then
for i = 1, #val do
if type(val[i]) == 'number' and val[i] > 1 then
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent.
end
for i = 1, 4 do
if val[i] then
if type(val[i]) == 'number' and val[i] > 1 then
val[i] = val[i]/255 -- convert RGB values from 0-255 to 0-1 equivilent.
end
else
val[i] = 0.8
log:warn("index $1 of color to mist.marker.add was missing, defaulted to 0.8", i)
end
end
elseif type(val) == 'string' then
val = mist.utils.hexToRGB(val)
@@ -7755,7 +7774,7 @@ do
--log:info('create maker DB: $1', e.idx)
mist.DBs.markList[e.idx] = {time = e.time, pos = e.pos, groupId = e.groupId, mType = 'panel', text = e.text, markId = e.idx, coalition = e.coalition}
if e.unit then
mist.DBs.markList[e.idx].unit = e.intiator:getName()
mist.DBs.markList[e.idx].unit = e.initiator:getName()
end
--log:info(mist.marker.list[e.idx])
end
@@ -7778,7 +7797,7 @@ do
else
for mEntry, mData in pairs(mist.DBs.markList) do
if id == mData.name or id == mData.id then
return mData.id
return mData.markId
end
end
end
@@ -7788,11 +7807,16 @@ do
local function removeMark(id)
--log:info("Removing Mark: $1", id
--log:info("Removing Mark: $1", id)
local removed = false
if type(id) == 'table' then
for ind, val in pairs(id) do
local r = getMarkId(val)
local r
if val.markId then
r = val.markId
else
r = getMarkId(val)
end
if r then
trigger.action.removeMark(r)
mist.DBs.markList[r] = nil
@@ -7802,9 +7826,11 @@ do
else
local r = getMarkId(id)
trigger.action.removeMark(r)
mist.DBs.markList[r] = nil
removed = true
if r then
trigger.action.removeMark(r)
mist.DBs.markList[r] = nil
removed = true
end
end
return removed
end
@@ -7926,6 +7952,7 @@ do
if markForCoa then
if type(markForCoa) == 'string' then
--log:warn("coa is string")
if tonumber(markForCoa) then
coa = coas[tonumber(markForCoa)]
markScope = 'coa'
@@ -7940,11 +7967,10 @@ do
end
elseif type(markForCoa) == 'number' and markForCoa >=-1 and markForCoa <= #coas then
coa = markForCoa
markScore = 'coa'
--log:warn("coa is number")
markScope = 'coa'
end
markFor = coa
elseif markFor then
if type(markFor) == 'number' then -- groupId
if mist.DBs.groupsById[markFor] then
@@ -8053,7 +8079,7 @@ do
end
for i = 1, #markForTable do
local newId = iterate()
local data = {markId = newId, text = text, pos = pos[i], markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}
local data = {markId = newId, text = text, pos = pos[i], markScope = markScope, markFor = markForTable[i], markType = 'panel', name = name, readOnly = readOnly, time = timer.getTime()}
mist.DBs.markList[newId] = data
table.insert(list, data)
@@ -8177,6 +8203,7 @@ do
end
function mist.marker.remove(id)
return removeMark(id)
end
@@ -8967,8 +8994,8 @@ do -- group tasks scope
minR = mist.utils.get2DDist(avg, zone[i])
end
end
--log:warn('Radius: $1', radius)
--log:warn('minR: $1', minR)
--log:warn('Radius: $1', radius)
local lSpawnPos = {}
for j = 1, 100 do
newCoord = mist.getRandPointInCircle(avg, radius)
@@ -9200,7 +9227,7 @@ do -- group tasks scope
function mist.groupIsDead(groupName) -- copy more or less from on station
local gp = Group.getByName(groupName)
if gp then
if #gp:getUnits() > 0 or gp:isExist() == true then
if #gp:getUnits() > 0 and gp:isExist() == true then
return false
end
end

View File

@@ -68,7 +68,7 @@
"args": {
"key": "folder",
"description": "DCS folder location",
"default": "E:\\Eagle Dynamics\\DCS World (Open Beta)"
"default": "C:\\Program Files\\Eagle Dynamics\\DCS World"
}
}
]

View File

@@ -4,8 +4,6 @@ import inspect
import difflib
from slpp import slpp as lua
SEARCH_FOLDER = sys.argv[2]
from dcs.weapons_data import Weapons
from dcs.planes import *
from dcs.helicopters import *
@@ -114,14 +112,14 @@ if len(sys.argv) > 1:
database = json.load(f)
# Loads the loadout names
with open('../unitPayloads.lua') as f:
with open('unitPayloads.lua') as f:
lines = f.readlines()
unit_payloads = lua.decode("".join(lines).replace("Olympus.unitPayloads = ", "").replace("\n", ""))
# Loads the loadout roles
with open('payloadRoles.json') as f:
payloads_roles = json.load(f)
# Loop on all the units in the database
for unit_name in database:
try:
@@ -130,10 +128,6 @@ if len(sys.argv) > 1:
unitmap = plane_map
elif (sys.argv[1] == "helicopter"):
unitmap = helicopter_map
elif (sys.argv[1] == "groundunit"):
unitmap = vehicle_map
elif (sys.argv[1] == "navyunit"):
unitmap = ship_map
lowercase_keys = [key.lower() for key in unitmap.keys()]
res = difflib.get_close_matches(unit_name.lower(), lowercase_keys)
if len(res) > 0:
@@ -153,6 +147,26 @@ if len(sys.argv) > 1:
"roles": ["No task", rename_task(cls.task_default.name)]
}
database[unit_name]["loadouts"].append(empty_loadout)
pylon_usage = {}
for pylon_name in cls.pylons:
pylon_usage[pylon_name] = []
# The pylon data is expressed as a class named PylonX, where X is the pylon_name
pylon_cls_name = f'Pylon{pylon_name}'
if hasattr(cls, pylon_cls_name):
pylon_cls = getattr(cls, pylon_cls_name)
# The pylon class has as many attributes as there are possible weapons for that pylon
for attr_name in dir(pylon_cls):
if not attr_name.startswith('__') and not callable(getattr(pylon_cls, attr_name)):
weapon_data = getattr(pylon_cls, attr_name)
if isinstance(weapon_data[1], dict) and "clsid" in weapon_data[1]:
pylon_usage[pylon_name].append(weapon_data[1])
# Add the available pylon usage
database[unit_name]["acceptedPayloads"] = {}
for pylon_name in pylon_usage:
pylon_data = pylon_usage[pylon_name]
database[unit_name]["acceptedPayloads"][pylon_name] = pylon_usage[pylon_name]
# Loop on all the loadouts for that unit
for payload_name in unit_payloads[unit_name]:

View File

@@ -47,8 +47,10 @@ if len(sys.argv) > 1:
print(f"Warning, could not find {unit_name} in classes list. Skipping...")
continue
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
if not "acquisitionRange" in database[unit_name]:
database[unit_name]["acquisitionRange"] = unitmap[found_name].detection_range
if not "engagementRange" in database[unit_name]:
database[unit_name]["engagementRange"] = unitmap[found_name].threat_range
except Exception as e:
print(f"Could not find data for unitof type {unit_name}: {e}, skipping...")

View File

@@ -0,0 +1,144 @@
"""Add a "type" entry to each loadout item in an aircraft/helicopter database JSON.
Usage: run from repository root (or adjust paths) using python.
Creates a backup of the file before overwriting.
"""
import json
from pathlib import Path
import shutil
import re
# Paths to database files to process (aircraft + helicopter)
DB_PATHS = [
Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\mock-dcs\Mods\Services\Olympus\databases\units\aircraftdatabase.json"),
Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\mock-dcs\Mods\Services\Olympus\databases\units\helicopterdatabase.json"),
]
BACKUP_SUFFIX = ".bak"
MAP_PATH = Path(r"c:\Users\dpass\Documents\GitHub\DCSOlympus\scripts\python\loadout_type_map.json")
# Simple keyword -> type mapping. Order matters: first match wins.
TYPE_MAP = [
# Expanded A/A missiles (common and many variants)
(r"\bAIM-?\d+\b|Sidewinder|AIM-9|R-73|R-27|R-60|R-77|R-24R|R-24T|R-33|R-40RD|R-40TD|R530|R530F|SD-10A|SD-10|MICA|Matra|Magic|Super\s*530|PL-5|PL-8|PL-5E|PL-8B|R550|S530D|S530F|RVV|AA-", "A/A missile"),
# A/G / anti-ship missiles
(r"\bAGM-?\d+\b|Maverick|ASM|Anti-ship|Harpoon|Kh-35|Kh-31|Exocet|AGM-65|AGM-84|Kh-22|Kh-25MPU|Kh-58U|Kh-66|YJ-12|YJ-83|YJ-83K|C-802|CM802|KD-20|KD-63|KG-600", "A/G missile"),
# Guided bombs / glide weapons
(r"\bGBU-?\d+\b|JDAM|Laser Guided Bomb|GBU|JSOW|LS-6|MPRL|BROACH", "Guided bomb"),
# General purpose bombs and mk-series
(r"\bMk-?\d+\b|\bFAB-?\d+\b|500lb|2000lb|GP Bomb|GP Bombs|Bomb|MC Mk|S.A.P\.|GP Mk", "General purpose bomb"),
# Cluster bombs
(r"\bCBU-?\d+\b|Cluster Bomb|BLU-|SFW|CEM", "Cluster bomb"),
# Practice / training munitions
(r"\bBDU-?\w*\b|Practice Bomb|Captive Trg|CAP-?\d+|CATM-?\b", "Practice/Training munition"),
# Unguided rockets and rocket pods
(r"\bLAU-?\d+\b|Hydra|Hydra 70|70 mm|M156|M151|MK151|APKWS|S-5M|S-8|S-13|S-25|UB-16|UB-32|RP-3|B-13L|ORO-57K|R-?|RP-3|R-?P-?3", "Unguided rocket"),
# Targeting pods and cameras
(r"\bAN/AAQ-?\d+\b|AN/ASQ-?\d+|Laser Spot Tracker|LST/SCAM|Targeting Pod|LITENING|TGP|TGM-?\d+|LANTIRN|FLIR|Pod", "Targeting pod"),
# ECM and jammer pods
(r"\bALQ-?\d+\b|ECM Pod|ECM|Jammer|U22/A|U22A", "ECM pod"),
# Flares and dispensers (chaff/flares/countermeasures)
(r"\bALE-?40\b|BOZ-107|Dispenser|Disperser|Countermeasure Dispenser|BOZ|ALE-40|SUU-?\d+|flares|Flare|LUU-2|Flare|Dispenser\(Empty\)", "Flares/Dispensers"),
# Training rounds / captive
(r"\bCATM|CAP-?9|TGM-?\d+|CATM", "Training/trg round"),
(r"\bTGM-?\d+|TGM|CATM", "Training/trg round"),
# Fuel tanks (various naming conventions)
(r"\bFuel Tank\b|Fuel tank|Drop Tank|External[- ]?tank|Auxiliary Drop Tank|Sargent Fletcher Fuel Tank|RP35 Pylon Fuel Tank|RPL \d+|Cylindrical Tip Tank|Elliptic Tip Tank|\b\d+\s*(?:gal|gallons|liters|litres|L|lt)\b|1150L|1400L|2000L|3000L", "Fuel tank"),
# Practice of captive or other small categories
(r"\bMk-82 AIR Ballute|Ballute", "General purpose bomb"),
# Misc / smoke / oil tanks / containers
(r"\bSmoke\b|Smoke Generator|Smoke System|White Smoke|red colorant|yellow colorant|Color Oil Tank|White Oil Tank", "Misc"),
# Pylons, containers and luggage
(r"\bPYLON|Pylon|MPS-410|CLB4-PYLON|Luggage Container|Container", "Pylon"),
# Guns and cannon mounts
(r"\bDEFA-553|Browning|7.62mm|12.7mm|GPMG|Gun|Cannon", "Gun"),
# Fallback guided bomb entries covered specifically
(r"\bGBU-12|GBU-10|GBU-31|GBU-38", "Guided bomb"),
]
# Default type when no pattern matches
DEFAULT_TYPE = "unknown"
def detect_type(item_name: str) -> str:
name = item_name or ""
# normalize
s = name
# 1) try mapping file exact match
if MAP_PATH.exists():
try:
with MAP_PATH.open('r', encoding='utf-8') as mf:
mapping = json.load(mf)
except Exception:
mapping = {}
# exact name match (case-sensitive), then case-insensitive key match
if name in mapping and mapping[name]:
return mapping[name]
# case-insensitive exact
lower_map = {k.lower(): v for k, v in mapping.items() if v}
if name.lower() in lower_map:
return lower_map[name.lower()]
# substring mapping: if a mapping key is contained in the name, use it
for k, v in mapping.items():
if not v:
continue
if k.lower() in name.lower():
return v
for pattern, t in TYPE_MAP:
if re.search(pattern, s, re.IGNORECASE):
return t
return DEFAULT_TYPE
def process_db(db_path: Path):
if not db_path.exists():
print(f"Database file not found: {db_path}")
return
backup_path = db_path.with_suffix(db_path.suffix + BACKUP_SUFFIX)
shutil.copy2(db_path, backup_path)
print(f"Created backup: {backup_path}")
with db_path.open("r", encoding="utf-8") as f:
data = json.load(f)
total_items = 0
updated_items = 0
type_counts = {}
# data is a dict of vehicles (aircraft or helicopter)
for ac_name, ac in data.items():
loadouts = ac.get("loadouts")
if not isinstance(loadouts, list):
continue
for loadout in loadouts:
items = loadout.get("items")
if not isinstance(items, list):
continue
for item in items:
total_items += 1
name = item.get("name", "")
t = detect_type(name)
prev = item.get("type")
if prev != t:
item["type"] = t
updated_items += 1
type_counts[t] = type_counts.get(t, 0) + 1
# write back
with db_path.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
print(f"Processed {total_items} loadout items, updated {updated_items} entries for {db_path.name}.")
print("Type counts:")
for k, v in sorted(type_counts.items(), key=lambda x: -x[1]):
print(f" {k}: {v}")
def main():
for p in DB_PATHS:
process_db(p)
if __name__ == '__main__':
main()

View File

@@ -92,7 +92,9 @@ for filename in filenames:
src = tmp['payloads'].values()
else:
src = tmp['payloads']
print(f"Processing {filename} with {len(src)} payloads, detected unit name {tmp['unitType']}")
names[tmp['unitType']] = []
roles[tmp['unitType']] = {}
payloads[tmp['unitType']] = {}
@@ -129,9 +131,22 @@ for filename in filenames:
with open('payloadRoles.json', 'w') as f:
json.dump(roles, f, ensure_ascii = False, indent = 2)
with open('../unitPayloads.lua', 'w') as f:
with open('unitPayloads.lua', 'w') as f:
f.write("Olympus.unitPayloads = " + dump_lua(payloads))
# Iterate over the payloads and accumulate the pylon data
pylon_usage = {}
for unitType, unitPayloads in payloads.items():
pylon_usage[unitType] = {}
for payloadName, pylons in unitPayloads.items():
for pylonID, pylonData in pylons.items():
# Keep track of what CLSIDs are used on each pylon
clsid = pylonData['CLSID']
if pylonID not in pylon_usage[unitType]:
pylon_usage[unitType][pylonID] = []
if clsid not in pylon_usage[unitType][pylonID]:
pylon_usage[unitType][pylonID].append(clsid)
# Save the pylon usage data to a JSON file
with open('pylonUsage.json', 'w') as f:
json.dump(pylon_usage, f, ensure_ascii=False, indent=2)

View File

@@ -0,0 +1,65 @@
{
"2 x FAB-250": "General purpose bomb",
"2 x FAB-500": "General purpose bomb",
"6 x AGM-86D on MER": "A/G missile",
"8 x AGM-86D": "A/G missile",
"<CLEAN>": "Empty",
"AGM-114K * 2": "A/G missile",
"AGM-114K Hellfire": "A/G missile",
"AGM-154C - JSOW Unitary BROACH": "Guided bomb",
"AGM-88C HARM - High Speed Anti-Radiation Missile": "A/G missile",
"AIM-120B AMRAAM - Active Radar AAM": "A/A missile",
"AIM-120C AMRAAM - Active Radar AAM": "A/A missile",
"AIM-7E Sparrow Semi-Active Radar": "A/A missile",
"AIM-7E-2 Sparrow Semi-Active Radar": "A/A missile",
"AIM-7F": "A/A missile",
"AIM-7M": "A/A missile",
"AIM-7M Sparrow Semi-Active Radar": "A/A missile",
"AIM-7MH Sparrow Semi-Active Radar": "A/A missile",
"AKD-10": "A/G missile",
"ALARM": "A/G missile",
"AN-M3 - 2*Browning Machine Guns 12.7mm": "Gun",
"AN/ASQ-213 HTS - HARM Targeting System": "Targeting pod",
"APU-13U-2 with R-3R (AA-2 Atoll-C) - Semi Active AAM": "A/A missile",
"APU-13U-2 with R-3S (AA-2 Atoll-B) - IR AAM": "A/A missile",
"BAP-100 x 18": "Unguided rocket",
"BLG-66-AC Belouga": "Cluster bomb",
"C-701IR": "A/G missile",
"C-701T": "A/G missile",
"C-802AK": "A/G missile",
"CLB4-PYLON-SAMP250HD": "Pylon",
"CLB4-PYLON-SAMP400LD": "Pylon",
"CM802AKG (DIS)": "A/G missile",
"DEFA-553 - 30mm Revolver Cannon": "Gun",
"ETHER": "Misc",
"FAB-100SV": "General purpose bomb",
"GB-6": "General purpose bomb",
"GB-6-HE": "General purpose bomb",
"K-13A": "A/A missile",
"KD-20": "A/G missile",
"KD-63": "A/G missile",
"KG-600": "A/G missile",
"Kh-22 (AS-4 Kitchen) - 1000kg, AShM, IN & Act/Pas Rdr": "A/G missile",
"Kh-25MPU (Updated AS-12 Kegler) - 320kg, ARM, IN & Pas Rdr": "A/G missile",
"Kh-58U (AS-11 Kilter) - 640kg, ARM, IN & Pas Rdr": "A/G missile",
"Kh-66 Grom (21) - AGM, radar guided APU-68": "A/G missile",
"LD-10": "Fuel tank",
"LD-10 x 2": "Fuel tank",
"LS-6-100 Dual": "Guided bomb",
"LS-6-250 Dual": "Guided bomb",
"LS-6-500": "Guided bomb",
"Luggage Container": "Container",
"MPRL - 4 x AGM-154C - JSOW Unitary BROACH": "Guided bomb",
"MPS-410": "Pylon",
"Mk-82AIR": "General purpose bomb",
"PK-3 - 7.62mm GPMG": "Gun",
"PL-5EII": "A/A missile",
"PL-8B": "A/A missile",
"R550 Magic 1 IR AAM": "A/A missile",
"R550 Magic 2 IR AAM": "A/A missile",
"S530D": "A/A missile",
"S530F": "A/A missile",
"TYPE-200A": "Flares/Dispensers",
"TYPE-200A Dual": "Flares/Dispensers",
"{6C0D552F-570B-42ff-9F6D-F10D9C1D4E1C}": "Misc"
}

View File

@@ -3773,6 +3773,76 @@
"1": 29
}
},
"F4U-1D": {
"Drop tank 175 US gal.": {
"1": 11,
"2": 18,
"3": 19,
"4": 17
},
"HVAR x 8": {
"1": 31
},
"M-64 bomb x 2, HVAR x 8": {
"1": 31
},
"M-64 bomb x 3, HVAR x 6": {
"1": 31
},
"M-64 bomb x 2, HVAR x 4": {
"1": 32
},
"M-64 bomb, M-65 bomb x 2": {
"1": 32
},
"Tiny Tim x 2, HVAR x 4": {
"1": 32
},
"Bat Bomb": {
"1": 30
},
"Tiny Tim x 2": {
"1": 30
},
"Bat Bomb, HVAR x 8": {
"1": 30
}
},
"F4U-1D_CW": {
"Drop tank 175 US gal.": {
"1": 11,
"2": 18,
"3": 19,
"4": 17
},
"HVAR x 8": {
"1": 31
},
"M-64 bomb x 2, HVAR x 8": {
"1": 31
},
"M-64 bomb x 3, HVAR x 6": {
"1": 31
},
"M-64 bomb x 2, HVAR x 4": {
"1": 32
},
"M-64 bomb, M-65 bomb x 2": {
"1": 32
},
"Tiny Tim x 2, HVAR x 4": {
"1": 32
},
"Bat Bomb": {
"1": 30
},
"Tiny Tim x 2": {
"1": 30
},
"Bat Bomb, HVAR x 8": {
"1": 30
}
},
"F/A-18A": {
"GBU-16*2,AIM-9*2,AIM-7,FLIR Pod,Fuel*3": {
"1": 33
@@ -4725,7 +4795,219 @@
},
"AEROBATIC": {}
},
"MiG-29 Fulcrum": {
"2 * R-27T, 4 * R-73": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27R, 4 * R-73": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27T, 2 * R-73": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27R, 2 * R-73": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"6 * R-73": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27T, 4 * R-60M": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27R, 4 * R-60M": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27T, 2 * R-60M": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27R, 2 * R-60M": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"6 * R-60": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"4 * S-24, 2 * R-73": {
"1": 34,
"2": 30,
"3": 31,
"4": 32
},
"4 * S-24, 2 * R-60M ": {
"1": 34,
"2": 30,
"3": 31,
"4": 32
},
"4 * S-24": {
"1": 34,
"2": 30,
"3": 31,
"4": 32
},
"80 * S-8OFP": {
"1": 31,
"2": 32
},
"80 * S-8OFP, 2 * R-60M": {
"1": 31,
"2": 32
},
"80 * S-8KOM, 2 * R-73": {
"1": 31,
"2": 32
},
"4 * BetAB-500, 2 * R-60": {
"1": 34,
"2": 32
},
"4 * BetAB-500, 2 * R-73": {
"1": 34,
"2": 32
},
"4 * BetAB-500": {
"1": 34,
"2": 32
},
"4 * KMGU": {
"1": 31
},
"4 * KMGU AO-2.5RT, 2 * R-60M": {
"1": 31
},
"4 * KMGU PTAB-2.5KO, 2 * R-73": {
"1": 31
},
"2 * R-27T": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"2 * R-27R": {
"1": 11,
"2": 18,
"3": 19,
"4": 10
},
"4 * RBK-250 PTAB-2.5M, 2 * R-73": {
"1": 31
},
"4 * RBK-250 AO-1SCh, 2 * R-73": {
"1": 31
},
"4 * RBK-500 PTAB-1M, 2 * R-73": {
"1": 31
},
"4 * RBK-500 PTAB-10, 2 * R-73": {
"1": 31
},
"4 * FAB-250M-62, 2 * R-73": {
"1": 31,
"2": 32
},
"4 * FAB-500M-62, 2 * R-73": {
"1": 31,
"2": 32,
"3": 30
},
"80 * S-8OFP, 2 * R-73": {
"1": 31
},
"4 * BetAB-500ShP, 2 * R-73": {
"1": 34
},
"80 * S-8TsM, 2 * R-73": {
"1": 16
},
"6 * R-60M": {
"1": 19
},
"2 * R-27ER, 4 * R-73": {
"1": 19,
"2": 11,
"3": 18,
"4": 10
},
"2 * R-27ET, 4 * R-73": {
"1": 19,
"2": 11,
"3": 18,
"4": 10
},
"2 * R-27ER, 4 * R-60M": {
"1": 19,
"2": 11,
"3": 18,
"4": 10
},
"2 * R-27ER": {
"1": 19,
"2": 11,
"3": 18,
"4": 10
},
"2 * R-27ET": {
"1": 19,
"2": 11,
"3": 18,
"4": 10
},
"2 * R-27ER, 4 * R-73, Fuel": {
"1": 18
},
"2 * R-27ET, 4 * R-73, fuel": {
"1": 18
},
"6 * R-73, Fuel": {
"1": 18
},
"6 * R-60M, Fuel": {
"1": 18
},
"2 * R-73, Fuel": {
"1": 18
}
},
"Mirage-F1B": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4781,6 +5063,15 @@
}
},
"Mirage-F1BD": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4830,6 +5121,15 @@
}
},
"Mirage-F1BE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4909,6 +5209,15 @@
}
},
"Mirage-F1BQ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -4958,6 +5267,15 @@
}
},
"Mirage-F1C-200": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5013,6 +5331,15 @@
}
},
"Mirage-F1C": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5068,6 +5395,15 @@
}
},
"Mirage-F1CE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5147,6 +5483,15 @@
}
},
"Mirage-F1CG": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM-9 JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5208,6 +5553,15 @@
}
},
"Mirage-F1CH": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5263,6 +5617,15 @@
}
},
"Mirage-F1CJ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5318,6 +5681,15 @@
}
},
"Mirage-F1CK": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5373,6 +5745,15 @@
}
},
"Mirage-F1CR": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I": {
"1": 10
},
@@ -5410,6 +5791,15 @@
}
},
"Mirage-F1CT": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5459,6 +5849,15 @@
}
},
"Mirage-F1CZ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5514,6 +5913,15 @@
}
},
"Mirage-F1DDA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5563,6 +5971,15 @@
}
},
"Mirage-F1ED": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic II, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5612,6 +6029,15 @@
}
},
"Mirage-F1EDA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5661,6 +6087,15 @@
}
},
"Mirage-F1EE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5746,6 +6181,15 @@
}
},
"Mirage-F1EH": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5801,6 +6245,15 @@
}
},
"Mirage-F1EQ": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*S530F, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5850,6 +6303,15 @@
}
},
"Mirage-F1JA": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*R550 Magic I, 2*Python III, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5896,6 +6358,15 @@
}
},
"Mirage-F1M-CE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -5969,6 +6440,15 @@
}
},
"Mirage-F1M-EE": {
"Clean": {
"1": 10,
"2": 11,
"3": 18,
"4": 19,
"5": 31,
"6": 32,
"7": 34
},
"2*AIM9-JULI, 2*R530IR, 1*Fuel Tank": {
"1": 10,
"2": 11,
@@ -6213,6 +6693,125 @@
"1": 18
}
},
"Mi-28N": {
"2xFAB-250": {
"1": 32
},
"4xFuel tank": {
"1": 15
},
"80xS-8": {
"1": 31,
"2": 32,
"3": 18
},
"4xKMGU AP": {
"1": 32
},
"4xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xKMGU AT": {
"1": 31,
"2": 32,
"3": 18
},
"4xFAB-500": {
"1": 32
},
"16x9M114, 2xFAB-500": {
"1": 32
},
"40xS-8": {
"1": 31,
"2": 32,
"3": 18
},
"40xS-8 TsM": {
"1": 16
},
"2xKMGU AP": {
"1": 32
},
"2xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"2xFAB-500": {
"1": 32
},
"16x9M114, 40xS-8": {
"1": 31,
"2": 32,
"3": 18,
"4": 30
},
"16x9M114": {
"1": 31,
"2": 32,
"3": 18
},
"20xS-13": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xKMGU AP": {
"1": 31,
"2": 32,
"3": 18
},
"4xFAB-250": {
"1": 32
},
"4xKMGU AT": {
"1": 32
},
"16x9M114, 40xS-8 TsM": {
"1": 16
},
"80xS-8 TsM": {
"1": 16
},
"2xKMGU AT": {
"1": 32
},
"9x9M114": {
"1": 31,
"2": 32,
"3": 18
},
"2xFuel tank": {
"1": 15
},
"10xS-13": {
"1": 31,
"2": 32,
"3": 18
},
"2xFAB-250, 16x9M114": {
"1": 32
},
"16x9M114, 10xS-13": {
"1": 31,
"2": 32,
"3": 18,
"4": 30
}
},
"Tu-95MS": {
"Kh-65*6": {
"1": 33
}
},
"B-1B": {
"Mk-82*84": {
"1": 34,
@@ -7401,120 +8000,6 @@
}
},
"Mi-26": {},
"Mi-28N": {
"2xFAB-250": {
"1": 32
},
"4xFuel tank": {
"1": 15
},
"80xS-8": {
"1": 31,
"2": 32,
"3": 18
},
"4xKMGU AP": {
"1": 32
},
"4xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xKMGU AT": {
"1": 31,
"2": 32,
"3": 18
},
"4xFAB-500": {
"1": 32
},
"16x9M114, 2xFAB-500": {
"1": 32
},
"40xS-8": {
"1": 31,
"2": 32,
"3": 18
},
"40xS-8 TsM": {
"1": 16
},
"2xKMGU AP": {
"1": 32
},
"2xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xUPK-23": {
"1": 31,
"2": 32,
"3": 18
},
"2xFAB-500": {
"1": 32
},
"16x9M114, 40xS-8": {
"1": 31,
"2": 32,
"3": 18,
"4": 30
},
"16x9M114": {
"1": 31,
"2": 32,
"3": 18
},
"20xS-13": {
"1": 31,
"2": 32,
"3": 18
},
"16x9M114, 2xKMGU AP": {
"1": 31,
"2": 32,
"3": 18
},
"4xFAB-250": {
"1": 32
},
"4xKMGU AT": {
"1": 32
},
"16x9M114, 40xS-8 TsM": {
"1": 16
},
"80xS-8 TsM": {
"1": 16
},
"2xKMGU AT": {
"1": 32
},
"9x9M114": {
"1": 31,
"2": 32,
"3": 18
},
"2xFuel tank": {
"1": 15
},
"10xS-13": {
"1": 31,
"2": 32,
"3": 18
},
"2xFAB-250, 16x9M114": {
"1": 32
},
"16x9M114, 10xS-13": {
"1": 31,
"2": 32,
"3": 18,
"4": 30
}
},
"Mi-8MT": {
"4 x B8": {
"1": 32
@@ -8145,7 +8630,7 @@
"UB-32*2,Fuel*3": {
"1": 32
},
"Kh-59M*2,R-60M*2,Fuel": {
"Kh-59M*2,R-60M*2": {
"1": 33
},
"S-25*4": {
@@ -8934,11 +9419,6 @@
"2": 34
}
},
"Tu-95MS": {
"Kh-65*6": {
"1": 33
}
},
"UH-1H": {
"M134 Minigun*2, XM158*2": {
"1": 32,

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
AB 250-2 - 144 x SD-2, 250kg CBU with HE submunitions