From 2183b79ce884d74a53e45bc7b220378a3c6da42b Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Mon, 9 Feb 2026 14:46:35 +0000 Subject: [PATCH] feat(ui): add separate Left/Right ring slots in loadout manager - Split accessories section into Left Ring and Right Ring selectors - Added Clothing and Pet buttons - Updated AccessoriesSelectorDialog to accept slot_filter parameter - Filter rings by Left Finger/Right Finger slot when selecting - Store current_left_ring and current_right_ring in loadout - Display ring effects (multi-effect support) in the UI Ring effects like 'Decreased Critical Damage Taken 4%, Increased Dodge Chance 4%' are now properly displayed. --- ui/accessories_selector.py | 25 +- ui/loadout_manager.py | 731 ++++++++++++++++++++----------------- 2 files changed, 416 insertions(+), 340 deletions(-) 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)