First implementation of weapon wizard

This commit is contained in:
Pax1601 2025-10-23 18:07:55 +02:00
parent 504c0a0ed9
commit 057603f926
7 changed files with 147146 additions and 81486 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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

View File

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

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 *
@ -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]: