diff --git a/ui/accessories_selector.py b/ui/accessories_selector.py
index 9034d58..086330f 100644
--- a/ui/accessories_selector.py
+++ b/ui/accessories_selector.py
@@ -38,30 +38,32 @@ class AccessoriesLoaderThread(QThread):
class AccessoriesSelectorDialog(QDialog):
"""Dialog for selecting rings, clothing, and pets from Entropia Nexus API."""
-
+
ring_selected = pyqtSignal(NexusRing)
clothing_selected = pyqtSignal(NexusClothing)
pet_selected = pyqtSignal(NexusPet)
-
- def __init__(self, parent=None, initial_tab: str = "rings"):
+
+ def __init__(self, parent=None, initial_tab: str = "rings", slot_filter: str = None):
super().__init__(parent)
self.setWindowTitle("Select Accessories - Entropia Nexus")
self.setMinimumSize(900, 600)
-
+
self.all_rings: List[NexusRing] = []
self.all_clothing: List[NexusClothing] = []
self.all_pets: List[NexusPet] = []
-
+
self.selected_ring: Optional[NexusRing] = None
self.selected_clothing: Optional[NexusClothing] = None
self.selected_pet: Optional[NexusPet] = None
-
+
+ self.slot_filter = slot_filter # "Left Finger" or "Right Finger" or None
+
self._setup_ui()
-
+
# Set initial tab
tab_map = {"rings": 0, "clothing": 1, "pets": 2}
self.tabs.setCurrentIndex(tab_map.get(initial_tab, 0))
-
+
self._load_data()
def _setup_ui(self):
@@ -302,7 +304,12 @@ class AccessoriesSelectorDialog(QDialog):
tree.addTopLevelItem(item)
return
- for ring in self.all_rings:
+ # Filter rings by slot if specified
+ rings_to_show = self.all_rings
+ if self.slot_filter:
+ rings_to_show = [r for r in rings_to_show if r.slot == self.slot_filter]
+
+ for ring in rings_to_show:
item = QTreeWidgetItem()
item.setText(0, ring.name)
# Format effects as string
diff --git a/ui/loadout_manager.py b/ui/loadout_manager.py
index d11e794..edcfc68 100644
--- a/ui/loadout_manager.py
+++ b/ui/loadout_manager.py
@@ -26,7 +26,7 @@ from PyQt6.QtGui import QFont
from core.nexus_api import EntropiaNexusAPI, WeaponStats, ArmorStats, FinderStats
from core.nexus_full_api import (
- get_nexus_api, NexusArmor, NexusHealingTool, NexusPlate,
+ get_nexus_api, NexusArmor, NexusHealingTool, NexusPlate,
NexusAttachment, NexusEnhancer, NexusRing, NexusClothing, NexusPet
)
from core.attachments import (
@@ -60,7 +60,7 @@ class AttachmentConfig:
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,
@@ -72,7 +72,7 @@ class AttachmentConfig:
'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(
@@ -91,7 +91,7 @@ class AttachmentConfig:
class LoadoutConfig:
"""Configuration for a hunting loadout with full armor system."""
name: str
-
+
# Weapon
weapon_name: str
weapon_id: int = 0
@@ -99,22 +99,22 @@ class LoadoutConfig:
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
-
+
# Weapon Enhancers (up to 10 slots)
weapon_enhancers: List[AttachmentConfig] = field(default_factory=list)
-
+
# Armor System
equipped_armor: Optional[EquippedArmor] = None
armor_set_name: str = "-- None --"
-
+
# Armor Plates (per slot)
armor_plates: Dict[str, AttachmentConfig] = field(default_factory=dict)
-
+
# Legacy armor fields for backward compatibility
armor_name: str = "-- None --"
armor_id: int = 0
@@ -128,30 +128,30 @@ class LoadoutConfig:
protection_cold: Decimal = Decimal("0")
protection_acid: Decimal = Decimal("0")
protection_electric: Decimal = Decimal("0")
-
+
# Healing
heal_name: str = "-- Custom --"
heal_cost_pec: Decimal = Decimal("2.0")
heal_amount: Decimal = Decimal("20")
-
+
# Accessories
left_ring: Optional[str] = None
right_ring: Optional[str] = None
clothing_items: List[str] = field(default_factory=list)
pet: Optional[str] = None
-
+
# Settings
shots_per_hour: int = 3600
hits_per_hour: int = 720
heals_per_hour: int = 60
-
+
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 and enhancers."""
total = self.weapon_decay_pec
@@ -165,14 +165,14 @@ class LoadoutConfig:
for enhancer in self.weapon_enhancers:
total += enhancer.decay_pec
return total
-
+
def get_total_ammo_per_shot(self) -> Decimal:
"""Calculate total ammo cost per shot in PEC."""
total = self.weapon_ammo_pec * Decimal("0.01")
if self.weapon_amplifier:
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()
@@ -180,12 +180,12 @@ class LoadoutConfig:
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 plates."""
base_cost = Decimal("0")
@@ -194,37 +194,37 @@ class LoadoutConfig:
else:
# Legacy fallback
base_cost = self.armor_decay_pec * Decimal(self.hits_per_hour)
-
+
# Add plate decay costs
for slot, plate_config in self.armor_plates.items():
base_cost += plate_config.decay_pec * Decimal(self.hits_per_hour)
-
+
return base_cost
-
+
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."""
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")
-
+
def calculate_break_even(self, mob_health: Decimal) -> Decimal:
"""Calculate break-even loot value for a mob."""
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")
-
+
def get_total_protection(self) -> ProtectionProfile:
"""Get total protection from equipped armor."""
if self.equipped_armor:
@@ -241,7 +241,7 @@ class LoadoutConfig:
acid=self.protection_acid,
electric=self.protection_electric,
)
-
+
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
data = {
@@ -265,7 +265,7 @@ class LoadoutConfig:
if self.equipped_armor:
data['equipped_armor'] = self.equipped_armor.to_dict()
return data
-
+
@classmethod
def from_dict(cls, data: dict) -> "LoadoutConfig":
"""Create LoadoutConfig from dictionary."""
@@ -276,45 +276,45 @@ class LoadoutConfig:
'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
-
+
# Handle weapon enhancers
if 'weapon_enhancers' in data and data['weapon_enhancers']:
data['weapon_enhancers'] = [AttachmentConfig.from_dict(e) for e in data['weapon_enhancers']]
else:
data['weapon_enhancers'] = []
-
+
# Handle armor plates
if 'armor_plates' in data and data['armor_plates']:
data['armor_plates'] = {k: AttachmentConfig.from_dict(v) for k, v in data['armor_plates'].items()}
else:
data['armor_plates'] = {}
-
+
# Handle accessories
if 'clothing_items' not in data:
data['clothing_items'] = []
@@ -324,19 +324,19 @@ class LoadoutConfig:
data['right_ring'] = None
if 'pet' not in data:
data['pet'] = None
-
+
# Handle equipped armor
if 'equipped_armor' in data and data['equipped_armor']:
data['equipped_armor'] = EquippedArmor.from_dict(data['equipped_armor'])
else:
data['equipped_armor'] = None
-
+
# Handle legacy configs
if 'heal_name' not in data:
data['heal_name'] = '-- Custom --'
if 'armor_set_name' not in data:
data['armor_set_name'] = '-- None --'
-
+
return cls(**data)
@@ -378,11 +378,11 @@ MOCK_HEALING = get_healing_tools_data()
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()
@@ -392,7 +392,7 @@ class DecimalLineEdit(QLineEdit):
return Decimal(text)
except InvalidOperation:
return Decimal("0")
-
+
def set_decimal(self, value: Decimal):
"""Set value from Decimal."""
self.setText(str(value))
@@ -400,7 +400,7 @@ class DecimalLineEdit(QLineEdit):
class DarkGroupBox(QGroupBox):
"""Group box with dark theme styling."""
-
+
def __init__(self, title: str, parent=None):
super().__init__(title, parent)
self.setStyleSheet("""
@@ -422,66 +422,66 @@ class DarkGroupBox(QGroupBox):
class ArmorSlotWidget(QWidget):
"""Widget for configuring a single armor slot with piece and plate."""
-
+
piece_changed = pyqtSignal()
plate_changed = pyqtSignal()
-
+
def __init__(self, slot: ArmorSlot, parent=None):
super().__init__(parent)
self.slot = slot
self.current_piece: Optional[ArmorPiece] = None
self.current_plate: Optional[ArmorPlate] = None
self._setup_ui()
-
+
def _setup_ui(self):
layout = QHBoxLayout(self)
layout.setContentsMargins(5, 2, 5, 2)
layout.setSpacing(10)
-
+
slot_name = self._get_slot_display_name()
-
+
# Slot label
self.slot_label = QLabel(f"{slot_name}:")
self.slot_label.setFixedWidth(100)
layout.addWidget(self.slot_label)
-
+
# Armor piece selector
self.piece_combo = QComboBox()
self.piece_combo.setMinimumWidth(180)
self.piece_combo.currentTextChanged.connect(self._on_piece_changed)
layout.addWidget(self.piece_combo)
-
+
# Protection display
self.protection_label = QLabel("-")
self.protection_label.setStyleSheet("color: #888888; font-size: 11px;")
self.protection_label.setFixedWidth(120)
layout.addWidget(self.protection_label)
-
+
# Plate selector
self.plate_combo = QComboBox()
self.plate_combo.setMinimumWidth(150)
self.plate_combo.currentTextChanged.connect(self._on_plate_changed)
layout.addWidget(self.plate_combo)
-
+
# Add plate search button
self.search_plate_btn = QPushButton("🔍")
self.search_plate_btn.setToolTip("Search plates from Nexus API")
self.search_plate_btn.setFixedWidth(40)
self.search_plate_btn.clicked.connect(self._on_search_plate)
layout.addWidget(self.search_plate_btn)
-
+
# Total protection
self.total_label = QLabel("Total: 0")
self.total_label.setStyleSheet("color: #4caf50; font-weight: bold;")
self.total_label.setFixedWidth(80)
layout.addWidget(self.total_label)
-
+
layout.addStretch()
-
+
# Populate combos
self._populate_pieces()
self._populate_plates()
-
+
def _get_slot_display_name(self) -> str:
"""Get human-readable slot name (matches Entropia Nexus)."""
names = {
@@ -494,32 +494,32 @@ class ArmorSlotWidget(QWidget):
ArmorSlot.FEET: "Feet",
}
return names.get(self.slot, self.slot.value)
-
+
def _populate_pieces(self):
"""Populate armor piece combo."""
self.piece_combo.clear()
self.piece_combo.addItem("-- Empty --")
-
+
# Get pieces for this slot
pieces = get_pieces_by_slot(self.slot)
for piece in pieces:
display = f"{piece.name} ({piece.set_name})"
self.piece_combo.addItem(display, piece)
-
+
def _populate_plates(self):
"""Populate plate combo."""
self.plate_combo.clear()
self.plate_combo.addItem("-- No Plate --")
-
+
plates = get_mock_plates()
for plate in plates:
display = f"{plate.name} (+{plate.get_total_protection()})"
self.plate_combo.addItem(display, plate)
-
+
def _on_search_plate(self):
"""Open plate selector dialog from Nexus API."""
from ui.plate_selector import PlateSelectorDialog
-
+
# Get current piece's protection to suggest matching plates
preferred_type = ""
if self.current_piece:
@@ -532,11 +532,11 @@ class ArmorSlotWidget(QWidget):
'cold': self.current_piece.protection.cold,
}
preferred_type = max(protections, key=protections.get)
-
+
dialog = PlateSelectorDialog(self, damage_type=preferred_type)
dialog.plate_selected.connect(self._on_api_plate_selected)
dialog.exec()
-
+
def _on_api_plate_selected(self, plate: NexusPlate):
"""Handle plate selection from API."""
# Add to combo if not exists
@@ -554,10 +554,10 @@ class ArmorSlotWidget(QWidget):
display = f"{plate.name} (+{plate.protection_impact + plate.protection_cut + plate.protection_stab} prot)"
self.plate_combo.addItem(display, local_plate)
index = self.plate_combo.count() - 1
-
+
self.plate_combo.setCurrentIndex(index)
self._update_total()
-
+
def _on_piece_changed(self, text: str):
"""Handle armor piece selection."""
if text == "-- Empty --":
@@ -568,70 +568,70 @@ class ArmorSlotWidget(QWidget):
if self.current_piece:
prot = format_protection(self.current_piece.protection)
self.protection_label.setText(prot)
-
+
self._update_total()
self.piece_changed.emit()
-
+
def _on_plate_changed(self, text: str):
"""Handle plate selection."""
if text == "-- No Plate --":
self.current_plate = None
else:
self.current_plate = self.plate_combo.currentData()
-
+
self._update_total()
self.plate_changed.emit()
-
+
def _update_total(self):
"""Update total protection display."""
total = Decimal("0")
-
+
if self.current_piece:
total += self.current_piece.protection.get_total()
-
+
if self.current_plate:
total += self.current_plate.get_total_protection()
-
+
self.total_label.setText(f"Total: {total}")
-
+
def get_piece(self) -> Optional[ArmorPiece]:
"""Get selected armor piece."""
return self.current_piece
-
+
def get_plate(self) -> Optional[ArmorPlate]:
"""Get selected plate."""
return self.current_plate
-
+
def set_piece(self, piece: Optional[ArmorPiece]):
"""Set selected armor piece."""
if piece is None:
self.piece_combo.setCurrentIndex(0)
return
-
+
# Find and select the piece
for i in range(self.piece_combo.count()):
data = self.piece_combo.itemData(i)
if data and data.item_id == piece.item_id:
self.piece_combo.setCurrentIndex(i)
return
-
+
self.piece_combo.setCurrentIndex(0)
-
+
def set_plate(self, plate: Optional[ArmorPlate]):
"""Set selected plate."""
if plate is None:
self.plate_combo.setCurrentIndex(0)
return
-
+
# Find and select the plate
for i in range(self.plate_combo.count()):
data = self.plate_combo.itemData(i)
if data and data.item_id == plate.item_id:
self.plate_combo.setCurrentIndex(i)
return
-
+
self.plate_combo.setCurrentIndex(0)
-
+
def get_total_protection(self) -> ProtectionProfile:
"""Get total protection for this slot."""
total = ProtectionProfile()
@@ -640,23 +640,23 @@ class ArmorSlotWidget(QWidget):
if self.current_plate:
total = total.add(self.current_plate.protection)
return total
-
+
def get_total_decay(self) -> Decimal:
"""Get total decay per hit for this slot (estimated)."""
# Estimate based on typical hit of 10 hp
typical_hit = Decimal("10")
decay = Decimal("0")
-
+
if self.current_piece:
# Armor only decays for damage it actually absorbs
armor_absorb = min(typical_hit, self.current_piece.protection.get_total())
decay += self.current_piece.get_decay_for_damage(armor_absorb)
-
+
if self.current_plate:
# Plate only decays for damage it actually absorbs
plate_absorb = min(typical_hit, self.current_plate.get_total_protection())
decay += self.current_plate.get_decay_for_damage(plate_absorb)
-
+
return decay
@@ -668,7 +668,7 @@ class WeaponLoaderThread(QThread):
"""Thread to load weapons from API."""
weapons_loaded = pyqtSignal(list)
error_occurred = pyqtSignal(str)
-
+
def run(self):
try:
api = EntropiaNexusAPI()
@@ -683,7 +683,7 @@ class ArmorLoaderThread(QThread):
"""Thread to load armors from API."""
armors_loaded = pyqtSignal(list)
error_occurred = pyqtSignal(str)
-
+
def run(self):
try:
api = EntropiaNexusAPI()
@@ -700,9 +700,9 @@ class ArmorLoaderThread(QThread):
class WeaponSelectorDialog(QDialog):
"""Dialog for selecting weapons from Entropia Nexus API."""
-
+
weapon_selected = pyqtSignal(object)
-
+
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Weapon - Entropia Nexus")
@@ -710,17 +710,17 @@ class WeaponSelectorDialog(QDialog):
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)
-
+
self.status_label = QLabel("Loading weapons from Entropia Nexus...")
layout.addWidget(self.status_label)
-
+
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit()
@@ -731,7 +731,7 @@ class WeaponSelectorDialog(QDialog):
self.search_btn.clicked.connect(self._on_search)
search_layout.addWidget(self.search_btn)
layout.addLayout(search_layout)
-
+
self.results_tree = QTreeWidget()
self.results_tree.setHeaderLabels([
"Name", "Type", "Category", "Damage", "DPP", "Decay", "Ammo", "Cost/h"
@@ -747,17 +747,17 @@ class WeaponSelectorDialog(QDialog):
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)
-
+
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)
-
+
button_box = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
@@ -767,29 +767,29 @@ class WeaponSelectorDialog(QDialog):
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])
-
+
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,
@@ -803,18 +803,18 @@ class WeaponSelectorDialog(QDialog):
])
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()
@@ -826,12 +826,12 @@ class WeaponSelectorDialog(QDialog):
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)))
@@ -841,11 +841,11 @@ class WeaponSelectorDialog(QDialog):
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:
@@ -859,9 +859,9 @@ class WeaponSelectorDialog(QDialog):
class ArmorSelectorDialog(QDialog):
"""Dialog for selecting armors from Entropia Nexus API."""
-
+
armor_selected = pyqtSignal(object)
-
+
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Armor - Entropia Nexus")
@@ -869,17 +869,17 @@ class ArmorSelectorDialog(QDialog):
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)
-
+
self.status_label = QLabel("Loading armors from Entropia Nexus...")
layout.addWidget(self.status_label)
-
+
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit()
@@ -890,7 +890,7 @@ class ArmorSelectorDialog(QDialog):
self.search_btn.clicked.connect(self._on_search)
search_layout.addWidget(self.search_btn)
layout.addLayout(search_layout)
-
+
self.results_tree = QTreeWidget()
self.results_tree.setHeaderLabels([
"Name", "Type", "Durability", "Impact", "Cut", "Stab", "Burn", "Cold"
@@ -901,14 +901,14 @@ class ArmorSelectorDialog(QDialog):
self.results_tree.itemSelectionChanged.connect(self._on_selection_changed)
self.results_tree.itemDoubleClicked.connect(self._on_double_click)
layout.addWidget(self.results_tree)
-
+
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self._on_accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
-
+
def _load_data(self):
"""Load armors from API."""
try:
@@ -917,7 +917,7 @@ class ArmorSelectorDialog(QDialog):
self.status_label.setText(f"Loaded {len(self.armors)} armors from Entropia Nexus")
except Exception as e:
self.status_label.setText(f"Error loading armors: {e}")
-
+
def _populate_results(self, armors):
"""Populate results tree."""
self.results_tree.clear()
@@ -933,28 +933,28 @@ class ArmorSelectorDialog(QDialog):
item.setText(7, str(armor.protection_cold))
item.setData(0, Qt.ItemDataRole.UserRole, armor)
self.results_tree.addTopLevelItem(item)
-
+
def _on_search(self):
"""Handle search."""
query = self.search_input.text().lower()
if not query:
self._populate_results(self.armors)
return
-
+
filtered = [a for a in self.armors if query in a.name.lower()]
self._populate_results(filtered)
self.status_label.setText(f"Found {len(filtered)} armors matching '{query}'")
-
+
def _on_selection_changed(self):
"""Handle selection change."""
items = self.results_tree.selectedItems()
if items:
self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole)
-
+
def _on_double_click(self, item, column):
"""Handle double click."""
self._on_accept()
-
+
def _on_accept(self):
"""Handle OK button."""
if self.selected_armor:
@@ -968,28 +968,30 @@ class ArmorSelectorDialog(QDialog):
class LoadoutManagerDialog(QDialog):
"""Main dialog for managing hunting loadouts with full armor system."""
-
+
loadout_saved = pyqtSignal(object)
-
+
def __init__(self, parent=None, config_dir: Optional[str] = None):
super().__init__(parent)
self.setWindowTitle("Lemontropia Suite - Loadout Manager v3.0")
self.setMinimumSize(1100, 900)
-
+
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_set: Optional[ArmorSet] = None
self.equipped_armor: Optional[EquippedArmor] = None
-
+ self.current_left_ring: Optional[NexusRing] = None
+ self.current_right_ring: Optional[NexusRing] = None
+
# Armor slot widgets
self.slot_widgets: Dict[ArmorSlot, ArmorSlotWidget] = {}
-
+
self._apply_dark_theme()
self._create_widgets()
self._create_layout()
@@ -997,7 +999,7 @@ class LoadoutManagerDialog(QDialog):
self._load_saved_loadouts()
self._populate_armor_sets()
self._populate_healing_data()
-
+
def _apply_dark_theme(self):
"""Apply dark theme styling."""
self.setStyleSheet("""
@@ -1105,42 +1107,42 @@ class LoadoutManagerDialog(QDialog):
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")
@@ -1154,35 +1156,35 @@ class LoadoutManagerDialog(QDialog):
self.remove_amp_btn.setFixedWidth(30)
self.remove_scope_btn.setFixedWidth(30)
self.remove_absorber_btn.setFixedWidth(30)
-
+
# Armor section - NEW COMPLETE SYSTEM
self.armor_group = DarkGroupBox("🛡️ Armor Configuration")
-
+
# Armor set selector
self.armor_set_combo = QComboBox()
self.armor_set_combo.setMinimumWidth(250)
-
+
self.equip_set_btn = QPushButton("Equip Full Set")
self.equip_set_btn.setObjectName("selectButton")
self.clear_armor_btn = QPushButton("Clear All")
self.clear_armor_btn.setObjectName("clearButton")
-
+
# Armor protection summary
self.armor_summary_label = QLabel("No armor equipped")
self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;")
-
+
# Create slot widgets
for slot in ALL_ARMOR_SLOTS:
self.slot_widgets[slot] = ArmorSlotWidget(slot)
self.slot_widgets[slot].piece_changed.connect(self._on_armor_changed)
self.slot_widgets[slot].plate_changed.connect(self._on_armor_changed)
-
+
# 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")
@@ -1192,21 +1194,21 @@ class LoadoutManagerDialog(QDialog):
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;")
-
+
# Protection summary
self.protection_summary_label = QLabel("No protection")
self.protection_summary_label.setStyleSheet("color: #4a90d9; font-size: 12px;")
-
+
# 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")
@@ -1216,43 +1218,43 @@ class LoadoutManagerDialog(QDialog):
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.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:")
@@ -1260,7 +1262,7 @@ class LoadoutManagerDialog(QDialog):
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)
@@ -1271,20 +1273,20 @@ class LoadoutManagerDialog(QDialog):
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)
@@ -1294,33 +1296,33 @@ class LoadoutManagerDialog(QDialog):
self.attach_amp_btn.clicked.connect(lambda: self._on_select_attachment("amplifier"))
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)
self.attach_scope_btn.setText("🔍 Search Scopes")
self.attach_scope_btn.clicked.connect(lambda: self._on_select_attachment("scope"))
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)
self.attach_absorber_btn.setText("🔍 Search Absorbers")
self.attach_absorber_btn.clicked.connect(lambda: self._on_select_attachment("absorber"))
attachments_layout.addWidget(self.attach_absorber_btn, 2, 2)
attachments_layout.addWidget(self.remove_absorber_btn, 2, 3)
-
+
# Add enhancer selection button
self.select_enhancer_btn = QPushButton("✨ Select Enhancers")
self.select_enhancer_btn.setObjectName("selectButton")
self.select_enhancer_btn.clicked.connect(self._on_select_enhancer)
attachments_layout.addWidget(self.select_enhancer_btn, 3, 0, 1, 4)
-
+
weapon_layout.addRow("Attachments:", attachments_frame)
right_layout.addWidget(self.weapon_group)
-
+
# Armor configuration - COMPLETE SYSTEM
armor_layout = QVBoxLayout(self.armor_group)
-
+
# Armor set selection row
set_layout = QHBoxLayout()
set_layout.addWidget(QLabel("Armor Set:"))
@@ -1328,63 +1330,97 @@ class LoadoutManagerDialog(QDialog):
set_layout.addWidget(self.equip_set_btn)
set_layout.addWidget(self.clear_armor_btn)
armor_layout.addLayout(set_layout)
-
+
# Add API armor selector button
self.select_armor_api_btn = QPushButton("🔍 Search Entropia Nexus Armors")
self.select_armor_api_btn.setObjectName("selectButton")
self.select_armor_api_btn.clicked.connect(self._on_select_armor_from_api)
armor_layout.addWidget(self.select_armor_api_btn)
-
+
# Armor summary
armor_layout.addWidget(self.armor_summary_label)
-
+
# Separator
separator = QFrame()
separator.setFrameShape(QFrame.Shape.HLine)
separator.setStyleSheet("background-color: #3d3d3d;")
separator.setFixedHeight(2)
armor_layout.addWidget(separator)
-
+
# Individual slot widgets
slots_label = QLabel("Individual Pieces & Plates:")
slots_label.setStyleSheet("padding-top: 10px;")
armor_layout.addWidget(slots_label)
-
+
for slot in ALL_ARMOR_SLOTS:
armor_layout.addWidget(self.slot_widgets[slot])
-
+
right_layout.addWidget(self.armor_group)
-
+
# Healing configuration
heal_layout = QFormLayout(self.heal_group)
-
+
# Add healing tool search button
self.select_healing_api_btn = QPushButton("🔍 Search Healing Tools from Nexus")
self.select_healing_api_btn.setObjectName("selectButton")
self.select_healing_api_btn.clicked.connect(self._on_select_healing_from_api)
heal_layout.addRow(self.select_healing_api_btn)
-
+
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)
-
- # Accessories section (NEW)
- self.accessories_group = DarkGroupBox("💍 Accessories (Rings, Clothing, Pets)")
+
+ # Accessories section (Rings, Clothing, Pets)
+ self.accessories_group = DarkGroupBox("💍 Accessories")
accessories_layout = QVBoxLayout(self.accessories_group)
-
- self.select_accessories_btn = QPushButton("🔍 Search Accessories from Nexus")
- self.select_accessories_btn.setObjectName("selectButton")
- self.select_accessories_btn.clicked.connect(self._on_select_accessories)
- accessories_layout.addWidget(self.select_accessories_btn)
-
- # Display selected accessories
- self.accessories_summary = QLabel("No accessories selected")
- self.accessories_summary.setStyleSheet("color: #888888; padding: 5px;")
- accessories_layout.addWidget(self.accessories_summary)
-
+
+ # Rings - Left and Right
+ rings_layout = QHBoxLayout()
+
+ left_ring_layout = QVBoxLayout()
+ left_ring_layout.addWidget(QLabel("Left Ring:"))
+ self.left_ring_label = QLabel("None")
+ self.left_ring_label.setStyleSheet("color: #888888;")
+ left_ring_layout.addWidget(self.left_ring_label)
+ self.select_left_ring_btn = QPushButton("🔍 Select")
+ self.select_left_ring_btn.setObjectName("selectButton")
+ self.select_left_ring_btn.clicked.connect(self._on_select_left_ring)
+ left_ring_layout.addWidget(self.select_left_ring_btn)
+ rings_layout.addLayout(left_ring_layout)
+
+ right_ring_layout = QVBoxLayout()
+ right_ring_layout.addWidget(QLabel("Right Ring:"))
+ self.right_ring_label = QLabel("None")
+ self.right_ring_label.setStyleSheet("color: #888888;")
+ right_ring_layout.addWidget(self.right_ring_label)
+ self.select_right_ring_btn = QPushButton("🔍 Select")
+ self.select_right_ring_btn.setObjectName("selectButton")
+ self.select_right_ring_btn.clicked.connect(self._on_select_right_ring)
+ right_ring_layout.addWidget(self.select_right_ring_btn)
+ rings_layout.addLayout(right_ring_layout)
+
+ accessories_layout.addLayout(rings_layout)
+
+ # Clothing and Pets
+ other_accessories_layout = QHBoxLayout()
+
+ self.select_clothing_btn = QPushButton("👕 Clothing")
+ self.select_clothing_btn.setObjectName("selectButton")
+ self.select_clothing_btn.clicked.connect(self._on_select_clothing)
+ other_accessories_layout.addWidget(self.select_clothing_btn)
+
+ self.select_pet_btn = QPushButton("🐾 Pet")
+ self.select_pet_btn.setObjectName("selectButton")
+ self.select_pet_btn.clicked.connect(self._on_select_pet)
+ other_accessories_layout.addWidget(self.select_pet_btn)
+
+ accessories_layout.addLayout(other_accessories_layout)
+
right_layout.addWidget(self.accessories_group)
-
+
+ right_layout.addWidget(self.accessories_group)
+
# Cost summary
summary_layout = QFormLayout(self.summary_group)
summary_layout.addRow("Weapon Cost:", self.weapon_cost_label)
@@ -1392,33 +1428,33 @@ class LoadoutManagerDialog(QDialog):
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)
-
+
# Protection summary
summary_layout.addRow("Protection:", self.protection_summary_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, 850])
-
+
main_layout.addWidget(splitter)
-
+
def _connect_signals(self):
"""Connect all signal handlers."""
# Weapon selection
@@ -1426,7 +1462,7 @@ class LoadoutManagerDialog(QDialog):
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)
-
+
# Attachments
self.attach_amp_btn.clicked.connect(lambda: self._on_attach("amplifier"))
self.attach_scope_btn.clicked.connect(lambda: self._on_attach("scope"))
@@ -1434,19 +1470,19 @@ class LoadoutManagerDialog(QDialog):
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)
-
+
# Armor
self.equip_set_btn.clicked.connect(self._on_equip_full_set)
self.clear_armor_btn.clicked.connect(self._on_clear_armor)
-
+
# 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.load_btn.clicked.connect(self._load_selected)
@@ -1455,51 +1491,51 @@ class LoadoutManagerDialog(QDialog):
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_armor_sets(self):
"""Populate armor set combo."""
self.armor_set_combo.clear()
self.armor_set_combo.addItem("-- Select a Set --")
-
+
sets = get_all_armor_sets()
for armor_set in sets:
total_prot = armor_set.get_total_protection().get_total()
display = f"{armor_set.name} (Prot: {total_prot})"
self.armor_set_combo.addItem(display, armor_set)
-
+
def _populate_healing_data(self):
"""Populate healing combo with real data from database."""
self.heal_combo.clear()
self.heal_combo.addItem("-- Custom --")
-
+
# Get real healing tools
healing_tools = get_healing_tools_data()
-
+
# Sort by category (chips last)
medical_tools = [h for h in healing_tools if not h.get("is_chip", False)]
chips = [h for h in healing_tools if h.get("is_chip", False)]
-
+
# Add medical tools first
if medical_tools:
self.heal_combo.addItem("--- Medical Tools ---")
for tool in medical_tools:
self.heal_combo.addItem(tool["name"])
-
+
# Add restoration chips
if chips:
self.heal_combo.addItem("--- Restoration Chips ---")
for chip in sorted(chips, key=lambda x: x["amount"]):
self.heal_combo.addItem(chip["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
@@ -1508,37 +1544,37 @@ class LoadoutManagerDialog(QDialog):
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_from_api(self):
"""Open armor selector dialog from Nexus API."""
from ui.armor_selector import ArmorSelectorDialog
dialog = ArmorSelectorDialog(self)
dialog.armor_selected.connect(self._on_api_armor_selected)
dialog.exec()
-
+
def _on_api_armor_selected(self, armor: NexusArmor):
"""Handle armor selection from API."""
# Store selected armor info
self._selected_api_armor = armor
QMessageBox.information(
- self,
- "Armor Selected",
+ self,
+ "Armor Selected",
f"Selected: {armor.name}\n"
f"Durability: {armor.durability}\n"
f"Protection: Impact {armor.protection_impact}, Cut {armor.protection_cut}, Stab {armor.protection_stab}"
)
-
+
def _on_select_healing_from_api(self):
"""Open healing tool selector dialog from Nexus API."""
from ui.healing_selector import HealingSelectorDialog
dialog = HealingSelectorDialog(self)
dialog.tool_selected.connect(self._on_api_healing_selected)
dialog.exec()
-
+
def _on_api_healing_selected(self, tool: NexusHealingTool):
"""Handle healing tool selection from API."""
self._selected_api_healing = tool
-
+
# Update the healing combo to show selected tool
# Find or add the tool to combo
index = self.heal_combo.findText(tool.name)
@@ -1546,13 +1582,13 @@ class LoadoutManagerDialog(QDialog):
self.heal_combo.addItem(tool.name)
index = self.heal_combo.count() - 1
self.heal_combo.setCurrentIndex(index)
-
+
# Update cost and amount fields
self.heal_cost_edit.setText(str(tool.decay))
self.heal_amount_edit.setText(str(tool.heal_amount))
-
+
self._update_calculations()
-
+
QMessageBox.information(
self,
"Healing Tool Selected",
@@ -1560,7 +1596,7 @@ class LoadoutManagerDialog(QDialog):
f"Heal: {tool.heal_amount} HP\n"
f"Decay: {tool.decay:.2f} PEC ({tool.heal_per_pec:.2f} hp/pec)"
)
-
+
def _on_select_attachment(self, attachment_type: str):
"""Open attachment selector dialog from Nexus API."""
from ui.attachment_selector import AttachmentSelectorDialog
@@ -1569,7 +1605,7 @@ class LoadoutManagerDialog(QDialog):
lambda att: self._on_api_attachment_selected(att, attachment_type)
)
dialog.exec()
-
+
def _on_api_attachment_selected(self, attachment: NexusAttachment, att_type: str):
"""Handle attachment selection from API."""
# Update UI based on attachment type
@@ -1579,7 +1615,7 @@ class LoadoutManagerDialog(QDialog):
self.scope_label.setText(f"{attachment.name} (+{attachment.range_bonus} rng)")
elif att_type == "absorber":
self.absorber_label.setText(f"{attachment.name}")
-
+
QMessageBox.information(
self,
"Attachment Selected",
@@ -1589,14 +1625,14 @@ class LoadoutManagerDialog(QDialog):
f"Range: +{attachment.range_bonus}\n"
f"Decay: {attachment.decay:.2f} PEC"
)
-
+
def _on_select_enhancer(self):
"""Open enhancer selector dialog."""
from ui.enhancer_selector import EnhancerSelectorDialog
dialog = EnhancerSelectorDialog(self)
dialog.enhancer_selected.connect(self._on_api_enhancer_selected)
dialog.exec()
-
+
def _on_api_enhancer_selected(self, enhancer: NexusEnhancer):
"""Handle enhancer selection from API."""
QMessageBox.information(
@@ -1608,27 +1644,60 @@ class LoadoutManagerDialog(QDialog):
f"Effect: +{enhancer.effect_value}%\n"
f"Break Chance: {enhancer.break_chance * 100:.1f}%"
)
-
- def _on_select_accessories(self):
- """Open accessories selector dialog (rings, clothing, pets)."""
+
+ def _on_select_left_ring(self):
+ """Open ring selector for left finger."""
from ui.accessories_selector import AccessoriesSelectorDialog
- dialog = AccessoriesSelectorDialog(self)
- dialog.ring_selected.connect(self._on_ring_selected)
+ dialog = AccessoriesSelectorDialog(self, slot_filter="Left Finger")
+ dialog.ring_selected.connect(self._on_left_ring_selected)
+ dialog.exec()
+
+ def _on_select_right_ring(self):
+ """Open ring selector for right finger."""
+ from ui.accessories_selector import AccessoriesSelectorDialog
+ dialog = AccessoriesSelectorDialog(self, slot_filter="Right Finger")
+ dialog.ring_selected.connect(self._on_right_ring_selected)
+ dialog.exec()
+
+ def _on_left_ring_selected(self, ring: NexusRing):
+ """Handle left ring selection."""
+ self.current_left_ring = ring
+ effects_str = ", ".join([f"{k}: {v}" for k, v in ring.effects.items()]) if ring.effects else "No effects"
+ self.left_ring_label.setText(f"{ring.name}\n{effects_str}")
+ self.left_ring_label.setStyleSheet("color: #4caf50;")
+
+ def _on_right_ring_selected(self, ring: NexusRing):
+ """Handle right ring selection."""
+ self.current_right_ring = ring
+ effects_str = ", ".join([f"{k}: {v}" for k, v in ring.effects.items()]) if ring.effects else "No effects"
+ self.right_ring_label.setText(f"{ring.name}\n{effects_str}")
+ self.right_ring_label.setStyleSheet("color: #4caf50;")
+
+ def _on_select_clothing(self):
+ """Open clothing selector."""
+ from ui.accessories_selector import AccessoriesSelectorDialog
+ dialog = AccessoriesSelectorDialog(self, initial_tab="clothing")
dialog.clothing_selected.connect(self._on_clothing_selected)
+ dialog.exec()
+
+ def _on_select_pet(self):
+ """Open pet selector."""
+ from ui.accessories_selector import AccessoriesSelectorDialog
+ dialog = AccessoriesSelectorDialog(self, initial_tab="pets")
dialog.pet_selected.connect(self._on_pet_selected)
dialog.exec()
-
+
+ def _on_select_accessories(self):
+ """Open accessories selector dialog (rings, clothing, pets) - legacy."""
+ self._on_select_left_ring()
+
def _on_ring_selected(self, ring: NexusRing):
- """Handle ring selection."""
- effects_str = ", ".join([f"{k}: {v}" for k, v in ring.effects.items()]) if ring.effects else "No effects"
- QMessageBox.information(
- self,
- "Ring Selected",
- f"Selected: {ring.name}\n"
- f"Effects: {effects_str}\n"
- f"Slot: {ring.slot}"
- )
-
+ """Handle ring selection - legacy, routes to appropriate slot."""
+ if ring.slot == "Left Finger":
+ self._on_left_ring_selected(ring)
+ else:
+ self._on_right_ring_selected(ring)
+
def _on_clothing_selected(self, clothing: NexusClothing):
"""Handle clothing selection."""
buffs = ", ".join([f"{k}:{v}" for k, v in clothing.buffs.items()])
@@ -1639,7 +1708,7 @@ class LoadoutManagerDialog(QDialog):
f"Slot: {clothing.slot}\n"
f"Buffs: {buffs if buffs else 'None'}"
)
-
+
def _on_pet_selected(self, pet: NexusPet):
"""Handle pet selection."""
QMessageBox.information(
@@ -1649,11 +1718,11 @@ class LoadoutManagerDialog(QDialog):
f"Effect: {pet.effect_type} {pet.effect_value}\n"
f"Level Required: {pet.level_required if pet.level_required > 0 else 'None'}"
)
-
+
def _on_attach(self, attachment_type: str):
"""Handle attachment selection (legacy - now uses API)."""
self._on_select_attachment(attachment_type)
-
+
def _apply_attachment(self, attachment_type: str, att):
"""Apply selected attachment."""
if attachment_type == "amplifier":
@@ -1662,9 +1731,9 @@ class LoadoutManagerDialog(QDialog):
self.scope_label.setText(f"{att.name} (+{att.range_increase}m)")
elif attachment_type == "absorber":
self.absorber_label.setText(f"{att.name} (-{att.damage_reduction} dmg)")
-
+
self._update_calculations()
-
+
def _clear_attachment(self, attachment_type: str):
"""Clear an attachment."""
if attachment_type == "amplifier":
@@ -1674,59 +1743,59 @@ class LoadoutManagerDialog(QDialog):
elif attachment_type == "absorber":
self.absorber_label.setText("None")
self._update_calculations()
-
+
def _on_remove_amp(self):
"""Remove amplifier."""
self.amp_label.setText("None")
self._update_calculations()
-
+
def _on_remove_scope(self):
"""Remove scope."""
self.scope_label.setText("None")
self._update_calculations()
-
+
def _on_remove_absorber(self):
"""Remove absorber."""
self.absorber_label.setText("None")
self._update_calculations()
-
+
def _on_equip_full_set(self):
"""Equip a full armor set."""
if self.armor_set_combo.currentIndex() <= 0:
QMessageBox.information(self, "No Selection", "Please select an armor set first.")
return
-
+
armor_set = self.armor_set_combo.currentData()
if not armor_set:
return
-
+
# Clear any individual pieces
for widget in self.slot_widgets.values():
widget.set_piece(None)
widget.set_plate(None)
-
+
# Equip set pieces
for slot, piece in armor_set.pieces.items():
if slot in self.slot_widgets:
self.slot_widgets[slot].set_piece(piece)
-
+
self.current_armor_set = armor_set
self._update_armor_summary()
self._update_calculations()
-
+
QMessageBox.information(self, "Set Equipped", f"Equipped {armor_set.name}")
-
+
def _on_clear_armor(self):
"""Clear all armor."""
for widget in self.slot_widgets.values():
widget.set_piece(None)
widget.set_plate(None)
-
+
self.current_armor_set = None
self.armor_set_combo.setCurrentIndex(0)
self._update_armor_summary()
self._update_calculations()
-
+
def _on_armor_changed(self):
"""Handle armor piece or plate change."""
# If individual pieces are changed, we're no longer using a pure full set
@@ -1740,20 +1809,20 @@ class LoadoutManagerDialog(QDialog):
if not current or current.item_id != piece.item_id:
all_match = False
break
-
+
if not all_match:
self.current_armor_set = None
-
+
self._update_armor_summary()
self._update_calculations()
-
+
def _update_armor_summary(self):
"""Update armor summary display."""
equipped_count = 0
for widget in self.slot_widgets.values():
if widget.get_piece():
equipped_count += 1
-
+
if equipped_count == 0:
self.armor_summary_label.setText("No armor equipped")
self.armor_summary_label.setStyleSheet("color: #888888; padding: 5px;")
@@ -1767,7 +1836,7 @@ class LoadoutManagerDialog(QDialog):
else:
self.armor_summary_label.setText(f"⚠ {equipped_count}/7 pieces equipped")
self.armor_summary_label.setStyleSheet("color: #ff9800; padding: 5px;")
-
+
def _on_heal_changed(self, name: str):
"""Handle healing selection change."""
if name == "-- Custom --":
@@ -1784,28 +1853,28 @@ class LoadoutManagerDialog(QDialog):
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")
-
+
# Update protection summary
protection = config.get_total_protection()
prot_text = format_protection(protection)
@@ -1813,27 +1882,27 @@ class LoadoutManagerDialog(QDialog):
self.protection_summary_label.setText("No protection")
else:
self.protection_summary_label.setText(f"Total: {protection.get_total()} | {prot_text}")
-
+
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."""
# Build equipped armor from slot widgets
@@ -1862,7 +1931,7 @@ class LoadoutManagerDialog(QDialog):
durability=piece.durability,
weight=piece.weight,
)
-
+
# Attach plate if selected
plate = widget.get_plate()
if plate:
@@ -1884,13 +1953,13 @@ class LoadoutManagerDialog(QDialog):
durability=plate.durability,
)
piece_copy.attach_plate(plate_copy)
-
+
equipped.equip_piece(piece_copy)
-
+
# Set full set if all pieces match
if self.current_armor_set:
equipped.equip_full_set(self.current_armor_set)
-
+
return 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 --"),
@@ -1907,30 +1976,30 @@ class LoadoutManagerDialog(QDialog):
hits_per_hour=self.hits_per_hour_spin.value(),
heals_per_hour=self.heals_per_hour_spin.value(),
)
-
+
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 (simplified - just labels)
self.amp_label.setText("None")
self.scope_label.setText("None")
self.absorber_label.setText("None")
-
+
# Armor - use equipped_armor if available
if config.equipped_armor:
self.equipped_armor = config.equipped_armor
pieces = config.equipped_armor.get_all_pieces()
-
+
for slot, widget in self.slot_widgets.items():
piece = pieces.get(slot)
widget.set_piece(piece)
@@ -1938,7 +2007,7 @@ class LoadoutManagerDialog(QDialog):
widget.set_plate(piece.attached_plate)
else:
widget.set_plate(None)
-
+
# Check if it's a full set
if config.equipped_armor.full_set:
self.current_armor_set = config.equipped_armor.full_set
@@ -1954,61 +2023,61 @@ class LoadoutManagerDialog(QDialog):
else:
# Legacy or empty
self._on_clear_armor()
-
+
self._update_armor_summary()
-
+
# 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
-
+
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(config)
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()
@@ -2025,7 +2094,7 @@ class LoadoutManagerDialog(QDialog):
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()
@@ -2033,39 +2102,39 @@ class LoadoutManagerDialog(QDialog):
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 _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)
@@ -2073,45 +2142,45 @@ class LoadoutManagerDialog(QDialog):
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")
-
+
# Clear weapon
self.weapon_damage_edit.clear()
self.weapon_decay_edit.clear()
self.weapon_ammo_edit.clear()
-
+
# Clear attachments
self.amp_label.setText("None")
self.scope_label.setText("None")
self.absorber_label.setText("None")
-
+
# Clear armor
self._on_clear_armor()
-
+
# Clear healing
self.heal_cost_edit.clear()
self.heal_amount_edit.clear()
-
+
# 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_set = 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
@@ -2124,22 +2193,22 @@ class LoadoutManagerDialog(QDialog):
def main():
"""Run the loadout manager as a standalone application."""
import sys
-
+
# 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 cfg: print(f"Loadout saved: {cfg.name}"))
-
+
if dialog.exec() == QDialog.DialogCode.Accepted:
config = dialog.get_current_loadout()
if config:
@@ -2152,7 +2221,7 @@ def main():
print(f" Total DPP: {config.calculate_dpp():.4f}")
print(f" Total Cost: {config.calculate_total_cost_per_hour():.2f} PED/hr")
print(f" Protection: {format_protection(config.get_total_protection())}")
-
+
sys.exit(0)