1839 lines
73 KiB
Python
1839 lines
73 KiB
Python
"""
|
|
Lemontropia Suite - Loadout Manager UI v2.0
|
|
Full API integration with Entropia Nexus and complete attachment support.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import logging
|
|
from dataclasses import dataclass, asdict, field
|
|
from decimal import Decimal, InvalidOperation
|
|
from pathlib import Path
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout,
|
|
QLineEdit, QComboBox, QLabel, QPushButton,
|
|
QGroupBox, QSpinBox, QMessageBox,
|
|
QListWidget, QListWidgetItem, QSplitter, QWidget,
|
|
QFrame, QScrollArea, QGridLayout, QCheckBox,
|
|
QDialogButtonBox, QTreeWidget, QTreeWidgetItem,
|
|
QHeaderView, QTabWidget, QProgressDialog
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QThread
|
|
from PyQt6.QtGui import QFont
|
|
|
|
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
|
|
from core.attachments import (
|
|
Attachment, WeaponAmplifier, WeaponScope, WeaponAbsorber,
|
|
ArmorPlating, Enhancer, can_attach, get_mock_attachments
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Data Structures
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class AttachmentConfig:
|
|
"""Configuration for an equipped attachment."""
|
|
name: str
|
|
item_id: str
|
|
attachment_type: str
|
|
decay_pec: Decimal
|
|
# Effect values
|
|
damage_bonus: Decimal = Decimal("0")
|
|
range_bonus: Decimal = Decimal("0")
|
|
efficiency_bonus: Decimal = Decimal("0")
|
|
protection_bonus: Dict[str, Decimal] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
'name': self.name,
|
|
'item_id': self.item_id,
|
|
'attachment_type': self.attachment_type,
|
|
'decay_pec': str(self.decay_pec),
|
|
'damage_bonus': str(self.damage_bonus),
|
|
'range_bonus': str(self.range_bonus),
|
|
'efficiency_bonus': str(self.efficiency_bonus),
|
|
'protection_bonus': {k: str(v) for k, v in self.protection_bonus.items()},
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "AttachmentConfig":
|
|
return cls(
|
|
name=data['name'],
|
|
item_id=data['item_id'],
|
|
attachment_type=data['attachment_type'],
|
|
decay_pec=Decimal(data['decay_pec']),
|
|
damage_bonus=Decimal(data.get('damage_bonus', '0')),
|
|
range_bonus=Decimal(data.get('range_bonus', '0')),
|
|
efficiency_bonus=Decimal(data.get('efficiency_bonus', '0')),
|
|
protection_bonus={k: Decimal(v) for k, v in data.get('protection_bonus', {}).items()},
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class LoadoutConfig:
|
|
"""Configuration for a hunting loadout with full gear and attachments."""
|
|
name: str
|
|
|
|
# Weapon
|
|
weapon_name: str
|
|
weapon_id: int = 0
|
|
weapon_damage: Decimal = Decimal("0")
|
|
weapon_decay_pec: Decimal = Decimal("0")
|
|
weapon_ammo_pec: Decimal = Decimal("0")
|
|
weapon_dpp: Decimal = Decimal("0")
|
|
|
|
# Weapon Attachments
|
|
weapon_amplifier: Optional[AttachmentConfig] = None
|
|
weapon_scope: Optional[AttachmentConfig] = None
|
|
weapon_absorber: Optional[AttachmentConfig] = None
|
|
|
|
# Armor
|
|
armor_name: str = "-- None --"
|
|
armor_id: int = 0
|
|
armor_decay_pec: Decimal = Decimal("0")
|
|
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")
|
|
|
|
# Armor Plating
|
|
armor_plating: Optional[AttachmentConfig] = None
|
|
|
|
# Healing
|
|
heal_name: str = "-- Custom --"
|
|
heal_cost_pec: Decimal = Decimal("2.0")
|
|
heal_amount: Decimal = Decimal("20")
|
|
|
|
# Settings
|
|
shots_per_hour: int = 3600
|
|
hits_per_hour: int = 720 # Estimate: 1 hit per 5 shots
|
|
heals_per_hour: int = 60 # Estimate: 1 heal per minute
|
|
|
|
def get_total_damage(self) -> Decimal:
|
|
"""Calculate total damage including amplifier."""
|
|
base = self.weapon_damage
|
|
if self.weapon_amplifier:
|
|
base += self.weapon_amplifier.damage_bonus
|
|
return base
|
|
|
|
def get_total_decay_per_shot(self) -> Decimal:
|
|
"""Calculate total decay per shot including attachments."""
|
|
total = self.weapon_decay_pec
|
|
if self.weapon_amplifier:
|
|
total += self.weapon_amplifier.decay_pec
|
|
if self.weapon_scope:
|
|
total += self.weapon_scope.decay_pec
|
|
if self.weapon_absorber:
|
|
total += self.weapon_absorber.decay_pec
|
|
return total
|
|
|
|
def get_total_ammo_per_shot(self) -> Decimal:
|
|
"""Calculate total ammo cost per shot in PEC.
|
|
|
|
Note: ammo_burn from API is in ammo units (1 ammo = 0.01 PEC)
|
|
"""
|
|
# Convert ammo units to PEC (1 ammo = 0.01 PEC)
|
|
total = self.weapon_ammo_pec * Decimal("0.01")
|
|
if self.weapon_amplifier:
|
|
# Amplifiers add ammo cost based on damage increase
|
|
# Rough estimate: 0.2 PEC per damage point for amp
|
|
total += self.weapon_amplifier.damage_bonus * Decimal("0.2")
|
|
return total
|
|
|
|
def calculate_dpp(self) -> Decimal:
|
|
"""Calculate Damage Per Pec (DPP) with all attachments."""
|
|
total_damage = self.get_total_damage()
|
|
total_cost = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
if total_cost == 0:
|
|
return Decimal("0")
|
|
return total_damage / total_cost
|
|
|
|
def calculate_weapon_cost_per_hour(self) -> Decimal:
|
|
"""Calculate weapon cost per hour."""
|
|
cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
return cost_per_shot * Decimal(self.shots_per_hour)
|
|
|
|
def calculate_armor_cost_per_hour(self) -> Decimal:
|
|
"""Calculate armor cost per hour including plating."""
|
|
total_decay = self.armor_decay_pec
|
|
if self.armor_plating:
|
|
total_decay += self.armor_plating.decay_pec
|
|
return total_decay * Decimal(self.hits_per_hour)
|
|
|
|
def calculate_heal_cost_per_hour(self) -> Decimal:
|
|
"""Calculate healing cost per hour."""
|
|
return self.heal_cost_pec * Decimal(self.heals_per_hour)
|
|
|
|
def calculate_total_cost_per_hour(self) -> Decimal:
|
|
"""Calculate total PED cost per hour including all gear and attachments."""
|
|
weapon_cost = self.calculate_weapon_cost_per_hour()
|
|
armor_cost = self.calculate_armor_cost_per_hour()
|
|
heal_cost = self.calculate_heal_cost_per_hour()
|
|
|
|
total_pec = weapon_cost + armor_cost + heal_cost
|
|
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."""
|
|
total_damage = self.get_total_damage()
|
|
shots_to_kill = mob_health / total_damage if total_damage > 0 else Decimal("1")
|
|
if shots_to_kill < 1:
|
|
shots_to_kill = Decimal("1")
|
|
|
|
cost_per_shot = self.get_total_decay_per_shot() + self.get_total_ammo_per_shot()
|
|
total_cost_pec = shots_to_kill * cost_per_shot
|
|
return total_cost_pec / Decimal("100") # Convert to PED
|
|
|
|
def get_total_protection(self) -> Dict[str, Decimal]:
|
|
"""Get total protection values including plating."""
|
|
protections = {
|
|
'stab': self.protection_stab,
|
|
'cut': self.protection_cut,
|
|
'impact': self.protection_impact,
|
|
'penetration': self.protection_penetration,
|
|
'shrapnel': self.protection_shrapnel,
|
|
'burn': self.protection_burn,
|
|
'cold': self.protection_cold,
|
|
'acid': self.protection_acid,
|
|
'electric': self.protection_electric,
|
|
}
|
|
|
|
if self.armor_plating and self.armor_plating.protection_bonus:
|
|
for dmg_type, value in self.armor_plating.protection_bonus.items():
|
|
if dmg_type in protections:
|
|
protections[dmg_type] += value
|
|
|
|
return protections
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary for JSON serialization."""
|
|
data = {
|
|
k: str(v) if isinstance(v, Decimal) else v
|
|
for k, v in asdict(self).items()
|
|
}
|
|
# Handle attachment configs
|
|
if self.weapon_amplifier:
|
|
data['weapon_amplifier'] = self.weapon_amplifier.to_dict()
|
|
if self.weapon_scope:
|
|
data['weapon_scope'] = self.weapon_scope.to_dict()
|
|
if self.weapon_absorber:
|
|
data['weapon_absorber'] = self.weapon_absorber.to_dict()
|
|
if self.armor_plating:
|
|
data['armor_plating'] = self.armor_plating.to_dict()
|
|
return data
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> "LoadoutConfig":
|
|
"""Create LoadoutConfig from dictionary."""
|
|
decimal_fields = [
|
|
'weapon_damage', 'weapon_decay_pec', 'weapon_ammo_pec', 'weapon_dpp',
|
|
'armor_decay_pec', 'heal_cost_pec', 'heal_amount', '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])
|
|
|
|
# Handle integer fields
|
|
int_fields = ['weapon_id', 'armor_id', 'shots_per_hour', 'hits_per_hour', 'heals_per_hour']
|
|
for field in int_fields:
|
|
if field in data:
|
|
data[field] = int(data[field])
|
|
|
|
# Handle attachment configs
|
|
if 'weapon_amplifier' in data and data['weapon_amplifier']:
|
|
data['weapon_amplifier'] = AttachmentConfig.from_dict(data['weapon_amplifier'])
|
|
else:
|
|
data['weapon_amplifier'] = None
|
|
|
|
if 'weapon_scope' in data and data['weapon_scope']:
|
|
data['weapon_scope'] = AttachmentConfig.from_dict(data['weapon_scope'])
|
|
else:
|
|
data['weapon_scope'] = None
|
|
|
|
if 'weapon_absorber' in data and data['weapon_absorber']:
|
|
data['weapon_absorber'] = AttachmentConfig.from_dict(data['weapon_absorber'])
|
|
else:
|
|
data['weapon_absorber'] = None
|
|
|
|
if 'armor_plating' in data and data['armor_plating']:
|
|
data['armor_plating'] = AttachmentConfig.from_dict(data['armor_plating'])
|
|
else:
|
|
data['armor_plating'] = None
|
|
|
|
# Handle legacy configs
|
|
if 'heal_name' not in data:
|
|
data['heal_name'] = '-- Custom --'
|
|
|
|
return cls(**data)
|
|
|
|
|
|
# ============================================================================
|
|
# Mock Data for Healing
|
|
# ============================================================================
|
|
|
|
MOCK_HEALING = [
|
|
{"name": "Vivo T10", "cost": Decimal("2.0"), "amount": Decimal("12")},
|
|
{"name": "Vivo T15", "cost": Decimal("3.5"), "amount": Decimal("18")},
|
|
{"name": "Vivo S10", "cost": Decimal("4.0"), "amount": Decimal("25")},
|
|
{"name": "Refurbished H.E.A.R.T.", "cost": Decimal("1.5"), "amount": Decimal("8")},
|
|
{"name": "Restoration Chip I", "cost": Decimal("5.0"), "amount": Decimal("30")},
|
|
{"name": "Restoration Chip II", "cost": Decimal("8.0"), "amount": Decimal("50")},
|
|
{"name": "Restoration Chip III", "cost": Decimal("12.0"), "amount": Decimal("80")},
|
|
{"name": "Mod 2350", "cost": Decimal("15.0"), "amount": Decimal("100")},
|
|
]
|
|
|
|
|
|
# ============================================================================
|
|
# 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;
|
|
}
|
|
""")
|
|
|
|
|
|
# ============================================================================
|
|
# Attachment Selector Dialog
|
|
# ============================================================================
|
|
|
|
class AttachmentSelectorDialog(QDialog):
|
|
"""Dialog for selecting attachments."""
|
|
|
|
attachment_selected = pyqtSignal(object) # AttachmentConfig
|
|
|
|
def __init__(self, attachment_type: str, parent=None):
|
|
super().__init__(parent)
|
|
self.attachment_type = attachment_type
|
|
self.selected_attachment = None
|
|
|
|
self.setWindowTitle(f"Select {attachment_type.title()}")
|
|
self.setMinimumSize(500, 400)
|
|
self._setup_ui()
|
|
self._load_attachments()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Header
|
|
header = QLabel(f"Select a {self.attachment_type.title()}")
|
|
header.setFont(QFont("Arial", 12, QFont.Weight.Bold))
|
|
layout.addWidget(header)
|
|
|
|
# Attachment list
|
|
self.list_widget = QListWidget()
|
|
self.list_widget.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.list_widget.itemDoubleClicked.connect(self._on_double_click)
|
|
layout.addWidget(self.list_widget)
|
|
|
|
# Stats preview
|
|
self.preview_label = QLabel("Select an attachment to view stats")
|
|
self.preview_label.setStyleSheet("color: #888888; padding: 10px;")
|
|
layout.addWidget(self.preview_label)
|
|
|
|
# Buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_accept)
|
|
button_box.rejected.connect(self.reject)
|
|
self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok)
|
|
self.ok_btn.setEnabled(False)
|
|
layout.addWidget(button_box)
|
|
|
|
# None option
|
|
self.none_btn = QPushButton("Remove Attachment")
|
|
self.none_btn.clicked.connect(self._on_none)
|
|
layout.addWidget(self.none_btn)
|
|
|
|
def _load_attachments(self):
|
|
"""Load attachments from mock data."""
|
|
attachments = get_mock_attachments(self.attachment_type)
|
|
self.attachments = attachments
|
|
|
|
self.list_widget.clear()
|
|
|
|
for att in attachments:
|
|
item = QListWidgetItem(f"📎 {att.name}")
|
|
item.setData(Qt.ItemDataRole.UserRole, att)
|
|
|
|
# Add tooltip with stats
|
|
tooltip = f"Decay: {att.decay_pec} PEC\n"
|
|
if hasattr(att, 'damage_increase'):
|
|
tooltip += f"Damage: +{att.damage_increase}"
|
|
elif hasattr(att, 'range_increase'):
|
|
tooltip += f"Range: +{att.range_increase}m"
|
|
elif hasattr(att, 'damage_reduction'):
|
|
tooltip += f"Protection: +{att.damage_reduction}"
|
|
elif hasattr(att, 'protection_impact'):
|
|
tooltip += f"Impact: +{att.protection_impact}"
|
|
|
|
item.setToolTip(tooltip)
|
|
self.list_widget.addItem(item)
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
selected = self.list_widget.selectedItems()
|
|
if selected:
|
|
attachment = selected[0].data(Qt.ItemDataRole.UserRole)
|
|
self.selected_attachment = attachment
|
|
self.ok_btn.setEnabled(True)
|
|
self._update_preview(attachment)
|
|
|
|
def _update_preview(self, attachment):
|
|
"""Update preview label."""
|
|
text = f"<b>{attachment.name}</b><br>"
|
|
text += f"Decay: {attachment.decay_pec} PEC<br>"
|
|
|
|
if isinstance(attachment, WeaponAmplifier):
|
|
text += f"Damage Increase: +{attachment.damage_increase}<br>"
|
|
text += f"Ammo Increase: +{attachment.ammo_increase}"
|
|
elif isinstance(attachment, WeaponScope):
|
|
text += f"Range Increase: +{attachment.range_increase}m"
|
|
elif isinstance(attachment, WeaponAbsorber):
|
|
text += f"Damage Reduction: -{attachment.damage_reduction}"
|
|
elif isinstance(attachment, ArmorPlating):
|
|
text += f"Total Protection: +{attachment.get_total_protection()}"
|
|
|
|
self.preview_label.setText(text)
|
|
self.preview_label.setStyleSheet("color: #e0e0e0; padding: 10px; background: #2d2d2d; border-radius: 4px;")
|
|
|
|
def _on_double_click(self, item):
|
|
"""Handle double click."""
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
"""Handle OK button."""
|
|
if self.selected_attachment:
|
|
att = self.selected_attachment
|
|
|
|
# Build protection bonus dict for armor plating
|
|
protection_bonus = {}
|
|
if isinstance(att, ArmorPlating):
|
|
protection_bonus = {
|
|
'stab': att.protection_stab,
|
|
'cut': att.protection_cut,
|
|
'impact': att.protection_impact,
|
|
'penetration': att.protection_penetration,
|
|
'shrapnel': att.protection_shrapnel,
|
|
'burn': att.protection_burn,
|
|
'cold': att.protection_cold,
|
|
'acid': att.protection_acid,
|
|
'electric': att.protection_electric,
|
|
}
|
|
|
|
config = AttachmentConfig(
|
|
name=att.name,
|
|
item_id=att.item_id,
|
|
attachment_type=att.attachment_type,
|
|
decay_pec=att.decay_pec,
|
|
damage_bonus=getattr(att, 'damage_increase', Decimal("0")),
|
|
range_bonus=getattr(att, 'range_increase', Decimal("0")),
|
|
efficiency_bonus=Decimal("0"),
|
|
protection_bonus=protection_bonus,
|
|
)
|
|
|
|
self.attachment_selected.emit(config)
|
|
self.accept()
|
|
|
|
def _on_none(self):
|
|
"""Remove attachment."""
|
|
self.attachment_selected.emit(None)
|
|
self.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Gear Loader Threads
|
|
# ============================================================================
|
|
|
|
class WeaponLoaderThread(QThread):
|
|
"""Thread to load weapons from API."""
|
|
weapons_loaded = pyqtSignal(list)
|
|
error_occurred = pyqtSignal(str)
|
|
|
|
def run(self):
|
|
try:
|
|
api = EntropiaNexusAPI()
|
|
weapons = api.get_all_weapons()
|
|
self.weapons_loaded.emit(weapons)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load weapons: {e}")
|
|
self.error_occurred.emit(str(e))
|
|
|
|
|
|
class ArmorLoaderThread(QThread):
|
|
"""Thread to load armors from API."""
|
|
armors_loaded = pyqtSignal(list)
|
|
error_occurred = pyqtSignal(str)
|
|
|
|
def run(self):
|
|
try:
|
|
api = EntropiaNexusAPI()
|
|
armors = api.get_all_armors()
|
|
self.armors_loaded.emit(armors)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load armors: {e}")
|
|
self.error_occurred.emit(str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# Weapon Selector Dialog
|
|
# ============================================================================
|
|
|
|
class WeaponSelectorDialog(QDialog):
|
|
"""Dialog for selecting weapons from Entropia Nexus API."""
|
|
|
|
weapon_selected = pyqtSignal(object) # WeaponStats
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Select Weapon - Entropia Nexus")
|
|
self.setMinimumSize(900, 600)
|
|
self.weapons = []
|
|
self.selected_weapon = None
|
|
self.api = EntropiaNexusAPI()
|
|
|
|
self._setup_ui()
|
|
self._load_data()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(10)
|
|
|
|
# Status
|
|
self.status_label = QLabel("Loading weapons from Entropia Nexus...")
|
|
layout.addWidget(self.status_label)
|
|
|
|
# Search
|
|
search_layout = QHBoxLayout()
|
|
search_layout.addWidget(QLabel("Search:"))
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Search weapons by name...")
|
|
self.search_input.returnPressed.connect(self._on_search)
|
|
search_layout.addWidget(self.search_input)
|
|
self.search_btn = QPushButton("Search")
|
|
self.search_btn.clicked.connect(self._on_search)
|
|
search_layout.addWidget(self.search_btn)
|
|
layout.addLayout(search_layout)
|
|
|
|
# Results tree
|
|
self.results_tree = QTreeWidget()
|
|
self.results_tree.setHeaderLabels([
|
|
"Name", "Type", "Category", "Damage", "DPP", "Decay", "Ammo", "Cost/h"
|
|
])
|
|
header = self.results_tree.header()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
for i in range(1, 8):
|
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
|
|
header.resizeSection(1, 80)
|
|
header.resizeSection(2, 80)
|
|
header.resizeSection(3, 60)
|
|
header.resizeSection(4, 60)
|
|
header.resizeSection(5, 70)
|
|
header.resizeSection(6, 60)
|
|
header.resizeSection(7, 70)
|
|
|
|
self.results_tree.setAlternatingRowColors(True)
|
|
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
|
|
layout.addWidget(self.results_tree)
|
|
|
|
# Stats preview
|
|
self.preview_group = DarkGroupBox("Weapon Stats")
|
|
self.preview_layout = QFormLayout(self.preview_group)
|
|
self.preview_layout.addRow("Select a weapon to view stats", QLabel(""))
|
|
layout.addWidget(self.preview_group)
|
|
|
|
# Buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_accept)
|
|
button_box.rejected.connect(self.reject)
|
|
self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok)
|
|
self.ok_btn.setEnabled(False)
|
|
self.ok_btn.setText("Select Weapon")
|
|
layout.addWidget(button_box)
|
|
|
|
def _load_data(self):
|
|
"""Load weapons asynchronously."""
|
|
self.loader = WeaponLoaderThread()
|
|
self.loader.weapons_loaded.connect(self._on_data_loaded)
|
|
self.loader.error_occurred.connect(self._on_load_error)
|
|
self.loader.start()
|
|
|
|
def _on_data_loaded(self, weapons):
|
|
"""Handle loaded weapons."""
|
|
self.weapons = weapons
|
|
self.status_label.setText(f"Loaded {len(weapons):,} weapons from Entropia Nexus")
|
|
self._populate_tree(weapons[:200]) # Show first 200
|
|
|
|
def _on_load_error(self, error):
|
|
"""Handle load error."""
|
|
self.status_label.setText(f"Error loading weapons: {error}")
|
|
QMessageBox.critical(self, "Error", f"Failed to load weapons: {error}")
|
|
|
|
def _populate_tree(self, weapons):
|
|
"""Populate tree with weapons."""
|
|
self.results_tree.clear()
|
|
|
|
for w in weapons:
|
|
item = QTreeWidgetItem([
|
|
w.name,
|
|
w.type,
|
|
w.category,
|
|
str(w.total_damage),
|
|
f"{w.dpp:.2f}",
|
|
f"{w.decay:.2f}" if w.decay else "-",
|
|
str(w.ammo_burn) if w.ammo_burn else "-",
|
|
f"{w.cost_per_hour:.0f}"
|
|
])
|
|
item.setData(0, Qt.ItemDataRole.UserRole, w)
|
|
self.results_tree.addTopLevelItem(item)
|
|
|
|
def _on_search(self):
|
|
"""Search weapons."""
|
|
query = self.search_input.text().strip().lower()
|
|
if not query:
|
|
self._populate_tree(self.weapons[:200])
|
|
return
|
|
|
|
results = [w for w in self.weapons if query in w.name.lower()]
|
|
self._populate_tree(results)
|
|
self.status_label.setText(f"Found {len(results)} weapons matching '{query}'")
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
selected = self.results_tree.selectedItems()
|
|
if selected:
|
|
weapon = selected[0].data(0, Qt.ItemDataRole.UserRole)
|
|
self.selected_weapon = weapon
|
|
self.ok_btn.setEnabled(True)
|
|
self._update_preview(weapon)
|
|
else:
|
|
self.selected_weapon = None
|
|
self.ok_btn.setEnabled(False)
|
|
|
|
def _update_preview(self, w):
|
|
"""Update stats preview."""
|
|
while self.preview_layout.rowCount() > 0:
|
|
self.preview_layout.removeRow(0)
|
|
|
|
self.preview_layout.addRow("Name:", QLabel(w.name))
|
|
self.preview_layout.addRow("Type:", QLabel(f"{w.type} {w.category}"))
|
|
self.preview_layout.addRow("Damage:", QLabel(str(w.total_damage)))
|
|
self.preview_layout.addRow("DPP:", QLabel(f"{w.dpp:.3f}"))
|
|
self.preview_layout.addRow("Decay:", QLabel(f"{w.decay:.3f} PEC/shot" if w.decay else "-"))
|
|
self.preview_layout.addRow("Ammo:", QLabel(f"{w.ammo_burn} units/shot" if w.ammo_burn else "-"))
|
|
self.preview_layout.addRow("Cost/Hour:", QLabel(f"{w.cost_per_hour:.2f} PED"))
|
|
if w.efficiency:
|
|
self.preview_layout.addRow("Efficiency:", QLabel(f"{w.efficiency:.1f}%"))
|
|
|
|
def _on_double_click(self, item, column):
|
|
"""Handle double click."""
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
"""Handle OK button."""
|
|
if self.selected_weapon:
|
|
self.weapon_selected.emit(self.selected_weapon)
|
|
self.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Armor Selector Dialog
|
|
# ============================================================================
|
|
|
|
class ArmorSelectorDialog(QDialog):
|
|
"""Dialog for selecting armors from Entropia Nexus API."""
|
|
|
|
armor_selected = pyqtSignal(object) # ArmorStats
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Select Armor - Entropia Nexus")
|
|
self.setMinimumSize(800, 500)
|
|
self.armors = []
|
|
self.selected_armor = None
|
|
self.api = EntropiaNexusAPI()
|
|
|
|
self._setup_ui()
|
|
self._load_data()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(10)
|
|
|
|
# Status
|
|
self.status_label = QLabel("Loading armors from Entropia Nexus...")
|
|
layout.addWidget(self.status_label)
|
|
|
|
# Search
|
|
search_layout = QHBoxLayout()
|
|
search_layout.addWidget(QLabel("Search:"))
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Search armors by name...")
|
|
self.search_input.returnPressed.connect(self._on_search)
|
|
search_layout.addWidget(self.search_input)
|
|
self.search_btn = QPushButton("Search")
|
|
self.search_btn.clicked.connect(self._on_search)
|
|
search_layout.addWidget(self.search_btn)
|
|
layout.addLayout(search_layout)
|
|
|
|
# Results tree
|
|
self.results_tree = QTreeWidget()
|
|
self.results_tree.setHeaderLabels([
|
|
"Name", "Impact", "Cut", "Stab", "Pen", "Burn", "Cold", "Total"
|
|
])
|
|
header = self.results_tree.header()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
|
|
for i in range(1, 8):
|
|
header.setSectionResizeMode(i, QHeaderView.ResizeMode.Fixed)
|
|
header.resizeSection(i, 60)
|
|
|
|
self.results_tree.setAlternatingRowColors(True)
|
|
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
|
|
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
|
|
layout.addWidget(self.results_tree)
|
|
|
|
# Stats preview
|
|
self.preview_group = DarkGroupBox("Armor Stats")
|
|
self.preview_layout = QFormLayout(self.preview_group)
|
|
self.preview_layout.addRow("Select an armor to view stats", QLabel(""))
|
|
layout.addWidget(self.preview_group)
|
|
|
|
# Buttons
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self._on_accept)
|
|
button_box.rejected.connect(self.reject)
|
|
self.ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok)
|
|
self.ok_btn.setEnabled(False)
|
|
self.ok_btn.setText("Select Armor")
|
|
layout.addWidget(button_box)
|
|
|
|
def _load_data(self):
|
|
"""Load armors asynchronously."""
|
|
self.loader = ArmorLoaderThread()
|
|
self.loader.armors_loaded.connect(self._on_data_loaded)
|
|
self.loader.error_occurred.connect(self._on_load_error)
|
|
self.loader.start()
|
|
|
|
def _on_data_loaded(self, armors):
|
|
"""Handle loaded armors."""
|
|
self.armors = armors
|
|
self.status_label.setText(f"Loaded {len(armors):,} armors from Entropia Nexus")
|
|
self._populate_tree(armors[:200])
|
|
|
|
def _on_load_error(self, error):
|
|
"""Handle load error."""
|
|
self.status_label.setText(f"Error loading armors: {error}")
|
|
QMessageBox.critical(self, "Error", f"Failed to load armors: {error}")
|
|
|
|
def _populate_tree(self, armors):
|
|
"""Populate tree with armors."""
|
|
self.results_tree.clear()
|
|
|
|
for a in armors:
|
|
item = QTreeWidgetItem([
|
|
a.name,
|
|
str(a.protection_impact),
|
|
str(a.protection_cut),
|
|
str(a.protection_stab),
|
|
str(a.protection_penetration),
|
|
str(a.protection_burn),
|
|
str(a.protection_cold),
|
|
str(a.total_protection),
|
|
])
|
|
item.setData(0, Qt.ItemDataRole.UserRole, a)
|
|
self.results_tree.addTopLevelItem(item)
|
|
|
|
def _on_search(self):
|
|
"""Search armors."""
|
|
query = self.search_input.text().strip().lower()
|
|
if not query:
|
|
self._populate_tree(self.armors[:200])
|
|
return
|
|
|
|
results = [a for a in self.armors if query in a.name.lower()]
|
|
self._populate_tree(results)
|
|
self.status_label.setText(f"Found {len(results)} armors matching '{query}'")
|
|
|
|
def _on_selection_changed(self):
|
|
"""Handle selection change."""
|
|
selected = self.results_tree.selectedItems()
|
|
if selected:
|
|
armor = selected[0].data(0, Qt.ItemDataRole.UserRole)
|
|
self.selected_armor = armor
|
|
self.ok_btn.setEnabled(True)
|
|
self._update_preview(armor)
|
|
else:
|
|
self.selected_armor = None
|
|
self.ok_btn.setEnabled(False)
|
|
|
|
def _update_preview(self, a):
|
|
"""Update stats preview."""
|
|
while self.preview_layout.rowCount() > 0:
|
|
self.preview_layout.removeRow(0)
|
|
|
|
self.preview_layout.addRow("Name:", QLabel(a.name))
|
|
self.preview_layout.addRow("Total Protection:", QLabel(str(a.total_protection)))
|
|
self.preview_layout.addRow("Durability:", QLabel(str(a.durability)))
|
|
|
|
# Protection breakdown
|
|
prot_text = f"Impact: {a.protection_impact}, Cut: {a.protection_cut}, Stab: {a.protection_stab}"
|
|
self.preview_layout.addRow("Protection:", QLabel(prot_text))
|
|
|
|
def _on_double_click(self, item, column):
|
|
"""Handle double click."""
|
|
self._on_accept()
|
|
|
|
def _on_accept(self):
|
|
"""Handle OK button."""
|
|
if self.selected_armor:
|
|
self.armor_selected.emit(self.selected_armor)
|
|
self.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Main Loadout Manager Dialog
|
|
# ============================================================================
|
|
|
|
class LoadoutManagerDialog(QDialog):
|
|
"""Main dialog for managing hunting loadouts with full API integration."""
|
|
|
|
loadout_saved = pyqtSignal(str) # Emitted when loadout is saved
|
|
loadout_selected = pyqtSignal(object) # Emitted when loadout is selected for use
|
|
|
|
def __init__(self, parent=None, config_dir: Optional[str] = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Lemontropia Suite - Loadout Manager v2.0")
|
|
self.setMinimumSize(1000, 800)
|
|
|
|
# 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.current_weapon: Optional[WeaponStats] = None
|
|
self.current_armor: Optional[ArmorStats] = None
|
|
|
|
self._apply_dark_theme()
|
|
self._create_widgets()
|
|
self._create_layout()
|
|
self._connect_signals()
|
|
self._load_saved_loadouts()
|
|
self._populate_healing_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:disabled {
|
|
background-color: #252525;
|
|
color: #888888;
|
|
border: 1px solid #2d2d2d;
|
|
}
|
|
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;
|
|
}
|
|
QPushButton#selectButton {
|
|
background-color: #1565c0;
|
|
border-color: #2196f3;
|
|
}
|
|
QPushButton#selectButton:hover {
|
|
background-color: #2196f3;
|
|
}
|
|
QListWidget {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
}
|
|
QListWidget::item:selected {
|
|
background-color: #4a90d9;
|
|
}
|
|
QScrollArea {
|
|
border: none;
|
|
}
|
|
QTabWidget::pane {
|
|
border: 1px solid #3d3d3d;
|
|
background-color: #1e1e1e;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #2d2d2d;
|
|
color: #e0e0e0;
|
|
padding: 8px 16px;
|
|
border: 1px solid #3d3d3d;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #4a90d9;
|
|
}
|
|
""")
|
|
|
|
def _create_widgets(self):
|
|
"""Create all UI widgets."""
|
|
# Loadout name
|
|
self.loadout_name_edit = QLineEdit()
|
|
self.loadout_name_edit.setPlaceholderText("Enter loadout name...")
|
|
|
|
# Activity settings
|
|
self.shots_per_hour_spin = QSpinBox()
|
|
self.shots_per_hour_spin.setRange(1, 20000)
|
|
self.shots_per_hour_spin.setValue(3600)
|
|
self.shots_per_hour_spin.setSuffix(" /hr")
|
|
|
|
self.hits_per_hour_spin = QSpinBox()
|
|
self.hits_per_hour_spin.setRange(0, 5000)
|
|
self.hits_per_hour_spin.setValue(720)
|
|
self.hits_per_hour_spin.setSuffix(" /hr")
|
|
|
|
self.heals_per_hour_spin = QSpinBox()
|
|
self.heals_per_hour_spin.setRange(0, 500)
|
|
self.heals_per_hour_spin.setValue(60)
|
|
self.heals_per_hour_spin.setSuffix(" /hr")
|
|
|
|
# Weapon section
|
|
self.weapon_group = DarkGroupBox("🔫 Weapon Configuration")
|
|
self.select_weapon_btn = QPushButton("🔍 Select from Entropia Nexus")
|
|
self.select_weapon_btn.setObjectName("selectButton")
|
|
self.weapon_name_label = QLabel("No weapon selected")
|
|
self.weapon_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;")
|
|
|
|
self.weapon_damage_edit = DecimalLineEdit()
|
|
self.weapon_decay_edit = DecimalLineEdit()
|
|
self.weapon_ammo_edit = DecimalLineEdit()
|
|
self.dpp_label = QLabel("0.0000")
|
|
self.dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 16px;")
|
|
|
|
# Weapon attachments
|
|
self.attach_amp_btn = QPushButton("⚡ Add Amplifier")
|
|
self.attach_scope_btn = QPushButton("🔭 Add Scope")
|
|
self.attach_absorber_btn = QPushButton("🛡️ Add Absorber")
|
|
self.amp_label = QLabel("None")
|
|
self.scope_label = QLabel("None")
|
|
self.absorber_label = QLabel("None")
|
|
self.remove_amp_btn = QPushButton("✕")
|
|
self.remove_scope_btn = QPushButton("✕")
|
|
self.remove_absorber_btn = QPushButton("✕")
|
|
self.remove_amp_btn.setFixedWidth(30)
|
|
self.remove_scope_btn.setFixedWidth(30)
|
|
self.remove_absorber_btn.setFixedWidth(30)
|
|
|
|
# Armor section
|
|
self.armor_group = DarkGroupBox("🛡️ Armor Configuration")
|
|
self.select_armor_btn = QPushButton("🔍 Select from Entropia Nexus")
|
|
self.select_armor_btn.setObjectName("selectButton")
|
|
self.armor_name_label = QLabel("No armor selected")
|
|
self.armor_name_label.setStyleSheet("font-weight: bold; color: #4a90d9;")
|
|
|
|
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()
|
|
|
|
# Armor plating
|
|
self.attach_plating_btn = QPushButton("🔩 Add Plating")
|
|
self.plating_label = QLabel("None")
|
|
self.remove_plating_btn = QPushButton("✕")
|
|
self.remove_plating_btn.setFixedWidth(30)
|
|
|
|
# Healing section
|
|
self.heal_group = DarkGroupBox("💊 Healing Configuration")
|
|
self.heal_combo = QComboBox()
|
|
self.heal_cost_edit = DecimalLineEdit()
|
|
self.heal_amount_edit = DecimalLineEdit()
|
|
|
|
# Cost summary
|
|
self.summary_group = DarkGroupBox("📊 Cost Summary")
|
|
self.weapon_cost_label = QLabel("0.00 PEC/hr")
|
|
self.armor_cost_label = QLabel("0.00 PEC/hr")
|
|
self.heal_cost_label = QLabel("0.00 PEC/hr")
|
|
self.total_cost_label = QLabel("0.00 PED/hr")
|
|
self.total_cost_label.setStyleSheet("color: #ff9800; font-weight: bold; font-size: 18px;")
|
|
self.total_dpp_label = QLabel("0.0000")
|
|
self.total_dpp_label.setStyleSheet("color: #4caf50; font-weight: bold; font-size: 18px;")
|
|
|
|
# Break-even calculator
|
|
self.mob_health_edit = DecimalLineEdit()
|
|
self.mob_health_edit.set_decimal(Decimal("100"))
|
|
self.calc_break_even_btn = QPushButton("Calculate")
|
|
self.break_even_label = QLabel("Break-even: 0.00 PED")
|
|
self.break_even_label.setStyleSheet("color: #4caf50;")
|
|
|
|
# Saved loadouts list
|
|
self.saved_list = QListWidget()
|
|
|
|
# Buttons
|
|
self.save_btn = QPushButton("💾 Save Loadout")
|
|
self.save_btn.setObjectName("saveButton")
|
|
self.use_btn = QPushButton("✅ Use Loadout")
|
|
self.use_btn.setObjectName("useButton")
|
|
self.use_btn.setToolTip("Use this loadout for current session")
|
|
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")
|
|
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.use_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)
|
|
right_layout.addLayout(name_layout)
|
|
|
|
# Activity settings
|
|
activity_group = DarkGroupBox("⚙️ Activity Settings")
|
|
activity_layout = QGridLayout(activity_group)
|
|
activity_layout.addWidget(QLabel("Shots/Hour:"), 0, 0)
|
|
activity_layout.addWidget(self.shots_per_hour_spin, 0, 1)
|
|
activity_layout.addWidget(QLabel("Hits Taken/Hour:"), 0, 2)
|
|
activity_layout.addWidget(self.hits_per_hour_spin, 0, 3)
|
|
activity_layout.addWidget(QLabel("Heals/Hour:"), 0, 4)
|
|
activity_layout.addWidget(self.heals_per_hour_spin, 0, 5)
|
|
right_layout.addWidget(activity_group)
|
|
|
|
# Weapon configuration
|
|
weapon_layout = QFormLayout(self.weapon_group)
|
|
|
|
weapon_select_layout = QHBoxLayout()
|
|
weapon_select_layout.addWidget(self.select_weapon_btn)
|
|
weapon_select_layout.addWidget(self.weapon_name_label, stretch=1)
|
|
weapon_layout.addRow("Weapon:", weapon_select_layout)
|
|
|
|
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("Total DPP:", self.dpp_label)
|
|
|
|
# Attachments
|
|
attachments_frame = QFrame()
|
|
attachments_layout = QGridLayout(attachments_frame)
|
|
attachments_layout.addWidget(QLabel("Amplifier:"), 0, 0)
|
|
attachments_layout.addWidget(self.amp_label, 0, 1)
|
|
attachments_layout.addWidget(self.attach_amp_btn, 0, 2)
|
|
attachments_layout.addWidget(self.remove_amp_btn, 0, 3)
|
|
|
|
attachments_layout.addWidget(QLabel("Scope:"), 1, 0)
|
|
attachments_layout.addWidget(self.scope_label, 1, 1)
|
|
attachments_layout.addWidget(self.attach_scope_btn, 1, 2)
|
|
attachments_layout.addWidget(self.remove_scope_btn, 1, 3)
|
|
|
|
attachments_layout.addWidget(QLabel("Absorber:"), 2, 0)
|
|
attachments_layout.addWidget(self.absorber_label, 2, 1)
|
|
attachments_layout.addWidget(self.attach_absorber_btn, 2, 2)
|
|
attachments_layout.addWidget(self.remove_absorber_btn, 2, 3)
|
|
|
|
weapon_layout.addRow("Attachments:", attachments_frame)
|
|
right_layout.addWidget(self.weapon_group)
|
|
|
|
# Armor configuration
|
|
armor_layout = QFormLayout(self.armor_group)
|
|
|
|
armor_select_layout = QHBoxLayout()
|
|
armor_select_layout.addWidget(self.select_armor_btn)
|
|
armor_select_layout.addWidget(self.armor_name_label, stretch=1)
|
|
armor_layout.addRow("Armor:", armor_select_layout)
|
|
|
|
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)
|
|
|
|
# Plating
|
|
plating_layout = QHBoxLayout()
|
|
plating_layout.addWidget(QLabel("Armor Plating:"))
|
|
plating_layout.addWidget(self.plating_label)
|
|
plating_layout.addWidget(self.attach_plating_btn)
|
|
plating_layout.addWidget(self.remove_plating_btn)
|
|
armor_layout.addRow("", plating_layout)
|
|
|
|
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)
|
|
heal_layout.addRow("Heal amount:", self.heal_amount_edit)
|
|
right_layout.addWidget(self.heal_group)
|
|
|
|
# Cost summary
|
|
summary_layout = QFormLayout(self.summary_group)
|
|
summary_layout.addRow("Weapon Cost:", self.weapon_cost_label)
|
|
summary_layout.addRow("Armor Cost:", self.armor_cost_label)
|
|
summary_layout.addRow("Healing Cost:", self.heal_cost_label)
|
|
summary_layout.addRow("Total DPP:", self.total_dpp_label)
|
|
summary_layout.addRow("Total Cost:", self.total_cost_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:", 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, 750])
|
|
|
|
main_layout.addWidget(splitter)
|
|
|
|
def _connect_signals(self):
|
|
"""Connect all signal handlers."""
|
|
# Weapon selection
|
|
self.select_weapon_btn.clicked.connect(self._on_select_weapon)
|
|
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 selection
|
|
self.select_armor_btn.clicked.connect(self._on_select_armor)
|
|
|
|
# Attachments
|
|
self.attach_amp_btn.clicked.connect(lambda: self._on_attach("amplifier"))
|
|
self.attach_scope_btn.clicked.connect(lambda: self._on_attach("scope"))
|
|
self.attach_absorber_btn.clicked.connect(lambda: self._on_attach("absorber"))
|
|
self.attach_plating_btn.clicked.connect(lambda: self._on_attach("plating"))
|
|
self.remove_amp_btn.clicked.connect(self._on_remove_amp)
|
|
self.remove_scope_btn.clicked.connect(self._on_remove_scope)
|
|
self.remove_absorber_btn.clicked.connect(self._on_remove_absorber)
|
|
self.remove_plating_btn.clicked.connect(self._on_remove_plating)
|
|
|
|
# Healing
|
|
self.heal_combo.currentTextChanged.connect(self._on_heal_changed)
|
|
|
|
# Activity settings
|
|
self.shots_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
self.hits_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
self.heals_per_hour_spin.valueChanged.connect(self._update_calculations)
|
|
|
|
# Buttons
|
|
self.save_btn.clicked.connect(self._save_loadout)
|
|
self.use_btn.clicked.connect(self._use_selected)
|
|
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)
|
|
|
|
# Double click on list
|
|
self.saved_list.itemDoubleClicked.connect(self._load_from_item)
|
|
|
|
def _populate_healing_data(self):
|
|
"""Populate healing combo with data."""
|
|
self.heal_combo.addItem("-- Custom --")
|
|
for heal in MOCK_HEALING:
|
|
self.heal_combo.addItem(heal["name"])
|
|
|
|
def _on_select_weapon(self):
|
|
"""Open weapon selector dialog."""
|
|
dialog = WeaponSelectorDialog(self)
|
|
dialog.weapon_selected.connect(self._on_weapon_selected)
|
|
dialog.exec()
|
|
|
|
def _on_weapon_selected(self, weapon: WeaponStats):
|
|
"""Handle weapon selection."""
|
|
self.current_weapon = weapon
|
|
self.weapon_name_label.setText(weapon.name)
|
|
self.weapon_damage_edit.set_decimal(weapon.total_damage)
|
|
self.weapon_decay_edit.set_decimal(weapon.decay or Decimal("0"))
|
|
self.weapon_ammo_edit.set_decimal(Decimal(weapon.ammo_burn or 0))
|
|
self._update_calculations()
|
|
|
|
def _on_select_armor(self):
|
|
"""Open armor selector dialog."""
|
|
dialog = ArmorSelectorDialog(self)
|
|
dialog.armor_selected.connect(self._on_armor_selected)
|
|
dialog.exec()
|
|
|
|
def _on_armor_selected(self, armor: ArmorStats):
|
|
"""Handle armor selection."""
|
|
self.current_armor = armor
|
|
self.armor_name_label.setText(armor.name)
|
|
|
|
# Calculate decay estimate (rough approximation)
|
|
decay_estimate = Decimal("0.05") # Default estimate
|
|
self.armor_decay_edit.set_decimal(decay_estimate)
|
|
|
|
# Set protection values
|
|
self.protection_stab_edit.set_decimal(armor.protection_stab)
|
|
self.protection_cut_edit.set_decimal(armor.protection_cut)
|
|
self.protection_impact_edit.set_decimal(armor.protection_impact)
|
|
self.protection_pen_edit.set_decimal(armor.protection_penetration)
|
|
self.protection_shrap_edit.set_decimal(armor.protection_shrapnel)
|
|
self.protection_burn_edit.set_decimal(armor.protection_burn)
|
|
self.protection_cold_edit.set_decimal(armor.protection_cold)
|
|
self.protection_acid_edit.set_decimal(armor.protection_acid)
|
|
self.protection_elec_edit.set_decimal(armor.protection_electric)
|
|
|
|
self._update_calculations()
|
|
|
|
def _on_attach(self, attachment_type: str):
|
|
"""Open attachment selector."""
|
|
dialog = AttachmentSelectorDialog(attachment_type, self)
|
|
dialog.attachment_selected.connect(lambda att: self._on_attachment_selected(attachment_type, att))
|
|
dialog.exec()
|
|
|
|
def _on_attachment_selected(self, attachment_type: str, config: Optional[AttachmentConfig]):
|
|
"""Handle attachment selection."""
|
|
if attachment_type == "amplifier":
|
|
if config:
|
|
self.current_loadout = self._get_current_config()
|
|
self.current_loadout.weapon_amplifier = config
|
|
self.amp_label.setText(f"{config.name} (+{config.damage_bonus} dmg)")
|
|
else:
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_amplifier = None
|
|
self.amp_label.setText("None")
|
|
|
|
elif attachment_type == "scope":
|
|
if config:
|
|
self.current_loadout = self._get_current_config()
|
|
self.current_loadout.weapon_scope = config
|
|
self.scope_label.setText(f"{config.name} (+{config.range_bonus}m)")
|
|
else:
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_scope = None
|
|
self.scope_label.setText("None")
|
|
|
|
elif attachment_type == "absorber":
|
|
if config:
|
|
self.current_loadout = self._get_current_config()
|
|
self.current_loadout.weapon_absorber = config
|
|
# Get damage reduction from attachment
|
|
attachments = get_mock_attachments("absorber")
|
|
absorber = next((a for a in attachments if a.name == config.name), None)
|
|
reduction = absorber.damage_reduction if absorber else Decimal("0")
|
|
self.absorber_label.setText(f"{config.name} (-{reduction} dmg)")
|
|
else:
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_absorber = None
|
|
self.absorber_label.setText("None")
|
|
|
|
elif attachment_type == "plating":
|
|
if config:
|
|
self.current_loadout = self._get_current_config()
|
|
self.current_loadout.armor_plating = config
|
|
# Calculate total protection from plating
|
|
attachments = get_mock_attachments("plating")
|
|
plating = next((p for p in attachments if p.name == config.name), None)
|
|
if plating:
|
|
total_prot = plating.get_total_protection()
|
|
self.plating_label.setText(f"{config.name} (+{total_prot} prot)")
|
|
else:
|
|
self.plating_label.setText(config.name)
|
|
else:
|
|
if self.current_loadout:
|
|
self.current_loadout.armor_plating = None
|
|
self.plating_label.setText("None")
|
|
|
|
self._update_calculations()
|
|
|
|
def _on_remove_amp(self):
|
|
"""Remove amplifier."""
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_amplifier = None
|
|
self.amp_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_scope(self):
|
|
"""Remove scope."""
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_scope = None
|
|
self.scope_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_absorber(self):
|
|
"""Remove absorber."""
|
|
if self.current_loadout:
|
|
self.current_loadout.weapon_absorber = None
|
|
self.absorber_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_remove_plating(self):
|
|
"""Remove plating."""
|
|
if self.current_loadout:
|
|
self.current_loadout.armor_plating = None
|
|
self.plating_label.setText("None")
|
|
self._update_calculations()
|
|
|
|
def _on_heal_changed(self, name: str):
|
|
"""Handle healing selection change."""
|
|
if name == "-- Custom --":
|
|
self.heal_cost_edit.setEnabled(True)
|
|
self.heal_amount_edit.setEnabled(True)
|
|
self.heal_cost_edit.clear()
|
|
self.heal_amount_edit.clear()
|
|
else:
|
|
for heal in MOCK_HEALING:
|
|
if heal["name"] == name:
|
|
self.heal_cost_edit.set_decimal(heal["cost"])
|
|
self.heal_amount_edit.set_decimal(heal["amount"])
|
|
break
|
|
self.heal_cost_edit.setEnabled(False)
|
|
self.heal_amount_edit.setEnabled(False)
|
|
self._update_calculations()
|
|
|
|
def _update_calculations(self):
|
|
"""Update all cost and DPP calculations."""
|
|
try:
|
|
config = self._get_current_config()
|
|
|
|
# Update DPP
|
|
dpp = config.calculate_dpp()
|
|
self.dpp_label.setText(f"{dpp:.4f}")
|
|
self.total_dpp_label.setText(f"{dpp:.4f}")
|
|
|
|
# Update cost breakdown
|
|
weapon_cost = config.calculate_weapon_cost_per_hour()
|
|
armor_cost = config.calculate_armor_cost_per_hour()
|
|
heal_cost = config.calculate_heal_cost_per_hour()
|
|
total_cost = config.calculate_total_cost_per_hour()
|
|
|
|
self.weapon_cost_label.setText(f"{weapon_cost:.0f} PEC/hr")
|
|
self.armor_cost_label.setText(f"{armor_cost:.0f} PEC/hr")
|
|
self.heal_cost_label.setText(f"{heal_cost:.0f} PEC/hr")
|
|
self.total_cost_label.setText(f"{total_cost:.2f} PED/hr")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Calculation error: {e}")
|
|
|
|
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 HP: {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."""
|
|
config = LoadoutConfig(
|
|
name=self.loadout_name_edit.text().strip() or "Unnamed",
|
|
weapon_name=self.current_weapon.name if self.current_weapon else (self.weapon_name_label.text() if self.weapon_name_label.text() != "No weapon selected" else "-- Custom --"),
|
|
weapon_id=self.current_weapon.id if self.current_weapon else 0,
|
|
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.current_armor.name if self.current_armor else (self.armor_name_label.text() if self.armor_name_label.text() != "No armor selected" else "-- None --"),
|
|
armor_id=self.current_armor.id if self.current_armor else 0,
|
|
armor_decay_pec=self.armor_decay_edit.get_decimal(),
|
|
heal_name=self.heal_combo.currentText(),
|
|
heal_cost_pec=self.heal_cost_edit.get_decimal(),
|
|
heal_amount=self.heal_amount_edit.get_decimal(),
|
|
shots_per_hour=self.shots_per_hour_spin.value(),
|
|
hits_per_hour=self.hits_per_hour_spin.value(),
|
|
heals_per_hour=self.heals_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(),
|
|
)
|
|
|
|
# Preserve existing attachments
|
|
if self.current_loadout:
|
|
config.weapon_amplifier = self.current_loadout.weapon_amplifier
|
|
config.weapon_scope = self.current_loadout.weapon_scope
|
|
config.weapon_absorber = self.current_loadout.weapon_absorber
|
|
config.armor_plating = self.current_loadout.armor_plating
|
|
|
|
return config
|
|
|
|
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.hits_per_hour_spin.setValue(config.hits_per_hour)
|
|
self.heals_per_hour_spin.setValue(config.heals_per_hour)
|
|
|
|
# Weapon
|
|
self.weapon_name_label.setText(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)
|
|
|
|
# Weapon attachments
|
|
if config.weapon_amplifier:
|
|
self.amp_label.setText(f"{config.weapon_amplifier.name} (+{config.weapon_amplifier.damage_bonus} dmg)")
|
|
else:
|
|
self.amp_label.setText("None")
|
|
|
|
if config.weapon_scope:
|
|
self.scope_label.setText(f"{config.weapon_scope.name} (+{config.weapon_scope.range_bonus}m)")
|
|
else:
|
|
self.scope_label.setText("None")
|
|
|
|
if config.weapon_absorber:
|
|
self.absorber_label.setText(config.weapon_absorber.name)
|
|
else:
|
|
self.absorber_label.setText("None")
|
|
|
|
# Armor
|
|
self.armor_name_label.setText(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)
|
|
|
|
# Armor plating
|
|
if config.armor_plating:
|
|
self.plating_label.setText(config.armor_plating.name)
|
|
else:
|
|
self.plating_label.setText("None")
|
|
|
|
# Healing
|
|
self.heal_combo.setCurrentText(config.heal_name)
|
|
self.heal_cost_edit.set_decimal(config.heal_cost_pec)
|
|
self.heal_amount_edit.set_decimal(config.heal_amount)
|
|
|
|
# Store config
|
|
self.current_loadout = config
|
|
|
|
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))
|
|
|
|
# Build tooltip
|
|
dpp = config.calculate_dpp()
|
|
cost = config.calculate_total_cost_per_hour()
|
|
tooltip = (
|
|
f"Weapon: {config.weapon_name}\n"
|
|
f"Armor: {config.armor_name}\n"
|
|
f"Total DPP: {dpp:.3f}\n"
|
|
f"Cost/hr: {cost:.2f} PED"
|
|
)
|
|
if config.weapon_amplifier:
|
|
tooltip += f"\nAmplifier: {config.weapon_amplifier.name}"
|
|
|
|
item.setToolTip(tooltip)
|
|
self.saved_list.addItem(item)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load {filepath}: {e}")
|
|
continue
|
|
except Exception as e:
|
|
logger.error(f"Failed to list loadouts: {e}")
|
|
|
|
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)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to load: {str(e)}")
|
|
|
|
def _use_selected(self):
|
|
"""Use the selected loadout for the current session."""
|
|
item = self.saved_list.currentItem()
|
|
if not item:
|
|
QMessageBox.information(self, "No Selection", "Please select a loadout to use")
|
|
return
|
|
|
|
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.current_loadout = config
|
|
# Emit signal with the loadout for main window to use
|
|
self.loadout_selected.emit(config)
|
|
|
|
QMessageBox.information(self, "Loadout Selected",
|
|
f"Loadout '{config.name}' is now active for your session.\n\n"
|
|
f"Weapon: {config.weapon_name}\n"
|
|
f"Healing Tool: {config.heal_name}\n"
|
|
f"Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
|
|
|
|
self.accept() # Close dialog
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to load loadout: {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_name_label.setText("No weapon selected")
|
|
self.armor_name_label.setText("No armor selected")
|
|
|
|
# Clear fields
|
|
self.weapon_damage_edit.clear()
|
|
self.weapon_decay_edit.clear()
|
|
self.weapon_ammo_edit.clear()
|
|
self.armor_decay_edit.clear()
|
|
self.protection_stab_edit.clear()
|
|
self.protection_cut_edit.clear()
|
|
self.protection_impact_edit.clear()
|
|
self.protection_pen_edit.clear()
|
|
self.protection_shrap_edit.clear()
|
|
self.protection_burn_edit.clear()
|
|
self.protection_cold_edit.clear()
|
|
self.protection_acid_edit.clear()
|
|
self.protection_elec_edit.clear()
|
|
self.heal_cost_edit.clear()
|
|
self.heal_amount_edit.clear()
|
|
|
|
# Reset attachment labels
|
|
self.amp_label.setText("None")
|
|
self.scope_label.setText("None")
|
|
self.absorber_label.setText("None")
|
|
self.plating_label.setText("None")
|
|
|
|
# Reset values
|
|
self.shots_per_hour_spin.setValue(3600)
|
|
self.hits_per_hour_spin.setValue(720)
|
|
self.heals_per_hour_spin.setValue(60)
|
|
self.mob_health_edit.set_decimal(Decimal("100"))
|
|
|
|
# Reset combos
|
|
self.heal_combo.setCurrentIndex(0)
|
|
|
|
# Clear stored objects
|
|
self.current_weapon = None
|
|
self.current_armor = None
|
|
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
|
|
|
|
# Setup logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
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}")
|
|
if config.weapon_amplifier:
|
|
print(f" Amplifier: {config.weapon_amplifier.name}")
|
|
print(f" Total DPP: {config.calculate_dpp():.4f}")
|
|
print(f" Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|