diff --git a/ui/armor_selector.py b/ui/armor_selector.py index e3aa2b7..a9929f5 100644 --- a/ui/armor_selector.py +++ b/ui/armor_selector.py @@ -7,12 +7,12 @@ from decimal import Decimal from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, - QProgressBar, QGroupBox, QFormLayout + QProgressBar, QGroupBox, QFormLayout, QTabWidget, QWidget ) from PyQt6.QtCore import Qt, QThread, pyqtSignal from typing import Optional, List -from core.nexus_full_api import get_nexus_api, NexusArmor +from core.nexus_full_api import get_nexus_api, NexusArmor, NexusArmorSet class ArmorLoaderThread(QThread): @@ -29,10 +29,25 @@ class ArmorLoaderThread(QThread): self.error_occurred.emit(str(e)) +class ArmorSetLoaderThread(QThread): + """Background thread for loading armor sets from API.""" + armor_sets_loaded = pyqtSignal(list) + error_occurred = pyqtSignal(str) + + def run(self): + try: + api = get_nexus_api() + armor_sets = api.get_all_armor_sets() + self.armor_sets_loaded.emit(armor_sets) + except Exception as e: + self.error_occurred.emit(str(e)) + + class ArmorSelectorDialog(QDialog): - """Dialog for selecting armors from Entropia Nexus API with search.""" + """Dialog for selecting armors from Entropia Nexus API with search and sets.""" armor_selected = pyqtSignal(NexusArmor) + armor_set_selected = pyqtSignal(NexusArmorSet) # New signal for full sets def __init__(self, parent=None): super().__init__(parent) @@ -40,7 +55,9 @@ class ArmorSelectorDialog(QDialog): self.setMinimumSize(1000, 700) self.all_armors: List[NexusArmor] = [] + self.all_armor_sets: List[NexusArmorSet] = [] self.selected_armor: Optional[NexusArmor] = None + self.selected_armor_set: Optional[NexusArmorSet] = None self._setup_ui() self._load_data() @@ -49,7 +66,7 @@ class ArmorSelectorDialog(QDialog): layout = QVBoxLayout(self) layout.setSpacing(10) - # Status and search + # Status self.status_label = QLabel("Loading armors from Entropia Nexus...") layout.addWidget(self.status_label) @@ -57,20 +74,27 @@ class ArmorSelectorDialog(QDialog): self.progress.setRange(0, 0) # Indeterminate layout.addWidget(self.progress) + # Tab widget for Individual Pieces vs Full Sets + self.tabs = QTabWidget() + + # === INDIVIDUAL PIECES TAB === + self.tab_individual = QWidget() + individual_layout = QVBoxLayout(self.tab_individual) + # Search search_layout = QHBoxLayout() search_layout.addWidget(QLabel("Search:")) self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Type to search armors...") + self.search_input.setPlaceholderText("Type to search individual armor pieces...") self.search_input.textChanged.connect(self._on_search) search_layout.addWidget(self.search_input) self.clear_btn = QPushButton("Clear") self.clear_btn.clicked.connect(self._clear_search) search_layout.addWidget(self.clear_btn) - layout.addLayout(search_layout) + individual_layout.addLayout(search_layout) - # Results tree + # Results tree for individual pieces self.results_tree = QTreeWidget() self.results_tree.setHeaderLabels([ "Name", "Type", "Durability", @@ -82,9 +106,45 @@ class ArmorSelectorDialog(QDialog): header.setStretchLastSection(False) self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) self.results_tree.itemDoubleClicked.connect(self._on_double_click) - layout.addWidget(self.results_tree) + individual_layout.addWidget(self.results_tree) - # Preview panel + self.tabs.addTab(self.tab_individual, "🛡️ Individual Pieces") + + # === FULL SETS TAB === + self.tab_sets = QWidget() + sets_layout = QVBoxLayout(self.tab_sets) + + # Search for sets + set_search_layout = QHBoxLayout() + set_search_layout.addWidget(QLabel("Search:")) + self.set_search_input = QLineEdit() + self.set_search_input.setPlaceholderText("Type to search armor sets (e.g., 'Ghost', 'Shogun')...") + self.set_search_input.textChanged.connect(self._on_set_search) + set_search_layout.addWidget(self.set_search_input) + + self.set_clear_btn = QPushButton("Clear") + self.set_clear_btn.clicked.connect(self._clear_set_search) + set_search_layout.addWidget(self.set_clear_btn) + sets_layout.addLayout(set_search_layout) + + # Results tree for sets + self.sets_tree = QTreeWidget() + self.sets_tree.setHeaderLabels([ + "Set Name", "Pieces Count", "Total Impact", "Total Cut", "Total Stab", + "Total Burn", "Total Cold", "Total Acid", "Total Electric", "Total Prot" + ]) + sets_header = self.sets_tree.header() + sets_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + sets_header.setStretchLastSection(False) + self.sets_tree.itemSelectionChanged.connect(self._on_set_selection_changed) + self.sets_tree.itemDoubleClicked.connect(self._on_set_double_click) + sets_layout.addWidget(self.sets_tree) + + self.tabs.addTab(self.tab_sets, "⚔️ Full Armor Sets") + + layout.addWidget(self.tabs) + + # Preview panel (shared) self.preview_group = QGroupBox("Armor Preview") preview_layout = QFormLayout(self.preview_group) self.preview_name = QLabel("-") @@ -106,18 +166,28 @@ class ArmorSelectorDialog(QDialog): self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) self.ok_button.setEnabled(False) layout.addWidget(buttons) + + # Update preview when tab changes + self.tabs.currentChanged.connect(self._on_tab_changed) def _load_data(self): - """Load armors in background thread.""" + """Load armors and armor sets in background threads.""" + # Load individual armors self.loader = ArmorLoaderThread() self.loader.armors_loaded.connect(self._on_armors_loaded) self.loader.error_occurred.connect(self._on_load_error) self.loader.start() + + # Load armor sets + self.set_loader = ArmorSetLoaderThread() + self.set_loader.armor_sets_loaded.connect(self._on_armor_sets_loaded) + self.set_loader.error_occurred.connect(self._on_set_load_error) + self.set_loader.start() def _on_armors_loaded(self, armors: List[NexusArmor]): """Handle loaded armors.""" self.all_armors = armors - self.status_label.setText(f"Loaded {len(armors)} armors from Entropia Nexus") + self.status_label.setText(f"Loaded {len(armors)} armors and {len(self.all_armor_sets)} armor sets from Entropia Nexus") self.progress.setRange(0, 100) self.progress.setValue(100) self._populate_results(armors) @@ -128,8 +198,17 @@ class ArmorSelectorDialog(QDialog): self.progress.setRange(0, 100) self.progress.setValue(0) + def _on_armor_sets_loaded(self, armor_sets: List[NexusArmorSet]): + """Handle loaded armor sets.""" + self.all_armor_sets = armor_sets + self._populate_sets(armor_sets) + + def _on_set_load_error(self, error: str): + """Handle armor set load error.""" + self.status_label.setText(f"Error loading armor sets: {error}") + def _populate_results(self, armors: List[NexusArmor]): - """Populate results tree.""" + """Populate results tree with individual armor pieces.""" self.results_tree.clear() for armor in armors: item = QTreeWidgetItem() @@ -150,8 +229,32 @@ class ArmorSelectorDialog(QDialog): item.setData(0, Qt.ItemDataRole.UserRole, armor) self.results_tree.addTopLevelItem(item) + def _populate_sets(self, armor_sets: List[NexusArmorSet]): + """Populate sets tree with full armor sets.""" + self.sets_tree.clear() + for armor_set in armor_sets: + item = QTreeWidgetItem() + item.setText(0, armor_set.name) + item.setText(1, str(len(armor_set.pieces))) + + # Get total protection from the set + prot = armor_set.total_protection + item.setText(2, str(prot.impact)) + item.setText(3, str(prot.cut)) + item.setText(4, str(prot.stab)) + item.setText(5, str(prot.burn)) + item.setText(6, str(prot.cold)) + item.setText(7, str(prot.acid)) + item.setText(8, str(prot.electric)) + + total = prot.get_total() + item.setText(9, str(total)) + + item.setData(0, Qt.ItemDataRole.UserRole, armor_set) + self.sets_tree.addTopLevelItem(item) + def _on_search(self, text: str): - """Handle search text change.""" + """Handle search text change for individual pieces.""" if not text: self._populate_results(self.all_armors) return @@ -162,26 +265,70 @@ class ArmorSelectorDialog(QDialog): self.status_label.setText(f"Found {len(filtered)} armors matching '{text}'") def _clear_search(self): - """Clear search.""" + """Clear search for individual pieces.""" self.search_input.clear() self._populate_results(self.all_armors) self.status_label.setText(f"Showing all {len(self.all_armors)} armors") + def _on_set_search(self, text: str): + """Handle search text change for armor sets.""" + if not text: + self._populate_sets(self.all_armor_sets) + return + + query = text.lower() + filtered = [s for s in self.all_armor_sets if query in s.name.lower()] + self._populate_sets(filtered) + self.status_label.setText(f"Found {len(filtered)} armor sets matching '{text}'") + + def _clear_set_search(self): + """Clear search for armor sets.""" + self.set_search_input.clear() + self._populate_sets(self.all_armor_sets) + self.status_label.setText(f"Showing all {len(self.all_armor_sets)} armor sets") + + def _on_tab_changed(self, index: int): + """Handle tab change - clear selection and update UI.""" + self.selected_armor = None + self.selected_armor_set = None + self.ok_button.setEnabled(False) + self._clear_preview() + + def _clear_preview(self): + """Clear preview panel.""" + self.preview_name.setText("-") + self.preview_type.setText("-") + self.preview_durability.setText("-") + self.preview_protection.setText("-") + def _on_selection_changed(self): - """Handle selection change.""" + """Handle selection change for individual pieces.""" items = self.results_tree.selectedItems() if items: self.selected_armor = items[0].data(0, Qt.ItemDataRole.UserRole) + self.selected_armor_set = None self.ok_button.setEnabled(True) self._update_preview(self.selected_armor) else: self.selected_armor = None self.ok_button.setEnabled(False) + def _on_set_selection_changed(self): + """Handle selection change for armor sets.""" + items = self.sets_tree.selectedItems() + if items: + self.selected_armor_set = items[0].data(0, Qt.ItemDataRole.UserRole) + self.selected_armor = None + self.ok_button.setEnabled(True) + self._update_set_preview(self.selected_armor_set) + else: + self.selected_armor_set = None + self.ok_button.setEnabled(False) + def _update_preview(self, armor: NexusArmor): - """Update preview panel.""" + """Update preview panel for individual armor.""" self.preview_name.setText(armor.name) - self.preview_type.setText(armor.type) + self.preview_type.setText(f"Piece: {armor.type}") self.preview_durability.setText(f"{armor.durability} (~{Decimal('20') / (Decimal('1') - Decimal(armor.durability) / Decimal('100000')):.2f} hp/pec)") prot_parts = [] @@ -195,15 +342,68 @@ class ArmorSelectorDialog(QDialog): prot_parts.append(f"Burn:{armor.protection_burn}") if armor.protection_cold > 0: prot_parts.append(f"Cold:{armor.protection_cold}") + if armor.protection_acid > 0: + prot_parts.append(f"Acid:{armor.protection_acid}") + if armor.protection_electric > 0: + prot_parts.append(f"Elec:{armor.protection_electric}") self.preview_protection.setText(", ".join(prot_parts) if prot_parts else "None") + def _update_set_preview(self, armor_set: NexusArmorSet): + """Update preview panel for armor set.""" + self.preview_name.setText(armor_set.name) + self.preview_type.setText(f"Full Set ({len(armor_set.pieces)} pieces)") + + # Show pieces in the set + pieces_text = "\n".join([f" • {piece}" for piece in armor_set.pieces[:7]]) + if len(armor_set.pieces) > 7: + pieces_text += f"\n ... and {len(armor_set.pieces) - 7} more" + self.preview_durability.setText(f"Pieces:\n{pieces_text}") + + # Show total protection + prot = armor_set.total_protection + prot_parts = [] + if prot.impact > 0: + prot_parts.append(f"Imp:{prot.impact}") + if prot.cut > 0: + prot_parts.append(f"Cut:{prot.cut}") + if prot.stab > 0: + prot_parts.append(f"Stab:{prot.stab}") + if prot.burn > 0: + prot_parts.append(f"Burn:{prot.burn}") + if prot.cold > 0: + prot_parts.append(f"Cold:{prot.cold}") + if prot.acid > 0: + prot_parts.append(f"Acid:{prot.acid}") + if prot.electric > 0: + prot_parts.append(f"Elec:{prot.electric}") + + total_prot = f"Total: {prot.get_total()}\n" + ", ".join(prot_parts) + if armor_set.set_bonus: + total_prot += f"\n\n✨ Set Bonus: {armor_set.set_bonus}" + self.preview_protection.setText(total_prot) + def _on_double_click(self, item, column): - """Handle double click.""" - self._on_accept() + """Handle double click on individual armor.""" + if self.selected_armor: + self._on_accept() + + def _on_set_double_click(self, item, column): + """Handle double click on armor set.""" + if self.selected_armor_set: + self._on_accept() def _on_accept(self): """Handle OK button.""" + if self.tabs.currentIndex() == 0 and self.selected_armor: + # Individual piece tab + self.armor_selected.emit(self.selected_armor) + self.accept() + elif self.tabs.currentIndex() == 1 and self.selected_armor_set: + # Full sets tab + self.armor_set_selected.emit(self.selected_armor_set) + self.accept() + """Handle OK button.""" if self.selected_armor: self.armor_selected.emit(self.selected_armor) self.accept() diff --git a/ui/loadout_manager.py b/ui/loadout_manager.py index 8625fbe..058dc04 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, NexusArmorSet, NexusHealingTool, NexusPlate, NexusAttachment, NexusEnhancer, NexusRing, NexusClothing, NexusPet ) from core.attachments import ( @@ -1590,20 +1590,129 @@ class LoadoutManagerDialog(QDialog): from ui.armor_selector import ArmorSelectorDialog dialog = ArmorSelectorDialog(self) dialog.armor_selected.connect(self._on_api_armor_selected) + dialog.armor_set_selected.connect(self._on_api_armor_set_selected) dialog.exec() def _on_api_armor_selected(self, armor: NexusArmor): - """Handle armor selection from API.""" + """Handle individual armor piece selection from API.""" # Store selected armor info self._selected_api_armor = armor QMessageBox.information( self, "Armor Selected", f"Selected: {armor.name}\n" + f"Type: {armor.type}\n" f"Durability: {armor.durability}\n" f"Protection: Impact {armor.protection_impact}, Cut {armor.protection_cut}, Stab {armor.protection_stab}" ) + def _on_api_armor_set_selected(self, armor_set: 'NexusArmorSet'): + """Handle full armor set selection from API.""" + from core.nexus_full_api import get_nexus_api + + # Get all armors to find matching pieces + api = get_nexus_api() + all_armors = api.get_all_armors() + + # Map slot names to armor slot widgets + slot_mapping = { + 'head': self.armor_slots[ArmorSlot.HEAD], + 'torso': self.armor_slots[ArmorSlot.TORSO], + 'harness': self.armor_slots[ArmorSlot.TORSO], + 'arms': self.armor_slots[ArmorSlot.ARMS], + 'arm guards': self.armor_slots[ArmorSlot.ARMS], + 'hands': self.armor_slots[ArmorSlot.HANDS], + 'gloves': self.armor_slots[ArmorSlot.HANDS], + 'legs': self.armor_slots[ArmorSlot.LEGS], + 'thigh guards': self.armor_slots[ArmorSlot.LEGS], + 'shins': self.armor_slots[ArmorSlot.SHINS], + 'shin guards': self.armor_slots[ArmorSlot.SHINS], + 'feet': self.armor_slots[ArmorSlot.FEET], + 'foot guards': self.armor_slots[ArmorSlot.FEET], + } + + pieces_found = 0 + pieces_not_found = [] + + # Try to find each piece in the set + for piece_name in armor_set.pieces: + # Find the armor in the API results + matching_armor = None + for armor in all_armors: + if armor.name == piece_name: + matching_armor = armor + break + + if matching_armor: + # Determine slot from armor type + armor_type = matching_armor.type.lower() + slot_widget = None + + # Find matching slot + for type_key, widget in slot_mapping.items(): + if type_key in armor_type: + slot_widget = widget + break + + if slot_widget: + # Create ArmorPiece from NexusArmor + from core.armor_system import ArmorPiece + piece = ArmorPiece( + item_id=matching_armor.item_id, + name=matching_armor.name, + slot=self._get_slot_from_type(armor_type), + protection=ProtectionProfile( + impact=matching_armor.protection_impact, + cut=matching_armor.protection_cut, + stab=matching_armor.protection_stab, + burn=matching_armor.protection_burn, + cold=matching_armor.protection_cold, + acid=matching_armor.protection_acid, + electric=matching_armor.protection_electric, + ), + durability=matching_armor.durability, + decay_per_hp=Decimal("0.05") * (1 - matching_armor.durability / 100000) + ) + slot_widget.set_piece(piece) + pieces_found += 1 + else: + pieces_not_found.append(f"{piece_name} (unknown slot)") + else: + pieces_not_found.append(piece_name) + + # Show summary + msg = f"Equipped armor set: {armor_set.name}\n\n" + msg += f"✓ Found and equipped {pieces_found}/{len(armor_set.pieces)} pieces\n" + + if pieces_not_found: + msg += f"\n⚠ Not found:\n" + "\n".join([f" • {p}" for p in pieces_not_found]) + + if armor_set.set_bonus: + msg += f"\n\n✨ Set Bonus: {armor_set.set_bonus}" + + QMessageBox.information(self, "Armor Set Equipped", msg) + self._update_calculations() + + def _get_slot_from_type(self, armor_type: str) -> 'ArmorSlot': + """Map armor type string to ArmorSlot enum.""" + armor_type = armor_type.lower() + if 'head' in armor_type or 'helmet' in armor_type: + return ArmorSlot.HEAD + elif 'torso' in armor_type or 'harness' in armor_type: + return ArmorSlot.TORSO + elif 'arm' in armor_type: + return ArmorSlot.ARMS + elif 'hand' in armor_type or 'glove' in armor_type: + return ArmorSlot.HANDS + elif 'thigh' in armor_type or ('leg' in armor_type and 'shin' not in armor_type): + return ArmorSlot.LEGS + elif 'shin' in armor_type: + return ArmorSlot.SHINS + elif 'foot' in armor_type or 'boot' in armor_type: + return ArmorSlot.FEET + else: + return ArmorSlot.TORSO # Default + def _on_select_healing_from_api(self): """Open healing tool selector dialog from Nexus API.""" from ui.healing_selector import HealingSelectorDialog