mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
First implementation of weapon wizard
This commit is contained in:
parent
504c0a0ed9
commit
057603f926
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -324,7 +324,7 @@ export interface UnitBlueprint {
|
||||
roles?: string[];
|
||||
type?: string;
|
||||
loadouts?: LoadoutBlueprint[];
|
||||
acceptedPayloads?: { [key: string]: { clsids: string[]; names: string[] } };
|
||||
acceptedPayloads?: { [key: string]: { clsids: string; name: string, weight: number }[] };
|
||||
filename?: string;
|
||||
liveries?: { [key: string]: { name: string; countries: string[] } };
|
||||
cost?: number;
|
||||
|
||||
@ -1,83 +1,247 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { OlDropdown, OlDropdownItem } from "../../components/oldropdown";
|
||||
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: string[];
|
||||
setSelectedWeapons: (weapons: string[]) => void;
|
||||
weaponsByType: { [type: string]: { name: string; available: boolean }[] };
|
||||
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 }[] };
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [selectedPylons, setSelectedPylons] = useState<string[]>([]);
|
||||
const [autofillPylons, setAutofillPylons] = useState(false);
|
||||
|
||||
// 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 }[] = [];
|
||||
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()));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.keys(props.weaponsByType).map((type, idx) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex w-full flex-col content-center gap-2
|
||||
`}
|
||||
key={idx}
|
||||
>
|
||||
<div className="flex justify-center">
|
||||
{Object.keys(props.weaponsByPylon).map((pylon) => (
|
||||
<div key={pylon} className={``}>
|
||||
<div
|
||||
className={`
|
||||
my-auto min-w-32 text-sm font-normal
|
||||
text-gray-400
|
||||
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]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
<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>
|
||||
<OlDropdown
|
||||
label={`${props.weaponsByType[type].filter((weapon) => props.selectedWeapons.includes(weapon.name)).length} weapons type selected, ${props.weaponsByType[type].filter((weapon) => weapon.available).length} available`}
|
||||
className={`w-full`}
|
||||
>
|
||||
<>
|
||||
{props.weaponsByType[type].map((weapon, weaponIdx) => {
|
||||
const isDisabled = !weapon.available && !props.selectedWeapons.includes(weapon.name);
|
||||
return (
|
||||
<OlDropdownItem
|
||||
onClick={() => {
|
||||
if (isDisabled) return;
|
||||
|
||||
if (props.selectedWeapons.includes(weapon.name))
|
||||
props.setSelectedWeapons(props.selectedWeapons.filter((w) => w !== weapon.name));
|
||||
else props.setSelectedWeapons([...props.selectedWeapons, weapon.name]);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<div className={`truncate`} key={weaponIdx}>
|
||||
{props.selectedWeapons.includes(weapon.name) ? "✓ " : ""}
|
||||
{weapon.name}
|
||||
</div>
|
||||
</OlDropdownItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</OlDropdown>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{props.selectedWeapons.length > 0 && (
|
||||
<div
|
||||
className={`
|
||||
my-auto mb-2 min-w-32 text-sm font-normal
|
||||
text-gray-400
|
||||
`}
|
||||
>
|
||||
Selected weapons
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
flex flex-col gap-2 rounded-lg bg-gray-800 p-4
|
||||
text-gray-200
|
||||
`}
|
||||
>
|
||||
{props.selectedWeapons.map((weapon, weaponIdx) => (
|
||||
<div key={weaponIdx} className={`truncate text-xs`}>
|
||||
{weapon}
|
||||
</div>
|
||||
))}
|
||||
{props.selectedWeapons.length === 0 && <div className={`
|
||||
text-sm
|
||||
`}>No weapons selected</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 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>
|
||||
|
||||
<OlSearchBar onChange={setSearchText} text={searchText} />
|
||||
|
||||
<div
|
||||
className={`
|
||||
flex max-h-48 flex-col overflow-y-auto border
|
||||
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>
|
||||
)
|
||||
) : (
|
||||
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 (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([]);
|
||||
}
|
||||
}}
|
||||
className={`
|
||||
cursor-pointer rounded-md p-1 text-sm
|
||||
hover:bg-gray-700
|
||||
`}
|
||||
>
|
||||
{weapon.name}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -59,7 +59,7 @@ export function UnitSpawnMenu(props: {
|
||||
const [spawnAltitudeType, setSpawnAltitudeType] = useState(false);
|
||||
const [spawnLiveryID, setSpawnLiveryID] = useState("");
|
||||
const [spawnSkill, setSpawnSkill] = useState("High");
|
||||
const [selectedWeapons, setSelectedWeapons] = useState([] as string[]);
|
||||
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);
|
||||
@ -76,6 +76,7 @@ export function UnitSpawnMenu(props: {
|
||||
setSpawnRole("");
|
||||
setSpawnLoadout(null);
|
||||
setSpawnLiveryID("");
|
||||
setSelectedWeapons({});
|
||||
}, [props.blueprint]);
|
||||
|
||||
/* When the menu is opened show the unit preview on the map as a cursor */
|
||||
@ -160,32 +161,6 @@ export function UnitSpawnMenu(props: {
|
||||
setCompassAngle(getApp()?.getMap().getSpawnHeading() ?? 0);
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedWeapons([]);
|
||||
|
||||
// Choose the first loadout that matches the selected role that is not empty
|
||||
const loadout = props.blueprint?.loadouts?.filter((loadout) => loadout.name !== "Empty loadout").find((loadout) => loadout.roles.includes(spawnRole));
|
||||
if (loadout) {
|
||||
setSpawnLoadout(loadout);
|
||||
//setSelectedWeapons(loadout.items.map((item) => item.name));
|
||||
}
|
||||
|
||||
// When the role changes, set the selected weapons to the weapons contained in the first loadout of that type
|
||||
// Commented out for usability tests
|
||||
//const loadout = loadouts.filter((loadout) => loadout.name !== "Empty loadout").find((loadout) => loadout.roles.includes(spawnRole));
|
||||
//if (loadout) {
|
||||
// setSelectedWeapons(loadout.items.map((item) => item.name));
|
||||
//}
|
||||
}, [spawnRole]);
|
||||
|
||||
useEffect(() => {
|
||||
// Select the first loadout that contains all the selected weapons
|
||||
if (selectedWeapons.length > 0) {
|
||||
const loadout = props.blueprint?.loadouts?.find((loadout) => selectedWeapons.every((weapon) => loadout.items.some((item) => item.name === weapon)));
|
||||
if (loadout) setSpawnLoadout(loadout);
|
||||
}
|
||||
}, [selectedWeapons]);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
@ -221,35 +196,6 @@ export function UnitSpawnMenu(props: {
|
||||
if (allRoles !== undefined) mainRole = mode(allRoles);
|
||||
spawnRole === "" && roles.length > 0 && setSpawnRole(mainRole);
|
||||
|
||||
/* Make a list of all the available weapons */
|
||||
let allWeapons: { [key: string]: string } = {};
|
||||
props.blueprint?.loadouts?.forEach((loadout) => {
|
||||
loadout.items.forEach((item) => {
|
||||
allWeapons[item.name] = item.type;
|
||||
});
|
||||
});
|
||||
|
||||
// List the loadouts that contain all the selected weapons
|
||||
let filteredLoadouts: LoadoutBlueprint[] = [];
|
||||
if (selectedWeapons.length > 0) {
|
||||
props.blueprint?.loadouts?.forEach((loadout) => {
|
||||
let loadoutWeaponNames = loadout.items.map((item) => item.name);
|
||||
if (selectedWeapons.every((weapon) => loadoutWeaponNames.includes(weapon))) {
|
||||
filteredLoadouts.push(loadout);
|
||||
}
|
||||
});
|
||||
} else filteredLoadouts = props.blueprint?.loadouts ?? [];
|
||||
|
||||
/* Organize the weapons by type */
|
||||
let weaponsByType: { [key: string]: { name: string; available: boolean }[] } = {};
|
||||
Object.keys(allWeapons).forEach((weaponName) => {
|
||||
// Set which weapons are available and which are not by checking if they are in any of the filtered loadouts
|
||||
const isAvailable = filteredLoadouts.some((loadout) => loadout.items.some((item) => item.name === weaponName));
|
||||
const weaponType = allWeapons[weaponName];
|
||||
if (weaponType in weaponsByType) weaponsByType[weaponType].push({ name: weaponName, available: isAvailable });
|
||||
else weaponsByType[weaponType] = [{ name: weaponName, available: isAvailable }];
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.compact ? (
|
||||
@ -702,7 +648,11 @@ export function UnitSpawnMenu(props: {
|
||||
open={openAccordion === OpenAccordion.LOADOUT}
|
||||
title="Loadout wizard"
|
||||
>
|
||||
<WeaponsWizard selectedWeapons={selectedWeapons} setSelectedWeapons={setSelectedWeapons} weaponsByType={weaponsByType} />
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={props.blueprint?.acceptedPayloads ?? {}}
|
||||
/>
|
||||
</OlAccordion>
|
||||
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
@ -773,7 +723,7 @@ export function UnitSpawnMenu(props: {
|
||||
{props.blueprint && <OlUnitSummary blueprint={props.blueprint} coalition={spawnCoalition} />}
|
||||
<div
|
||||
className={`
|
||||
flex h-fit flex-col gap-5 px-5 pb-8 pt-6
|
||||
flex h-fit flex-col gap-2 px-5 pb-8 pt-6
|
||||
`}
|
||||
>
|
||||
<div
|
||||
@ -1193,7 +1143,11 @@ export function UnitSpawnMenu(props: {
|
||||
open={openAccordion === OpenAccordion.LOADOUT}
|
||||
title="Loadout wizard"
|
||||
>
|
||||
<WeaponsWizard selectedWeapons={selectedWeapons} setSelectedWeapons={setSelectedWeapons} weaponsByType={weaponsByType} />
|
||||
<WeaponsWizard
|
||||
selectedWeapons={selectedWeapons}
|
||||
setSelectedWeapons={setSelectedWeapons}
|
||||
weaponsByPylon={props.blueprint?.acceptedPayloads ?? {}}
|
||||
/>
|
||||
</OlAccordion>
|
||||
{spawnLoadout && spawnLoadout.items.length > 0 && (
|
||||
<OlAccordion
|
||||
|
||||
@ -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 *
|
||||
@ -121,10 +119,7 @@ if len(sys.argv) > 1:
|
||||
# Loads the loadout roles
|
||||
with open('payloadRoles.json') as f:
|
||||
payloads_roles = json.load(f)
|
||||
|
||||
with open('pylonUsage.json') as f:
|
||||
pylon_usage = json.load(f)
|
||||
|
||||
|
||||
# Loop on all the units in the database
|
||||
for unit_name in database:
|
||||
try:
|
||||
@ -133,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:
|
||||
@ -156,15 +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[unit_name]:
|
||||
pylon_data = pylon_usage[unit_name][pylon_name]
|
||||
database[unit_name]["acceptedPayloads"][pylon_name] = {
|
||||
"clsids": pylon_data,
|
||||
"names": [find_weapon_name(clsid) for clsid in pylon_data]
|
||||
}
|
||||
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]:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user