Lemontropia-Suite/ui/loadout_manager.py

795 lines
31 KiB
Python

"""
Lemontropia Suite - Loadout Manager UI
A PyQt6 dialog for configuring hunting gear loadouts with cost calculations.
"""
import json
import os
from dataclasses import dataclass, asdict
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Optional, List
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
QLineEdit, QComboBox, QLabel, QPushButton,
QGroupBox, QSpinBox, QDoubleSpinBox, QMessageBox,
QListWidget, QListWidgetItem, QSplitter, QWidget,
QFrame, QScrollArea, QGridLayout
)
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont
# ============================================================================
# Data Structures
# ============================================================================
@dataclass
class LoadoutConfig:
"""Configuration for a hunting loadout."""
name: str
weapon_name: str
weapon_damage: Decimal
weapon_decay_pec: Decimal
weapon_ammo_pec: Decimal
armor_name: str
armor_decay_pec: Decimal
heal_cost_pec: Decimal
# Optional fields for extended calculations
shots_per_hour: int = 3600 # Default: 1 shot per second
protection_stab: Decimal = Decimal("0")
protection_cut: Decimal = Decimal("0")
protection_impact: Decimal = Decimal("0")
protection_penetration: Decimal = Decimal("0")
protection_shrapnel: Decimal = Decimal("0")
protection_burn: Decimal = Decimal("0")
protection_cold: Decimal = Decimal("0")
protection_acid: Decimal = Decimal("0")
protection_electric: Decimal = Decimal("0")
def calculate_dpp(self) -> Decimal:
"""Calculate Damage Per Pec (DPP) = Damage / (Decay + Ammo)."""
total_cost = self.weapon_decay_pec + self.weapon_ammo_pec
if total_cost == 0:
return Decimal("0")
return self.weapon_damage / total_cost
def calculate_cost_per_hour(self) -> Decimal:
"""Calculate total PED cost per hour based on shots per hour."""
cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec
total_weapon_cost = cost_per_shot * Decimal(self.shots_per_hour)
# Estimate armor decay (assume 1 hit per 5 shots on average)
armor_cost_per_hour = self.armor_decay_pec * Decimal(self.shots_per_hour // 5)
# Estimate healing cost (assume 1 heal per 10 shots)
heal_cost_per_hour = self.heal_cost_pec * Decimal(self.shots_per_hour // 10)
total_pec = total_weapon_cost + armor_cost_per_hour + heal_cost_per_hour
return total_pec / Decimal("100") # Convert PEC to PED
def calculate_break_even(self, mob_health: Decimal) -> Decimal:
"""Calculate break-even loot value for a mob with given health."""
shots_to_kill = mob_health / self.weapon_damage
if shots_to_kill < 1:
shots_to_kill = Decimal("1")
cost_per_shot = self.weapon_decay_pec + self.weapon_ammo_pec
total_cost_pec = shots_to_kill * cost_per_shot
return total_cost_pec / Decimal("100") # Convert to PED
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
k: str(v) if isinstance(v, Decimal) else v
for k, v in asdict(self).items()
}
@classmethod
def from_dict(cls, data: dict) -> "LoadoutConfig":
"""Create LoadoutConfig from dictionary."""
decimal_fields = [
'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec',
'armor_decay_pec', 'heal_cost_pec', 'protection_stab',
'protection_cut', 'protection_impact', 'protection_penetration',
'protection_shrapnel', 'protection_burn', 'protection_cold',
'protection_acid', 'protection_electric'
]
for field in decimal_fields:
if field in data:
data[field] = Decimal(data[field])
if 'shots_per_hour' in data:
data['shots_per_hour'] = int(data['shots_per_hour'])
return cls(**data)
# ============================================================================
# Mock Data
# ============================================================================
MOCK_WEAPONS = [
{"name": "Sollomate Opalo", "damage": Decimal("9.0"), "decay": Decimal("0.09"), "ammo": Decimal("1.80")},
{"name": "Omegaton M2100", "damage": Decimal("10.5"), "decay": Decimal("0.10"), "ammo": Decimal("2.10")},
{"name": "Breer M1a", "damage": Decimal("12.0"), "decay": Decimal("0.12"), "ammo": Decimal("2.40")},
{"name": "Justifier Mk.II", "damage": Decimal("25.0"), "decay": Decimal("0.25"), "ammo": Decimal("5.00")},
{"name": "Marber Bravo-Type", "damage": Decimal("45.0"), "decay": Decimal("0.45"), "ammo": Decimal("9.00")},
{"name": " Maddox IV", "damage": Decimal("80.0"), "decay": Decimal("0.80"), "ammo": Decimal("16.00")},
]
MOCK_ARMOR = [
{"name": "Pixie Harness", "decay": Decimal("0.05"), "impact": Decimal("4"), "cut": Decimal("3"), "stab": Decimal("2")},
{"name": "Shogun Harness", "decay": Decimal("0.12"), "impact": Decimal("8"), "cut": Decimal("6"), "stab": Decimal("5"), "burn": Decimal("4")},
{"name": "Ghost Harness", "decay": Decimal("0.20"), "impact": Decimal("12"), "cut": Decimal("10"), "stab": Decimal("9"), "burn": Decimal("8"), "cold": Decimal("7")},
{"name": "Vigilante Harness", "decay": Decimal("0.08"), "impact": Decimal("6"), "cut": Decimal("5"), "stab": Decimal("4")},
{"name": "Hermes Harness", "decay": Decimal("0.15"), "impact": Decimal("10"), "cut": Decimal("8"), "stab": Decimal("7"), "penetration": Decimal("5")},
]
MOCK_HEALING = [
{"name": "Vivo T10", "cost": Decimal("2.0")},
{"name": "Vivo T15", "cost": Decimal("3.5")},
{"name": "Vivo S10", "cost": Decimal("4.0")},
{"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5")},
{"name": "Restoration Chip I", "cost": Decimal("5.0")},
{"name": "Restoration Chip II", "cost": Decimal("8.0")},
]
# ============================================================================
# Custom Widgets
# ============================================================================
class DecimalLineEdit(QLineEdit):
"""Line edit with decimal validation."""
def __init__(self, parent=None):
super().__init__(parent)
self.setPlaceholderText("0.00")
def get_decimal(self) -> Decimal:
"""Get value as Decimal, returns 0 on invalid input."""
text = self.text().strip()
if not text:
return Decimal("0")
try:
return Decimal(text)
except InvalidOperation:
return Decimal("0")
def set_decimal(self, value: Decimal):
"""Set value from Decimal."""
self.setText(str(value))
class DarkGroupBox(QGroupBox):
"""Group box with dark theme styling."""
def __init__(self, title: str, parent=None):
super().__init__(title, parent)
self.setStyleSheet("""
QGroupBox {
color: #e0e0e0;
border: 2px solid #3d3d3d;
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px;
}
""")
# ============================================================================
# Main Dialog
# ============================================================================
class LoadoutManagerDialog(QDialog):
"""Main dialog for managing hunting loadouts."""
loadout_saved = pyqtSignal(str) # Emitted when loadout is saved
def __init__(self, parent=None, config_dir: Optional[str] = None):
super().__init__(parent)
self.setWindowTitle("Lemontropia Suite - Loadout Manager")
self.setMinimumSize(900, 700)
# Configuration directory
if config_dir is None:
self.config_dir = Path.home() / ".lemontropia" / "loadouts"
else:
self.config_dir = Path(config_dir)
self.config_dir.mkdir(parents=True, exist_ok=True)
self.current_loadout: Optional[LoadoutConfig] = None
self._apply_dark_theme()
self._create_widgets()
self._create_layout()
self._connect_signals()
self._load_saved_loadouts()
self._populate_mock_data()
def _apply_dark_theme(self):
"""Apply dark theme styling."""
self.setStyleSheet("""
QDialog {
background-color: #1e1e1e;
}
QLabel {
color: #e0e0e0;
}
QLineEdit {
background-color: #2d2d2d;
color: #e0e0e0;
border: 1px solid #3d3d3d;
border-radius: 4px;
padding: 5px;
}
QLineEdit:focus {
border: 1px solid #4a90d9;
}
QComboBox {
background-color: #2d2d2d;
color: #e0e0e0;
border: 1px solid #3d3d3d;
border-radius: 4px;
padding: 5px;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
}
QComboBox QAbstractItemView {
background-color: #2d2d2d;
color: #e0e0e0;
selection-background-color: #4a90d9;
}
QPushButton {
background-color: #3d3d3d;
color: #e0e0e0;
border: 1px solid #4d4d4d;
border-radius: 4px;
padding: 8px 16px;
}
QPushButton:hover {
background-color: #4d4d4d;
}
QPushButton:pressed {
background-color: #5d5d5d;
}
QPushButton#saveButton {
background-color: #2e7d32;
border-color: #4caf50;
}
QPushButton#saveButton:hover {
background-color: #4caf50;
}
QPushButton#deleteButton {
background-color: #7d2e2e;
border-color: #f44336;
}
QPushButton#deleteButton:hover {
background-color: #f44336;
}
QListWidget {
background-color: #2d2d2d;
color: #e0e0e0;
border: 1px solid #3d3d3d;
border-radius: 4px;
}
QListWidget::item:selected {
background-color: #4a90d9;
}
QScrollArea {
border: none;
}
""")
def _create_widgets(self):
"""Create all UI widgets."""
# Loadout name
self.loadout_name_edit = QLineEdit()
self.loadout_name_edit.setPlaceholderText("Enter loadout name...")
# Shots per hour
self.shots_per_hour_spin = QSpinBox()
self.shots_per_hour_spin.setRange(1, 10000)
self.shots_per_hour_spin.setValue(3600)
self.shots_per_hour_spin.setSuffix(" shots/hr")
# Weapon section
self.weapon_group = DarkGroupBox("🔫 Weapon Configuration")
self.weapon_combo = QComboBox()
self.weapon_combo.setEditable(True)
self.weapon_damage_edit = DecimalLineEdit()
self.weapon_decay_edit = DecimalLineEdit()
self.weapon_ammo_edit = DecimalLineEdit()
self.dpp_label = QLabel("0.00")
self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 14px;")
# Armor section
self.armor_group = DarkGroupBox("🛡️ Armor Configuration")
self.armor_combo = QComboBox()
self.armor_combo.setEditable(True)
self.armor_decay_edit = DecimalLineEdit()
# Protection values
self.protection_stab_edit = DecimalLineEdit()
self.protection_cut_edit = DecimalLineEdit()
self.protection_impact_edit = DecimalLineEdit()
self.protection_pen_edit = DecimalLineEdit()
self.protection_shrap_edit = DecimalLineEdit()
self.protection_burn_edit = DecimalLineEdit()
self.protection_cold_edit = DecimalLineEdit()
self.protection_acid_edit = DecimalLineEdit()
self.protection_elec_edit = DecimalLineEdit()
# Healing section
self.heal_group = DarkGroupBox("💊 Healing Configuration")
self.heal_combo = QComboBox()
self.heal_combo.setEditable(True)
self.heal_cost_edit = DecimalLineEdit()
# Cost summary
self.summary_group = DarkGroupBox("📊 Cost Summary")
self.cost_per_hour_label = QLabel("0.00 PED/hr")
self.cost_per_hour_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 16px;")
self.break_even_label = QLabel("Break-even: 0.00 PED (mob health: 100)")
self.break_even_label.setStyleSheet("color: #4caf50;")
# Break-even calculator
self.mob_health_edit = DecimalLineEdit()
self.mob_health_edit.set_decimal(Decimal("100"))
self.calc_break_even_btn = QPushButton("Calculate Break-Even")
# Saved loadouts list
self.saved_list = QListWidget()
# Buttons
self.save_btn = QPushButton("💾 Save Loadout")
self.save_btn.setObjectName("saveButton")
self.load_btn = QPushButton("📂 Load Selected")
self.delete_btn = QPushButton("🗑️ Delete")
self.delete_btn.setObjectName("deleteButton")
self.new_btn = QPushButton("🆕 New Loadout")
self.close_btn = QPushButton("❌ Close")
# Refresh button for saved list
self.refresh_btn = QPushButton("🔄 Refresh")
def _create_layout(self):
"""Create the main layout."""
main_layout = QHBoxLayout(self)
main_layout.setSpacing(15)
main_layout.setContentsMargins(15, 15, 15, 15)
# Left panel - Saved loadouts
left_panel = QWidget()
left_layout = QVBoxLayout(left_panel)
left_layout.setContentsMargins(0, 0, 0, 0)
saved_label = QLabel("💼 Saved Loadouts")
saved_label.setFont(QFont("Arial", 12, QFont.Weight.Bold))
left_layout.addWidget(saved_label)
left_layout.addWidget(self.saved_list)
left_btn_layout = QHBoxLayout()
left_btn_layout.addWidget(self.load_btn)
left_btn_layout.addWidget(self.delete_btn)
left_layout.addLayout(left_btn_layout)
left_layout.addWidget(self.refresh_btn)
left_layout.addWidget(self.new_btn)
left_layout.addStretch()
left_layout.addWidget(self.close_btn)
# Right panel - Configuration
right_scroll = QScrollArea()
right_scroll.setWidgetResizable(True)
right_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.setContentsMargins(0, 0, 10, 0)
# Loadout name header
name_layout = QHBoxLayout()
name_label = QLabel("Loadout Name:")
name_label.setFont(QFont("Arial", 10, QFont.Weight.Bold))
name_layout.addWidget(name_label)
name_layout.addWidget(self.loadout_name_edit, stretch=1)
name_layout.addWidget(QLabel("Shots/Hour:"))
name_layout.addWidget(self.shots_per_hour_spin)
right_layout.addLayout(name_layout)
# Weapon configuration
weapon_layout = QFormLayout(self.weapon_group)
weapon_layout.addRow("Weapon:", self.weapon_combo)
weapon_layout.addRow("Damage:", self.weapon_damage_edit)
weapon_layout.addRow("Decay/shot (PEC):", self.weapon_decay_edit)
weapon_layout.addRow("Ammo/shot (PEC):", self.weapon_ammo_edit)
weapon_layout.addRow("DPP (Damage Per Pec):", self.dpp_label)
right_layout.addWidget(self.weapon_group)
# Armor configuration
armor_layout = QFormLayout(self.armor_group)
armor_layout.addRow("Armor Set:", self.armor_combo)
armor_layout.addRow("Decay/hit (PEC):", self.armor_decay_edit)
# Protection grid
protection_frame = QFrame()
protection_layout = QGridLayout(protection_frame)
protection_layout.setSpacing(5)
protections = [
("Stab:", self.protection_stab_edit),
("Cut:", self.protection_cut_edit),
("Impact:", self.protection_impact_edit),
("Penetration:", self.protection_pen_edit),
("Shrapnel:", self.protection_shrap_edit),
("Burn:", self.protection_burn_edit),
("Cold:", self.protection_cold_edit),
("Acid:", self.protection_acid_edit),
("Electric:", self.protection_elec_edit),
]
for i, (label, edit) in enumerate(protections):
row = i // 3
col = (i % 3) * 2
protection_layout.addWidget(QLabel(label), row, col)
protection_layout.addWidget(edit, row, col + 1)
armor_layout.addRow("Protection Values:", protection_frame)
right_layout.addWidget(self.armor_group)
# Healing configuration
heal_layout = QFormLayout(self.heal_group)
heal_layout.addRow("Healing Tool:", self.heal_combo)
heal_layout.addRow("Cost/heal (PEC):", self.heal_cost_edit)
right_layout.addWidget(self.heal_group)
# Cost summary
summary_layout = QFormLayout(self.summary_group)
summary_layout.addRow("Estimated Cost:", self.cost_per_hour_label)
break_even_layout = QHBoxLayout()
break_even_layout.addWidget(QLabel("Mob Health:"))
break_even_layout.addWidget(self.mob_health_edit)
break_even_layout.addWidget(self.calc_break_even_btn)
summary_layout.addRow(break_even_layout)
summary_layout.addRow("", self.break_even_label)
right_layout.addWidget(self.summary_group)
# Save button
right_layout.addWidget(self.save_btn)
right_layout.addStretch()
right_scroll.setWidget(right_widget)
# Splitter
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.addWidget(left_panel)
splitter.addWidget(right_scroll)
splitter.setSizes([250, 650])
main_layout.addWidget(splitter)
def _connect_signals(self):
"""Connect all signal handlers."""
# Weapon changes
self.weapon_combo.currentTextChanged.connect(self._on_weapon_changed)
self.weapon_damage_edit.textChanged.connect(self._update_calculations)
self.weapon_decay_edit.textChanged.connect(self._update_calculations)
self.weapon_ammo_edit.textChanged.connect(self._update_calculations)
# Armor changes
self.armor_combo.currentTextChanged.connect(self._on_armor_changed)
# Healing changes
self.heal_combo.currentTextChanged.connect(self._on_heal_changed)
# Buttons
self.save_btn.clicked.connect(self._save_loadout)
self.load_btn.clicked.connect(self._load_selected)
self.delete_btn.clicked.connect(self._delete_selected)
self.new_btn.clicked.connect(self._new_loadout)
self.refresh_btn.clicked.connect(self._load_saved_loadouts)
self.close_btn.clicked.connect(self.reject)
self.calc_break_even_btn.clicked.connect(self._calculate_break_even)
self.shots_per_hour_spin.valueChanged.connect(self._update_calculations)
# Double click on list
self.saved_list.itemDoubleClicked.connect(self._load_from_item)
def _populate_mock_data(self):
"""Populate combos with mock data."""
# Weapons
self.weapon_combo.addItem("-- Custom --")
for weapon in MOCK_WEAPONS:
self.weapon_combo.addItem(weapon["name"])
# Armor
self.armor_combo.addItem("-- Custom --")
for armor in MOCK_ARMOR:
self.armor_combo.addItem(armor["name"])
# Healing
self.heal_combo.addItem("-- Custom --")
for heal in MOCK_HEALING:
self.heal_combo.addItem(heal["name"])
def _on_weapon_changed(self, name: str):
"""Handle weapon selection change."""
for weapon in MOCK_WEAPONS:
if weapon["name"] == name:
self.weapon_damage_edit.set_decimal(weapon["damage"])
self.weapon_decay_edit.set_decimal(weapon["decay"])
self.weapon_ammo_edit.set_decimal(weapon["ammo"])
break
self._update_calculations()
def _on_armor_changed(self, name: str):
"""Handle armor selection change."""
for armor in MOCK_ARMOR:
if armor["name"] == name:
self.armor_decay_edit.set_decimal(armor["decay"])
self.protection_impact_edit.set_decimal(Decimal(armor.get("impact", "0")))
self.protection_cut_edit.set_decimal(Decimal(armor.get("cut", "0")))
self.protection_stab_edit.set_decimal(Decimal(armor.get("stab", "0")))
self.protection_burn_edit.set_decimal(Decimal(armor.get("burn", "0")))
self.protection_cold_edit.set_decimal(Decimal(armor.get("cold", "0")))
self.protection_pen_edit.set_decimal(Decimal(armor.get("penetration", "0")))
break
def _on_heal_changed(self, name: str):
"""Handle healing selection change."""
for heal in MOCK_HEALING:
if heal["name"] == name:
self.heal_cost_edit.set_decimal(heal["cost"])
break
def _update_calculations(self):
"""Update DPP and cost calculations."""
try:
config = self._get_current_config()
# Update DPP
dpp = config.calculate_dpp()
self.dpp_label.setText(f"{dpp:.4f}")
# Update cost per hour
cost_per_hour = config.calculate_cost_per_hour()
self.cost_per_hour_label.setText(f"{cost_per_hour:.2f} PED/hr")
except Exception as e:
pass # Ignore calculation errors during typing
def _calculate_break_even(self):
"""Calculate and display break-even loot value."""
try:
config = self._get_current_config()
mob_health = self.mob_health_edit.get_decimal()
if mob_health <= 0:
QMessageBox.warning(self, "Invalid Input", "Mob health must be greater than 0")
return
break_even = config.calculate_break_even(mob_health)
self.break_even_label.setText(
f"Break-even: {break_even:.2f} PED (mob health: {mob_health})"
)
except Exception as e:
QMessageBox.critical(self, "Error", f"Calculation failed: {str(e)}")
def _get_current_config(self) -> LoadoutConfig:
"""Get current configuration from UI fields."""
return LoadoutConfig(
name=self.loadout_name_edit.text().strip() or "Unnamed",
weapon_name=self.weapon_combo.currentText(),
weapon_damage=self.weapon_damage_edit.get_decimal(),
weapon_decay_pec=self.weapon_decay_edit.get_decimal(),
weapon_ammo_pec=self.weapon_ammo_edit.get_decimal(),
armor_name=self.armor_combo.currentText(),
armor_decay_pec=self.armor_decay_edit.get_decimal(),
heal_cost_pec=self.heal_cost_edit.get_decimal(),
shots_per_hour=self.shots_per_hour_spin.value(),
protection_stab=self.protection_stab_edit.get_decimal(),
protection_cut=self.protection_cut_edit.get_decimal(),
protection_impact=self.protection_impact_edit.get_decimal(),
protection_penetration=self.protection_pen_edit.get_decimal(),
protection_shrapnel=self.protection_shrap_edit.get_decimal(),
protection_burn=self.protection_burn_edit.get_decimal(),
protection_cold=self.protection_cold_edit.get_decimal(),
protection_acid=self.protection_acid_edit.get_decimal(),
protection_electric=self.protection_elec_edit.get_decimal(),
)
def _set_config(self, config: LoadoutConfig):
"""Set UI fields from configuration."""
self.loadout_name_edit.setText(config.name)
self.shots_per_hour_spin.setValue(config.shots_per_hour)
self.weapon_combo.setCurrentText(config.weapon_name)
self.weapon_damage_edit.set_decimal(config.weapon_damage)
self.weapon_decay_edit.set_decimal(config.weapon_decay_pec)
self.weapon_ammo_edit.set_decimal(config.weapon_ammo_pec)
self.armor_combo.setCurrentText(config.armor_name)
self.armor_decay_edit.set_decimal(config.armor_decay_pec)
self.protection_stab_edit.set_decimal(config.protection_stab)
self.protection_cut_edit.set_decimal(config.protection_cut)
self.protection_impact_edit.set_decimal(config.protection_impact)
self.protection_pen_edit.set_decimal(config.protection_penetration)
self.protection_shrap_edit.set_decimal(config.protection_shrapnel)
self.protection_burn_edit.set_decimal(config.protection_burn)
self.protection_cold_edit.set_decimal(config.protection_cold)
self.protection_acid_edit.set_decimal(config.protection_acid)
self.protection_elec_edit.set_decimal(config.protection_electric)
self.heal_combo.setCurrentText("-- Custom --")
self.heal_cost_edit.set_decimal(config.heal_cost_pec)
self._update_calculations()
def _save_loadout(self):
"""Save current loadout to file."""
name = self.loadout_name_edit.text().strip()
if not name:
QMessageBox.warning(self, "Missing Name", "Please enter a loadout name")
return
# Sanitize filename
safe_name = "".join(c for c in name if c.isalnum() or c in "._- ").strip()
if not safe_name:
safe_name = "unnamed"
config = self._get_current_config()
config.name = name
filepath = self.config_dir / f"{safe_name}.json"
try:
with open(filepath, 'w') as f:
json.dump(config.to_dict(), f, indent=2)
self.current_loadout = config
self.loadout_saved.emit(name)
self._load_saved_loadouts()
QMessageBox.information(self, "Saved", f"Loadout '{name}' saved successfully!")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save: {str(e)}")
def _load_saved_loadouts(self):
"""Load list of saved loadouts."""
self.saved_list.clear()
try:
for filepath in sorted(self.config_dir.glob("*.json")):
try:
with open(filepath, 'r') as f:
data = json.load(f)
config = LoadoutConfig.from_dict(data)
item = QListWidgetItem(f"📋 {config.name}")
item.setData(Qt.ItemDataRole.UserRole, str(filepath))
item.setToolTip(
f"Weapon: {config.weapon_name}\n"
f"Armor: {config.armor_name}\n"
f"DPP: {config.calculate_dpp():.2f}"
)
self.saved_list.addItem(item)
except Exception:
continue
except Exception:
pass
def _load_selected(self):
"""Load the selected loadout from the list."""
item = self.saved_list.currentItem()
if item:
self._load_from_item(item)
else:
QMessageBox.information(self, "No Selection", "Please select a loadout to load")
def _load_from_item(self, item: QListWidgetItem):
"""Load loadout from a list item."""
filepath = item.data(Qt.ItemDataRole.UserRole)
if not filepath:
return
try:
with open(filepath, 'r') as f:
data = json.load(f)
config = LoadoutConfig.from_dict(data)
self._set_config(config)
self.current_loadout = config
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
def _delete_selected(self):
"""Delete the selected loadout."""
item = self.saved_list.currentItem()
if not item:
QMessageBox.information(self, "No Selection", "Please select a loadout to delete")
return
filepath = item.data(Qt.ItemDataRole.UserRole)
name = item.text().replace("📋 ", "")
reply = QMessageBox.question(
self, "Confirm Delete",
f"Are you sure you want to delete '{name}'?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
try:
os.remove(filepath)
self._load_saved_loadouts()
QMessageBox.information(self, "Deleted", f"'{name}' deleted successfully")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to delete: {str(e)}")
def _new_loadout(self):
"""Clear all fields for a new loadout."""
self.loadout_name_edit.clear()
self.weapon_combo.setCurrentIndex(0)
self.armor_combo.setCurrentIndex(0)
self.heal_combo.setCurrentIndex(0)
self.mob_health_edit.set_decimal(Decimal("100"))
self.current_loadout = None
self._update_calculations()
def get_current_loadout(self) -> Optional[LoadoutConfig]:
"""Get the currently loaded/created loadout."""
return self.current_loadout
# ============================================================================
# Main entry point for testing
# ============================================================================
def main():
"""Run the loadout manager as a standalone application."""
import sys
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
app.setStyle('Fusion')
# Set application-wide font
font = QFont("Segoe UI", 10)
app.setFont(font)
dialog = LoadoutManagerDialog()
# Connect signal for testing
dialog.loadout_saved.connect(lambda name: print(f"Loadout saved: {name}"))
if dialog.exec() == QDialog.DialogCode.Accepted:
config = dialog.get_current_loadout()
if config:
print(f"\nFinal Loadout: {config.name}")
print(f" Weapon: {config.weapon_name} (DPP: {config.calculate_dpp():.2f})")
print(f" Cost/hour: {config.calculate_cost_per_hour():.2f} PED")
sys.exit(0)
if __name__ == "__main__":
main()